summaryrefslogtreecommitdiffstats
path: root/src/mainwindow.cpp
diff options
context:
space:
mode:
authorDanilo M. <danix@danix.xyz>2026-08-07 17:51:52 +0200
committerDanilo M. <danix@danix.xyz>2026-08-07 17:51:52 +0200
commit334b510e2673a6ab3875ffa7a4c5b3b2dd09a369 (patch)
tree59b38af32af41d629a0c02ecd8e0fcfcddbb60e6 /src/mainwindow.cpp
parent6003bab2d4c491a857b7a728f2f23c719723ef1a (diff)
downloadqtmaildir-334b510e2673a6ab3875ffa7a4c5b3b2dd09a369.tar.gz
qtmaildir-334b510e2673a6ab3875ffa7a4c5b3b2dd09a369.zip
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.
Diffstat (limited to 'src/mainwindow.cpp')
-rw-r--r--src/mainwindow.cpp101
1 files changed, 101 insertions, 0 deletions
diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp
index 77322bc..5af6624 100644
--- a/src/mainwindow.cpp
+++ b/src/mainwindow.cpp
@@ -577,6 +577,8 @@ void MainWindow::buildUi()
m_messageView->setTagColors(&m_tagColors);
connect(m_messageView, &MessageView::statusMessage,
this, [this](const QString &text) { m_statusLabel->setText(text); });
+ connect(m_messageView, &MessageView::queryRequested,
+ this, &MainWindow::onPlaceholderQueryRequested);
m_splitter = new QSplitter(Qt::Horizontal, central);
m_splitter->addWidget(m_threadView);
@@ -781,6 +783,7 @@ void MainWindow::registerActions()
// CLAUDE.md.
m_currentThreadId.clear();
m_messageView->clear();
+ showPlaceholderPane();
m_markReadTimer->stop();
m_markReadThreadId.clear();
});
@@ -1060,6 +1063,8 @@ void MainWindow::wireWorker()
this, &MainWindow::onWorkerError);
connect(m_worker, &NotmuchWorker::allTagsReady,
this, &MainWindow::onAllTagsReady);
+ connect(m_worker, &NotmuchWorker::countsReady,
+ this, &MainWindow::onCountsReady);
// A confirmed write clears the pending revert: without this, a later
// unrelated error would roll back a change that actually succeeded.
@@ -1092,6 +1097,92 @@ void MainWindow::onAllTagsReady(const QStringList &tags)
m_queryCompleter->setTags(tags);
}
+namespace {
+
+/// The queries behind the placeholder's helper lines, in render order.
+///
+/// Wire format, deliberately untranslated: `tag:` is notmuch syntax, not user
+/// -facing prose. Only the labels beside them are translated.
+const std::array<const char *, 3> kPlaceholderQueries = {
+ "tag:unread",
+ "tag:flagged",
+ "tag:inbox",
+};
+
+} // namespace
+
+QList<HtmlBuilder::PlaceholderHelper> MainWindow::placeholderHelpers() const
+{
+ QList<HtmlBuilder::PlaceholderHelper> helpers;
+
+ // Empty until the first reply lands. Rendering three zeroes meanwhile
+ // would be worse than rendering nothing: a zero is a claim.
+ if (m_placeholderCounts.size() == int(kPlaceholderQueries.size())) {
+ const QStringList labels = {
+ tr("%n unread", "", m_placeholderCounts.at(0)),
+ tr("%n flagged", "", m_placeholderCounts.at(1)),
+ tr("%n in inbox", "", m_placeholderCounts.at(2)),
+ };
+
+ for (int i = 0; i < labels.size(); ++i) {
+ // A query notmuch could not count yields -1; skip that line rather
+ // than print a negative number at the user.
+ if (m_placeholderCounts.at(i) < 0)
+ continue;
+ helpers.append({ labels.at(i),
+ QString::fromLatin1(kPlaceholderQueries[i]) });
+ }
+ }
+
+ // The sync line, and only when something needs attention: a line that is
+ // always there becomes wallpaper and stops being read.
+ if (m_lastSyncFailed) {
+ helpers.append({ tr("last sync failed"), QString() });
+ } else if (const int pending = pendingEditCount(); pending > 0) {
+ helpers.append({ tr("%n change(s) waiting to sync", "", pending),
+ QString() });
+ }
+
+ return helpers;
+}
+
+void MainWindow::showPlaceholderPane()
+{
+ m_messageView->showPlaceholder(placeholderHelpers());
+
+ QStringList queries;
+ for (const char *query : kPlaceholderQueries)
+ queries.append(QString::fromLatin1(query));
+
+ QMetaObject::invokeMethod(m_worker, "requestCounts", Qt::QueuedConnection,
+ Q_ARG(QStringList, queries),
+ Q_ARG(quint64, ++m_countsGeneration));
+}
+
+void MainWindow::onCountsReady(const QVector<int> &counts, quint64 generation)
+{
+ // A reply for a superseded request carries counts taken before whatever
+ // prompted the newer one, so accepting it would repaint the pane with
+ // older numbers than it already has.
+ if (generation != m_countsGeneration)
+ return;
+
+ m_placeholderCounts = counts;
+
+ // Only repaint what is actually on screen. Without this, a reply arriving
+ // after the user opened a thread would replace the message with the logo.
+ if (m_messageView->showingPlaceholder())
+ m_messageView->showPlaceholder(placeholderHelpers());
+}
+
+void MainWindow::onPlaceholderQueryRequested(const QString &query)
+{
+ // Through the query bar rather than straight to the worker, so the bar
+ // shows what is being displayed and the user can edit it from there.
+ m_queryEdit->setText(query);
+ runCurrentQuery();
+}
+
void MainWindow::showWarnings()
{
const QStringList warnings = m_config.warnings() + m_keyMap.warnings();
@@ -1132,6 +1223,7 @@ void MainWindow::runCurrentQuery()
++m_generation;
m_model->clear();
m_messageView->clear();
+ showPlaceholderPane();
// Undo entries refer to rows that are about to be discarded. The model
// update they invert would be a no-op against the new result set, leaving
@@ -1287,6 +1379,7 @@ void MainWindow::onSelectionChanged()
m_markReadThreadId.clear();
m_currentThreadId.clear();
m_messageView->clear();
+ showPlaceholderPane();
}
void MainWindow::onThreadSelected(const QModelIndex &current,
@@ -1317,6 +1410,7 @@ void MainWindow::onThreadSelected(const QModelIndex &current,
m_markReadThreadId.clear();
m_currentThreadId.clear();
m_messageView->clear();
+ showPlaceholderPane();
return;
}
@@ -1488,6 +1582,7 @@ void MainWindow::onSyncFinished(bool success, int exitCode)
// what failed to put them there.
m_pendingTagEdits.clear();
m_unnettablePendingEdits = 0;
+ m_lastSyncFailed = false;
updatePendingIndicator();
showTransientStatus(tr("Sync complete"));
@@ -1544,6 +1639,12 @@ void MainWindow::onSyncFinished(bool success, int exitCode)
"has been left open."));
}
} else {
+ // Latched until a sync succeeds, so the placeholder's sync line still
+ // says so on the next blank pane rather than only in a status message
+ // the user may not have been looking at. A skipped run does not set
+ // this: it is a branch of its own above, and a skip means another
+ // process is doing the work rather than that the work failed.
+ m_lastSyncFailed = true;
m_statusLabel->setText(tr("Sync failed (exit %1)").arg(exitCode));
m_syncLogPane->show();