summaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/messageview.cpp106
-rw-r--r--src/messageview.h32
2 files changed, 135 insertions, 3 deletions
diff --git a/src/messageview.cpp b/src/messageview.cpp
index 2dba6d1..a7d340a 100644
--- a/src/messageview.cpp
+++ b/src/messageview.cpp
@@ -33,6 +33,7 @@
#include <QPushButton>
#include <QStandardPaths>
#include <QtNumeric>
+#include <QResizeEvent>
#include <QTimer>
#include <QTreeWidget>
#include <QVBoxLayout>
@@ -180,13 +181,39 @@ MessageView::MessageView(QWidget *parent)
{ QWebEnginePage::CopyImageUrlToClipboard, QT_TR_NOOP("Copied the image address") },
};
+ // The toast itself, a child of the pane rather than a layout item: it
+ // floats OVER the message, so nothing reflows when it appears and the text
+ // the user just copied does not jump under the cursor.
+ m_copyToast = new QLabel(this);
+ m_copyToast->setObjectName(QStringLiteral("copyToast"));
+ // Plain text, deliberately. The strings are ours, but a label that guesses
+ // under Qt::AutoText is one careless change away from rendering markup,
+ // and this pane's whole job is displaying input from strangers.
+ m_copyToast->setTextFormat(Qt::PlainText);
+ m_copyToast->setAlignment(Qt::AlignCenter);
+ // Opaque, or the message underneath shows through and the confirmation is
+ // unreadable over exactly the content it is confirming.
+ m_copyToast->setAutoFillBackground(true);
+ applyToastPalette();
+ m_copyToast->hide();
+
+ m_copyToastTimer = new QTimer(this);
+ m_copyToastTimer->setSingleShot(true);
+ m_copyToastTimer->setInterval(kToastMs);
+ connect(m_copyToastTimer, &QTimer::timeout,
+ m_copyToast, &QWidget::hide);
+
for (const auto &report : kCopyReports) {
QAction *action = m_view->page()->action(report.action);
if (!action)
continue;
const QString message = tr(report.message);
connect(action, &QAction::triggered, this, [this, message]() {
- emit statusMessage(message);
+ // In the pane, at the user's request, rather than in the status
+ // bar item 115 first used: a copy happens here, and the status bar
+ // is at the other end of the window, so the confirmation was
+ // landing far from the gesture that caused it.
+ showCopyToast(message);
});
}
@@ -773,14 +800,87 @@ void MessageView::showDetailsDialog()
dialog.exec();
}
+void MessageView::applyToastPalette()
+{
+ if (!m_copyToast)
+ return;
+
+ // From the PALETTE, never hardcoded. The pane already re-renders its
+ // document on a PaletteChange so the message follows the desktop theme;
+ // a toast painted in fixed colours would be the one part of the pane that
+ // did not, and would be unreadable under whichever theme it was not
+ // designed for.
+ //
+ // ToolTipBase/ToolTipText specifically: a toast IS a tooltip in everything
+ // but how it is triggered, so this is the role the theme already styles
+ // for "small transient thing floating over content".
+ QPalette toastPalette = m_copyToast->palette();
+ toastPalette.setColor(QPalette::Window,
+ palette().color(QPalette::ToolTipBase));
+ toastPalette.setColor(QPalette::WindowText,
+ palette().color(QPalette::ToolTipText));
+ m_copyToast->setPalette(toastPalette);
+}
+
+void MessageView::showCopyToast(const QString &text)
+{
+ if (!m_copyToast)
+ return;
+
+ // A checkmark, per the user's description. Prepended here rather than
+ // baked into each string so the four messages stay translatable as plain
+ // sentences and the mark cannot go missing from one of them.
+ m_copyToast->setText(QStringLiteral("\u2713 ") + text);
+ m_copyToast->adjustSize();
+ positionToast();
+ m_copyToast->show();
+ m_copyToast->raise();
+
+ // Restarted, not merely started: a second copy while the first toast is up
+ // must get its own full reading time rather than inheriting what is left
+ // of the previous countdown.
+ m_copyToastTimer->start();
+}
+
+void MessageView::positionToast()
+{
+ if (!m_copyToast)
+ return;
+
+ // Anchored to the pane's bottom right, inset by a margin so it does not
+ // touch the edges. Placed against the WIDGET rather than against m_view:
+ // the web view's geometry shifts as the header grows and the attachment
+ // bar appears, and the toast should sit in the same corner regardless.
+ constexpr int margin = 12;
+ const QSize size = m_copyToast->sizeHint();
+ m_copyToast->setGeometry(width() - size.width() - margin,
+ height() - size.height() - margin,
+ size.width(), size.height());
+}
+
+void MessageView::resizeEvent(QResizeEvent *event)
+{
+ QWidget::resizeEvent(event);
+
+ // A hand-placed child does not follow its parent the way a laid-out one
+ // does, so without this the toast stays where the pane used to end.
+ positionToast();
+}
+
void MessageView::changeEvent(QEvent *event)
{
QWidget::changeEvent(event);
// Only when there is something to re-render: rendering an empty item list
// would replace a deliberately blank pane with an empty document.
- if (event->type() == QEvent::PaletteChange && !m_items.isEmpty())
- render();
+ if (event->type() == QEvent::PaletteChange) {
+ // The toast follows the theme too, and unconditionally: unlike the
+ // document it has no items to guard against, and a toast left in the
+ // old theme's colours would be unreadable the first time it appeared.
+ applyToastPalette();
+ if (!m_items.isEmpty())
+ render();
+ }
}
void MessageView::render()
diff --git a/src/messageview.h b/src/messageview.h
index 0b18769..09fc905 100644
--- a/src/messageview.h
+++ b/src/messageview.h
@@ -20,6 +20,7 @@
#include <QList>
#include <QUrl>
+#include <QTimer>
#include <QWidget>
#include "htmlbuilder.h"
@@ -97,6 +98,10 @@ public:
/// The body zoom factor. Chromium's own range is roughly 0.25 to 5.0;
/// these are tighter, since a pane at either extreme is unusable and the
/// only visible way back is a menu entry the user cannot read.
+ /// How long the copy confirmation stays up. Long enough to read four
+ /// words, short enough that it is gone before it becomes furniture.
+ static constexpr int kToastMs = 2000;
+
static constexpr qreal kMinZoom = 0.5;
static constexpr qreal kMaxZoom = 3.0;
static constexpr qreal kDefaultZoom = 1.0;
@@ -257,6 +262,9 @@ protected:
/// until the next selection.
void changeEvent(QEvent *event) override;
+ /// Keeps the hand-placed toast anchored to the bottom right.
+ void resizeEvent(QResizeEvent *event) override;
+
private:
void render();
void updateHeader();
@@ -304,6 +312,30 @@ private:
/// different wording or a different pair of operations.
void addSearchEntries(QMenu *menu, const QList<SearchOffer> &offers);
+ /// The copy confirmation, floating over the web view in the pane's bottom
+ /// right rather than in the window's status bar.
+ ///
+ /// A CHILD placed by hand, never a layout item: it must sit on top of the
+ /// message rather than take a strip away from it, so nothing reflows when
+ /// it appears and the text the user just copied does not jump. That is
+ /// also why positionToast() exists and why resizeEvent() is overridden;
+ /// a hand-placed child does not follow its parent the way a laid-out one
+ /// does.
+ QLabel *m_copyToast = nullptr;
+ QTimer *m_copyToastTimer = nullptr;
+
+ /// Paints the toast in the theme's tooltip colours.
+ ///
+ /// Re-applied on a PaletteChange, so it follows the desktop theme the way
+ /// the rendered document already does.
+ void applyToastPalette();
+
+ /// Shows the toast with `text` and restarts its countdown.
+ void showCopyToast(const QString &text);
+
+ /// Puts the toast in the pane's bottom right, inside the margins.
+ void positionToast();
+
QList<ThreadRenderItem> m_items;
bool m_preferHtml = true;