diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/mainwindow.cpp | 64 | ||||
| -rw-r--r-- | src/mainwindow.h | 22 |
2 files changed, 76 insertions, 10 deletions
diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index a7f9b40..7cd2769 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -154,7 +154,7 @@ void MainWindow::closeEvent(QCloseEvent *event) return; } - if (!m_closeApproved && m_pendingEdits > 0 + if (!m_closeApproved && pendingEditCount() > 0 && m_config.syncOnExit() != Config::SyncOnExit::Never) { // Not a destructive-action confirmation, which CLAUDE.md forbids for @@ -167,7 +167,7 @@ void MainWindow::closeEvent(QCloseEvent *event) const auto answer = QMessageBox::warning( this, tr("Unsynced changes"), tr("%n tag change(s) have not been synced, and no sync command " - "is configured. Quit anyway?", "", m_pendingEdits), + "is configured. Quit anyway?", "", pendingEditCount()), QMessageBox::Discard | QMessageBox::Cancel, QMessageBox::Cancel); if (answer == QMessageBox::Cancel) { @@ -181,7 +181,7 @@ void MainWindow::closeEvent(QCloseEvent *event) box.setIcon(QMessageBox::Question); box.setWindowTitle(tr("Unsynced changes")); box.setText(tr("%n tag change(s) have not been synced.", "", - m_pendingEdits)); + pendingEditCount())); box.setInformativeText(tr("Sync before quitting?")); QPushButton *sync = box.addButton(tr("Sync and quit"), QMessageBox::AcceptRole); @@ -1299,7 +1299,8 @@ void MainWindow::onSyncFinished(bool success, int exitCode) // Only a SUCCESSFUL sync clears the count. Clearing on failure would // assert the edits had reached the mail store when the sync is exactly // what failed to put them there. - m_pendingEdits = 0; + m_pendingTagEdits.clear(); + m_unnettablePendingEdits = 0; updatePendingIndicator(); showTransientStatus(tr("Sync complete")); @@ -1363,10 +1364,33 @@ void MainWindow::onTagsApplied(const TagChange &change) m_pendingChange = {}; m_pendingThreadIds.clear(); - // Counted here, where a write is CONFIRMED, rather than where one is sent: + // Recorded here, where a write is CONFIRMED, rather than where one is sent: // an optimistic update the worker later rejects must not leave the // indicator claiming an edit that never landed. - ++m_pendingEdits; + // + // NET state, not a count of writes. An edit and its inverse leave the mail + // store where it started, so they must leave the indicator at zero: the + // automatic mark-read followed by Ctrl+U used to read as 2 unsynced + // changes when nothing was outstanding. What the user needs to know is + // whether quitting now would strand work. + // + // Keyed per (message, tag): removing `unread` and adding `flagged` on one + // message are two independent changes and must not cancel each other. + for (const QString &messageId : change.messageIds) { + for (const QString &tag : change.added) + recordPendingEdit(messageId, tag, true); + for (const QString &tag : change.removed) + recordPendingEdit(messageId, tag, false); + } + + // A change carrying no message ids cannot be netted against anything, and + // must still register: losing an edit understates the indicator, which is + // the direction that costs the user work. + if (change.messageIds.isEmpty() + && !(change.added.isEmpty() && change.removed.isEmpty())) { + ++m_unnettablePendingEdits; + } + updatePendingIndicator(); // A tag the user has just created is the one they are most likely to type @@ -1469,16 +1493,40 @@ void MainWindow::updateSyncControls() m_syncButton->setEnabled(!busy && m_sync && m_sync->isAvailable()); } +void MainWindow::recordPendingEdit(const QString &messageId, const QString &tag, + bool added) +{ + const QString key = messageId + QLatin1Char('\n') + tag; + + // A tag put back the way it was is not an outstanding change. Erase rather + // than store the new direction, or the ledger grows without bound over a + // long session of tagging and untagging. + const auto existing = m_pendingTagEdits.constFind(key); + if (existing != m_pendingTagEdits.constEnd()) { + if (*existing != added) + m_pendingTagEdits.erase(m_pendingTagEdits.find(key)); + return; + } + + m_pendingTagEdits.insert(key, added); +} + +int MainWindow::pendingEditCount() const +{ + return m_pendingTagEdits.size() + m_unnettablePendingEdits; +} + void MainWindow::updatePendingIndicator() { - if (m_pendingEdits <= 0) { + const int pending = pendingEditCount(); + if (pending <= 0) { m_pendingLabel->hide(); return; } // "Changes" and not "mutations": the unit the user thinks in is the tagging // they did, not the writes it became. - m_pendingLabel->setText(tr("%n unsynced change(s)", "", m_pendingEdits)); + m_pendingLabel->setText(tr("%n unsynced change(s)", "", pending)); m_pendingLabel->setToolTip( tr("Tag changes made here that a sync has not yet carried to the mail " "store. An external notmuch run can clear them without this count " diff --git a/src/mainwindow.h b/src/mainwindow.h index 2edeb79..cf9aca6 100644 --- a/src/mainwindow.h +++ b/src/mainwindow.h @@ -176,9 +176,17 @@ private: /// the one on screen. void markCurrentThreadRead(); - /// Redraws the unsynced-edits indicator from m_pendingEdits. + /// Redraws the unsynced-edits indicator from pendingEditCount(). void updatePendingIndicator(); + /// Records one confirmed (message, tag) change, cancelling it against an + /// opposite change already outstanding for the same pair. + void recordPendingEdit(const QString &messageId, const QString &tag, + bool added); + + /// Net changes the index holds that a sync has not carried over. + int pendingEditCount() const; + /// Shows or hides the "syncing" state: the progress bar and a disabled /// Sync button. /// @@ -318,7 +326,17 @@ private: /// /// A lower bound on what is outstanding, never a guarantee: the user's cron /// can run notmuch new without the application noticing. - int m_pendingEdits = 0; + /// + /// NET state rather than a tally of writes. Keyed "<messageId>\n<tag>", + /// value true for added and false for removed; a pair that reverts is + /// erased rather than stored, so an edit and its inverse leave nothing + /// behind and the map cannot grow without bound. + QHash<QString, bool> m_pendingTagEdits; + + /// Confirmed changes carrying no message ids, which cannot be netted + /// against anything. Counted separately rather than dropped: understating + /// the indicator is the direction that costs the user work. + int m_unnettablePendingEdits = 0; /// Marks the open thread read once it has been on screen long enough. /// |
