From 334b510e2673a6ab3875ffa7a4c5b3b2dd09a369 Mon Sep 17 00:00:00 2001 From: "Danilo M." Date: Fri, 7 Aug 2026 17:51:52 +0200 Subject: feat(ui): fill the blank message pane with a branded placeholder An empty right pane said nothing, and multi-select made it a routine sight. It now carries the wordmark, thread counts that run their query when clicked, and a sync line that appears only when something needs attention. Rendered into the existing web view as a third document shape, so there is one document path and one set of security rules. The brand palette is a deliberate exception to deriving colours from the desktop theme, since a logo is brand rather than chrome; the theme still picks which of the two sets is used. Counts refresh when the pane is about to show rather than in the background: one goes stale the moment a tag is edited, and refreshing one nobody is looking at is work for nothing. A generation counter discards a superseded reply, and a late answer cannot repaint over an opened thread. The helper lines are real links because JavaScript is off in this profile. The handler is gated on the placeholder actually being displayed, so the same URL inside a message body is dropped: a stranger's mail must not drive the thread list, even to run a harmless query. Three defects found while building, all silent: - Every CSS percentage was invalid. QString::arg does not collapse "%%" into "%", so the document carried "50%%" and the browser dropped each declaration holding one, disabling the mask, the glow and both radial gradients while still rendering something plausible. Substitution is by named token now, which cannot collide with a percent sign. - A geometry probe endorsed the layout while that was live, because it measured only properties without percentages. - The font test passed against a build with one face missing, since the other satisfied both of its checks on its own. The mockup's light values needed correcting against a real pane: the grid vanished at a 2% luminance step on white, and the glow subtracts light there rather than adding it, washing the pane. Strength only, not hue. --- src/messageview.cpp | 75 ++++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 72 insertions(+), 3 deletions(-) (limited to 'src/messageview.cpp') diff --git a/src/messageview.cpp b/src/messageview.cpp index 10b21a9..b804d37 100644 --- a/src/messageview.cpp +++ b/src/messageview.cpp @@ -43,12 +43,15 @@ #include #include +#include +#include #include "cidschemehandler.h" #include "htmlbuilder.h" #include "requestinterceptor.h" #include "tagstrip.h" #include "threadcidmap.h" +#include "version.h" namespace { @@ -56,8 +59,11 @@ namespace { class MessagePage : public QWebEnginePage { public: - MessagePage(QWebEngineProfile *profile, QObject *parent) - : QWebEnginePage(profile, parent) {} + using QueryHandler = std::function; + + MessagePage(QWebEngineProfile *profile, QObject *parent, + QueryHandler onQuery) + : QWebEnginePage(profile, parent), m_onQuery(std::move(onQuery)) {} protected: bool acceptNavigationRequest(const QUrl &url, NavigationType type, @@ -78,6 +84,24 @@ protected: return true; if (type == NavigationTypeLinkClicked) { + // The placeholder's helper lines. JavaScript is off in this + // profile, so a clickable count can only be a real link, and this + // is where it is turned back into an action. + // + // The handler decides whether to accept it, not this function: the + // view refuses unless the placeholder is what is actually + // displayed, so a qtmaildir-query: link inside a message body is + // dropped rather than handed a query to run. + if (url.scheme() == QLatin1String("qtmaildir-query")) { + // path() already percent-decodes; verified against Qt 6.11, + // which returns tag:unread for qtmaildir-query:tag%3Aunread. + // Decoding it a second time would corrupt a query carrying a + // literal '%', which notmuch accepts in a quoted term. + if (m_onQuery) + m_onQuery(url.path()); + return false; + } + QDesktopServices::openUrl(url); return false; } @@ -86,6 +110,9 @@ protected: // navigation would replace the pane, which no message may do. return !isMainFrame; } + +private: + QueryHandler m_onQuery; }; } // namespace @@ -105,7 +132,16 @@ MessageView::MessageView(QWidget *parent) m_profile->installUrlSchemeHandler(QByteArrayLiteral("cid"), m_cidHandler); m_view = new QWebEngineView(this); - m_view->setPage(new MessagePage(m_profile, m_view)); + // The gate the queryRequested() documentation describes: a helper link is + // only honoured while the placeholder is what is on screen, so the same + // URL inside a message body reaches here and is dropped. + m_view->setPage(new MessagePage(m_profile, m_view, + [this](const QString &query) { + if (!m_showingPlaceholder) + return false; + emit queryRequested(query); + return true; + })); QWebEngineSettings *settings = m_view->settings(); settings->setAttribute(QWebEngineSettings::JavascriptEnabled, false); @@ -196,9 +232,40 @@ void MessageView::setDocument(const QString &html) m_view->setHtml(html, documentUrl()); } +void MessageView::showPlaceholder( + const QList &helpers) +{ + // Everything clear() drops, dropped again: this is reachable directly and + // must not leave a previous thread's parts serveable behind the logo. + m_items.clear(); + m_tagStrip->setTags({}); + m_cidHandler->setParts({}); + m_interceptor->setAllowedCids({}); + m_interceptor->resetForNewMessage(); + + m_headerLabel->clear(); + m_detailsButton->hide(); + m_blockedLabel->hide(); + m_loadRemoteButton->hide(); + rebuildAttachmentBar(); + + // Set before the document loads, not after: acceptNavigationRequest reads + // it, and a click cannot arrive before setDocument() returns, but ordering + // it this way makes that independent of how the load is scheduled. + m_showingPlaceholder = true; + + // The widget's own palette, not qApp's, for the reason the render path + // uses it: a style sheet or a themed parent can give this pane different + // colours from the application. + setDocument(HtmlBuilder::buildPlaceholder( + helpers, QStringLiteral(QTMAILDIR_VERSION), + HtmlBuilder::brandPaletteFrom(palette()))); +} + void MessageView::clear() { m_items.clear(); + m_showingPlaceholder = false; m_tagStrip->setTags({}); // No thread is displayed, so nothing may be served or allowed. Without @@ -221,6 +288,7 @@ void MessageView::showThread(const QList &items) { m_items = items; m_preferHtml = true; + m_showingPlaceholder = false; // Every thread starts from a clean policy: no remote grant carries over. m_interceptor->resetForNewMessage(); @@ -258,6 +326,7 @@ void MessageView::showThread(const QList &items) void MessageView::showError(const QString &text, const QString &filePath) { m_items.clear(); + m_showingPlaceholder = false; // An error card references nothing, so the policy is emptied rather than // left holding the previous thread's parts. -- cgit v1.2.3