summaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/keymap.cpp5
-rw-r--r--src/mainwindow.cpp74
-rw-r--r--src/mainwindow.h26
3 files changed, 105 insertions, 0 deletions
diff --git a/src/keymap.cpp b/src/keymap.cpp
index f27f9a9..a16a700 100644
--- a/src/keymap.cpp
+++ b/src/keymap.cpp
@@ -31,6 +31,7 @@ QStringList KeyMap::knownActions()
QStringLiteral("delete"),
QStringLiteral("spam"),
QStringLiteral("toggle_unread"),
+ QStringLiteral("mark_all_read"),
QStringLiteral("edit_tags"),
QStringLiteral("flag"),
QStringLiteral("focus_query"),
@@ -67,6 +68,10 @@ QList<QPair<QString, QString>> KeyMap::defaultBindings()
{ QStringLiteral("Ctrl+D"), QStringLiteral("delete") },
{ QStringLiteral("Ctrl+Shift+S"), QStringLiteral("spam") },
{ QStringLiteral("Ctrl+U"), QStringLiteral("toggle_unread") },
+ // Shifted against Ctrl+U, which toggles unread on the selection: this
+ // is the same idea applied to the whole view, and the wider-reaching
+ // action takes the harder chord rather than the easier one.
+ { QStringLiteral("Ctrl+Shift+U"), QStringLiteral("mark_all_read") },
{ QStringLiteral("Ctrl+I"), QStringLiteral("flag") },
{ QStringLiteral("Ctrl+T"), QStringLiteral("edit_tags") },
{ QStringLiteral("Ctrl+L"), QStringLiteral("focus_query") },
diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp
index a3b5496..bb11261 100644
--- a/src/mainwindow.cpp
+++ b/src/mainwindow.cpp
@@ -677,6 +677,11 @@ void MainWindow::registerActions()
else
tagSelected({ QStringLiteral("unread") }, {}, tr("Mark unread"));
});
+ addAction(QStringLiteral("mark_all_read"), tr("Mark all &read"),
+ tr("Remove the unread tag from every thread in this view"),
+ [this]() {
+ markAllRead();
+ });
addAction(QStringLiteral("edit_tags"), tr("Edit &tags..."),
tr("Add or remove any tag on the selected threads"), [this]() {
editTagsOnSelection();
@@ -768,6 +773,13 @@ void MainWindow::registerActions()
// silently dead. KeyMap warns about unknown names, but only a check here
// catches the reverse: a known action nothing implements.
Q_ASSERT(m_actions.size() == KeyMap::knownActions().size());
+
+ // QAction starts enabled, so the view-wide actions have to be put into
+ // their real state here rather than waiting for the first query: a window
+ // that has not run one yet has an empty model and no complete result set,
+ // and offering "Mark all read" against nothing is a live control that does
+ // nothing.
+ updateViewWideActions();
}
void MainWindow::buildMenus()
@@ -791,6 +803,7 @@ void MainWindow::buildMenus()
messageMenu->addAction(m_actions.value(QStringLiteral("spam")));
messageMenu->addSeparator();
messageMenu->addAction(m_actions.value(QStringLiteral("toggle_unread")));
+ messageMenu->addAction(m_actions.value(QStringLiteral("mark_all_read")));
messageMenu->addAction(m_actions.value(QStringLiteral("edit_tags")));
messageMenu->addAction(m_actions.value(QStringLiteral("flag")));
@@ -876,6 +889,7 @@ void MainWindow::buildMenus()
toolBar->addSeparator();
toolBar->addAction(m_actions.value(QStringLiteral("archive")));
toolBar->addAction(m_actions.value(QStringLiteral("delete")));
+ toolBar->addAction(m_actions.value(QStringLiteral("mark_all_read")));
toolBar->addSeparator();
toolBar->addAction(m_actions.value(QStringLiteral("undo")));
}
@@ -1102,6 +1116,11 @@ void MainWindow::runCurrentQuery()
m_statusLabel->setText(tr("Searching..."));
+ // The result set is incomplete from here until queryFinished arrives, so
+ // anything claiming to act on the whole view must wait.
+ m_queryComplete = false;
+ updateViewWideActions();
+
QMetaObject::invokeMethod(m_worker, "runQuery", Qt::QueuedConnection,
Q_ARG(QString, query),
Q_ARG(quint64, m_generation));
@@ -1124,6 +1143,61 @@ void MainWindow::onQueryFinished(int total, quint64 generation)
// nothing.
m_defaultStatus = tr("%n thread(s)", "", total);
m_statusLabel->setText(m_defaultStatus);
+
+ // The model now holds every row the query matched, so "the whole view" is
+ // a thing that can honestly be acted on.
+ m_queryComplete = true;
+ updateViewWideActions();
+}
+
+void MainWindow::updateViewWideActions()
+{
+ // Threads arrive in batches of kBatchSize, so before the query reports its
+ // total the model holds only what has landed. An action that says "all"
+ // must not run against a partial set and silently skip the rest, and a
+ // disabled control says so without a dialog.
+ if (QAction *action = m_actions.value(QStringLiteral("mark_all_read")))
+ action->setEnabled(m_queryComplete && m_model->rowCount() > 0);
+}
+
+void MainWindow::markAllRead()
+{
+ // Every row, not the selection: this is the one action in the window that
+ // deliberately ignores what is selected.
+ QStringList threadIds;
+ const int rows = m_model->rowCount();
+ threadIds.reserve(rows);
+ for (int row = 0; row < rows; ++row) {
+ const ThreadSummary thread = m_model->threadAt(row);
+ // Only the threads that would actually change. Sending the rest would
+ // inflate the pending-edit count with writes that do nothing, and the
+ // quit prompt reads that count.
+ if (thread.isUnread())
+ threadIds.append(thread.threadId);
+ }
+
+ if (threadIds.isEmpty()) {
+ showTransientStatus(tr("Nothing unread in this view"));
+ return;
+ }
+
+ // An automatic mark-read armed for the open thread would fire after this
+ // and push a second, redundant command onto the stack.
+ m_markReadTimer->stop();
+ m_markReadThreadId.clear();
+
+ const QString description = tr("Mark all read");
+ sendThreadTagChange(threadIds, {}, { QStringLiteral("unread") },
+ description);
+
+ // ONE command for the batch, exactly as tagSelected does: a user who marks
+ // 400 threads read expects a single Ctrl+Z to put them back.
+ m_undoStack.push(new ThreadTagCommand(this, threadIds, {},
+ { QStringLiteral("unread") },
+ description));
+
+ showTransientStatus(
+ tr("%1: %n thread(s)", "", threadIds.size()).arg(description));
}
void MainWindow::showThreadContextMenu(const QPoint &pos)
diff --git a/src/mainwindow.h b/src/mainwindow.h
index 3984e78..adb973c 100644
--- a/src/mainwindow.h
+++ b/src/mainwindow.h
@@ -119,6 +119,19 @@ public:
static void setLocksPathForTesting(const QString &path);
static QString locksPath();
+ /// How many commands are on the undo stack.
+ ///
+ /// A test seam. The undo QAction is always enabled and checks canUndo()
+ /// when triggered, so its enabled state says nothing about whether a
+ /// command was pushed, which is what "this did nothing" has to assert.
+ int undoDepthForTesting() const { return m_undoStack.count(); }
+
+ /// The generation a worker reply must carry to be accepted.
+ ///
+ /// A test seam: onQueryFinished() discards a reply whose generation is
+ /// stale, so a test standing in for the worker has to know the current one.
+ quint64 currentGenerationForTesting() const { return m_generation; }
+
protected:
void closeEvent(QCloseEvent *event) override;
@@ -232,6 +245,14 @@ private:
/// when the phase or its detail changes.
void feedSyncPhase(const QString &chunk);
+ /// Removes `unread` from every thread in the current view, as one write and
+ /// one undo entry, ignoring the selection.
+ void markAllRead();
+
+ /// Enables or disables the actions that claim to act on a whole view,
+ /// according to whether the result set is complete.
+ void updateViewWideActions();
+
/// Applies the sync progress bar and button state from BOTH sync sources.
///
/// One function of both, never two assignments: with a local and a
@@ -383,6 +404,11 @@ private:
QStringList m_knownTags;
quint64 m_generation = 0;
+
+ /// True once the running query has reported its total, so the model holds
+ /// the whole result set rather than the batches that have arrived so far.
+ /// Gates mark_all_read, which cannot honestly say "all" before then.
+ bool m_queryComplete = false;
QString m_lastQuery;
QString m_currentThreadId;