diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/messageview.cpp | 106 | ||||
| -rw-r--r-- | src/messageview.h | 32 |
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; |
