diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/keymap.cpp | 5 | ||||
| -rw-r--r-- | src/mainwindow.cpp | 74 | ||||
| -rw-r--r-- | src/mainwindow.h | 26 |
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; |
