From e187db1b84fadaf67bb8bee91460fbaea6ce8efb Mon Sep 17 00:00:00 2001 From: "Danilo M." Date: Wed, 19 Aug 2026 11:34:58 +0200 Subject: feat(pane): move the copy confirmation into the message pane The user's preference after seeing item 115 ship: a small transient in the bottom right of the pane with a checkmark, rather than a status bar message at the far end of the window. A copy happens in the pane, so the confirmation belongs there. Three properties are load-bearing and each has a mutation that fails. The toast is a hand-placed CHILD rather than a layout item, because it floats over the message instead of taking a strip away from it: nothing reflows when it appears and the text just copied does not jump. That is why resizeEvent() is overridden, since a hand-placed child does not follow its parent. It is autoFillBackground and painted from the theme's ToolTipBase/ToolTipText, so it stays readable over a rendered message and follows the desktop theme the way the document already does. And its timer is restarted rather than started, so a second copy gets its own full reading time instead of inheriting what is left of the first. The resize test was wrong on its first draft and passed against the mutation it exists to catch. It grew the pane, which moves the right and bottom edges away, so a toast left at its old position still satisfied "inside the pane"; measured green with the reposition deleted. It shrinks now, where a stale position lands outside the new rect, which is also what the user would see. No new strings: the four messages are unchanged, only where they appear. Co-Authored-By: Claude Opus 5 --- src/messageview.cpp | 106 ++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 103 insertions(+), 3 deletions(-) (limited to 'src/messageview.cpp') 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 #include #include +#include #include #include #include @@ -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() -- cgit v1.2.3