summaryrefslogtreecommitdiffstats
path: root/src/mainwindow.cpp
diff options
context:
space:
mode:
authorDanilo M. <danix@danix.xyz>2026-08-04 19:54:40 +0200
committerDanilo M. <danix@danix.xyz>2026-08-04 19:54:40 +0200
commitbd1b907ba22464bb869bcc10cc9327ed74c44791 (patch)
treec593b55f9a5fd4912104c33cddf59f894c9308aa /src/mainwindow.cpp
parenta1f11aa861136fca3ec464e0a6268abeed1d8109 (diff)
downloadqtmaildir-bd1b907ba22464bb869bcc10cc9327ed74c44791.tar.gz
qtmaildir-bd1b907ba22464bb869bcc10cc9327ed74c44791.zip
feat(ui): make Delete a toggle, and expire transient status messages
Items 16 and 33. Delete now removes the `deleted` tag when every selected thread already carries it, so pressing it twice puts a thread back. One direction for the whole selection, never per row: toggling each independently would leave a single keystroke with the selection in two states, which is worse than either outcome. Status messages are classified rather than blanket-timed, which is the substance of item 33. Events expire after six seconds and fall back to the last query's thread count: "Sync complete", "Nothing to undo", the skip notice, the per-action "Archive: 3 threads". State does not expire: "Searching...", "Syncing...", the selection count, and "Sync failed (exit N)", because an error must not vanish before it is read. A test caught a mistake in that routing. Making the per-action message transient armed the timer during select-all, since tagSelected() runs on a selection onSelectionChanged() had just described, and the count would then be replaced while it was still true. Writing the count now cancels any transient still counting down. QStatusBar::showMessage() would give the same behaviour but the label is added with addWidget() beside permanent widgets, so adopting it means reworking that arrangement. One timer beside the label is the smaller change. Both fixes verified by reverting them and watching the tests fail. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Diffstat (limited to 'src/mainwindow.cpp')
-rw-r--r--src/mainwindow.cpp80
1 files changed, 69 insertions, 11 deletions
diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp
index b62d8d7..a7f9b40 100644
--- a/src/mainwindow.cpp
+++ b/src/mainwindow.cpp
@@ -337,6 +337,26 @@ void MainWindow::buildUi()
m_statusLabel->setObjectName(QStringLiteral("statusMessage"));
statusBar()->addWidget(m_statusLabel);
+ // Transient messages describe an EVENT and go stale: "Sync complete" reads
+ // as the present tense until something else overwrites it. State messages,
+ // the selection count above all, describe what is true right now and must
+ // not expire while it stays true, so only showTransientStatus() arms this.
+ //
+ // ponytail: one timer beside the label, not QStatusBar::showMessage().
+ // That would mean moving off addWidget() and reworking the permanent
+ // widgets beside it, for the same behaviour.
+ m_statusTimer = new QTimer(this);
+ m_statusTimer->setObjectName(QStringLiteral("statusTimer"));
+ m_statusTimer->setSingleShot(true);
+ m_statusTimer->setInterval(kStatusMessageMs);
+ connect(m_statusTimer, &QTimer::timeout, this, [this]() {
+ // Only take back a message this timer armed. Anything written since is
+ // newer and more relevant than the default.
+ if (m_statusLabel->text() == m_transientMessage)
+ m_statusLabel->setText(m_defaultStatus);
+ m_transientMessage.clear();
+ });
+
// Beside the sync status rather than as a widget competing with it: the two
// say related things and reading them apart would be worse than reading
// them together.
@@ -428,7 +448,7 @@ void MainWindow::buildUi()
}
connect(m_syncButton, &QPushButton::clicked, this, [this]() {
if (!m_sync->start()) {
- m_statusLabel->setText(tr("Sync already running"));
+ showTransientStatus(tr("Sync already running"));
return;
}
// Fresh run, fresh output: leaving the previous run's lines in place
@@ -594,8 +614,29 @@ void MainWindow::registerActions()
tagSelected({}, { QStringLiteral("inbox") }, tr("Archive"));
});
addAction(QStringLiteral("delete"), tr("&Delete"),
- tr("Add the deleted tag"), [this]() {
- tagSelected({ QStringLiteral("deleted") }, {}, tr("Delete"));
+ tr("Add or remove the deleted tag"), [this]() {
+ // A toggle, like toggle_unread: pressing Delete twice is the natural
+ // way to say "no, put it back", and adding a tag that is already there
+ // is a no-op the user cannot see.
+ //
+ // One direction for the WHOLE selection. Toggling each thread
+ // independently would leave one keystroke with the selection in two
+ // states, which is worse than either outcome, so undelete only when
+ // every selected thread is already deleted.
+ const QModelIndexList rows =
+ m_threadView->selectionModel()->selectedRows();
+ bool allDeleted = !rows.isEmpty();
+ for (const QModelIndex &index : rows) {
+ if (!m_model->threadAt(index.row()).isDeleted()) {
+ allDeleted = false;
+ break;
+ }
+ }
+
+ if (allDeleted)
+ tagSelected({}, { QStringLiteral("deleted") }, tr("Undelete"));
+ else
+ tagSelected({ QStringLiteral("deleted") }, {}, tr("Delete"));
});
addAction(QStringLiteral("spam"), tr("Mark &spam"),
tr("Add spam and remove inbox"), [this]() {
@@ -675,7 +716,7 @@ void MainWindow::registerActions()
if (m_undoStack.canUndo())
m_undoStack.undo();
else
- m_statusLabel->setText(tr("Nothing to undo"));
+ showTransientStatus(tr("Nothing to undo"));
});
addAction(QStringLiteral("sync"), tr("&Sync"),
tr("Run the configured sync command"), [this]() {
@@ -1061,7 +1102,11 @@ void MainWindow::onQueryFinished(int total, quint64 generation)
{
if (generation != m_generation)
return;
- m_statusLabel->setText(tr("%n thread(s)", "", total));
+ // The query's own result is what the bar says when nothing more pressing
+ // is happening, so a transient message falls back to it rather than to
+ // nothing.
+ m_defaultStatus = tr("%n thread(s)", "", total);
+ m_statusLabel->setText(m_defaultStatus);
}
void MainWindow::showThreadContextMenu(const QPoint &pos)
@@ -1112,6 +1157,12 @@ void MainWindow::onSelectionChanged()
m_selectionMessage = tr("%n thread(s) selected", "", selected);
m_statusLabel->setText(m_selectionMessage);
+ // State, not an event: it must persist while the selection does. Cancel any
+ // transient message still counting down, or that timer fires and replaces a
+ // count that is still true.
+ m_statusTimer->stop();
+ m_transientMessage.clear();
+
// Ctrl+click and selectAll() reach a multi-row selection without moving
// current, so onThreadSelected never runs and its guard never fires. The
// pane and the pending timer have to be dealt with here as well.
@@ -1251,7 +1302,7 @@ void MainWindow::onSyncFinished(bool success, int exitCode)
m_pendingEdits = 0;
updatePendingIndicator();
- m_statusLabel->setText(tr("Sync complete"));
+ showTransientStatus(tr("Sync complete"));
if (m_syncingForExit) {
// The work is safely across, so finish the quit the user asked for.
@@ -1274,8 +1325,8 @@ void MainWindow::onSyncFinished(bool success, int exitCode)
// Not a failure: another run holds the lock and is doing the work.
// The user's cron fires every ten minutes, so a click landing inside
// one is routine and must not raise an error or the log pane.
- m_statusLabel->setText(tr("A sync is already running (started "
- "elsewhere); this one was skipped"));
+ showTransientStatus(tr("A sync is already running (started "
+ "elsewhere); this one was skipped"));
if (m_syncingForExit) {
// The other run is syncing, but this application cannot see when
@@ -1374,12 +1425,19 @@ void MainWindow::onExternalSyncStateChanged(SyncMonitor::State state)
// be read, so nothing was observed, and "sync finished" would be a claim
// this cannot support.
if (state == SyncMonitor::State::Idle) {
- m_statusLabel->setText(
+ showTransientStatus(
tr("Background sync completed. Press Enter in the query bar to "
"refresh."));
}
}
+void MainWindow::showTransientStatus(const QString &text)
+{
+ m_transientMessage = text;
+ m_statusLabel->setText(text);
+ m_statusTimer->start();
+}
+
void MainWindow::setSyncBusy(bool busy)
{
m_localSyncBusy = busy;
@@ -1501,7 +1559,7 @@ void MainWindow::editTagsOnSelection()
const QModelIndexList rows =
m_threadView->selectionModel()->selectedRows();
if (rows.isEmpty()) {
- m_statusLabel->setText(tr("Select a thread first"));
+ showTransientStatus(tr("Select a thread first"));
return;
}
@@ -1551,7 +1609,7 @@ void MainWindow::tagSelected(const QStringList &add, const QStringList &remove,
m_undoStack.push(new ThreadTagCommand(this, threadIds, add, remove,
description));
- m_statusLabel->setText(
+ showTransientStatus(
tr("%1: %n thread(s)", "", threadIds.size()).arg(description));
}