diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/keymap.cpp | 5 | ||||
| -rw-r--r-- | src/mainwindow.cpp | 56 | ||||
| -rw-r--r-- | src/mainwindow.h | 17 |
3 files changed, 71 insertions, 7 deletions
diff --git a/src/keymap.cpp b/src/keymap.cpp index dcb63a8..f27f9a9 100644 --- a/src/keymap.cpp +++ b/src/keymap.cpp @@ -36,6 +36,7 @@ QStringList KeyMap::knownActions() QStringLiteral("focus_query"), QStringLiteral("complete_query"), QStringLiteral("select_all"), + QStringLiteral("clear_pane"), QStringLiteral("toggle_html"), QStringLiteral("load_remote"), QStringLiteral("message_details"), @@ -76,6 +77,10 @@ QList<QPair<QString, QString>> KeyMap::defaultBindings() // The conventional select-all key, and free here: the thread list is a // read-only view, so nothing else in the window wants it. { QStringLiteral("Ctrl+A"), QStringLiteral("select_all") }, + // Escape is not claimed by anything else at window level. The query + // completer handles its own Escape while its popup is up, and a popup + // consumes the key before a window shortcut sees it. + { QStringLiteral("Esc"), QStringLiteral("clear_pane") }, { QStringLiteral("Ctrl+H"), QStringLiteral("toggle_html") }, { QStringLiteral("Ctrl+M"), QStringLiteral("load_remote") }, // Shifted because Ctrl+D is delete. Both are "D for details/delete" diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index 792ba3f..e546af5 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -410,6 +410,7 @@ void MainWindow::buildUi() m_syncLogPane->hide(); m_syncButton = new QPushButton(tr("Sync"), central); + m_syncButton->setObjectName(QStringLiteral("syncButton")); m_sync = new MailSync(m_config.syncCommand(), this); m_syncButton->setEnabled(m_sync->isAvailable()); if (!m_sync->isAvailable()) { @@ -679,6 +680,21 @@ void MainWindow::registerActions() m_queryEdit->setFocus(); m_queryCompleter->triggerCompletion(); }); + addAction(QStringLiteral("clear_pane"), tr("Clear &message pane"), + tr("Blank the message pane without changing the selection"), + [this]() { + // A view change, not a mail change: the selection, the query and the + // undo stack are all left alone. + // + // m_currentThreadId is cleared with the pane, not merely alongside it. + // A threadLoaded still in flight for that id would otherwise paint the + // thread straight back, which is the queued-reply race documented in + // CLAUDE.md. + m_currentThreadId.clear(); + m_messageView->clear(); + m_markReadTimer->stop(); + m_markReadThreadId.clear(); + }); addAction(QStringLiteral("select_all"), tr("Select &all threads"), tr("Select every thread in the current result list"), [this]() { // A registered action rather than the view's built-in SelectAll key, so @@ -1318,7 +1334,8 @@ void MainWindow::onExternalSyncStateChanged(SyncMonitor::State state) if (m_localSyncHoldsLock) return; - m_syncProgress->setVisible(true); + m_externalSyncBusy = true; + updateSyncControls(); m_statusLabel->setText(tr("Background sync running...")); return; } @@ -1327,10 +1344,16 @@ void MainWindow::onExternalSyncStateChanged(SyncMonitor::State state) // said what happened, including for a failure, so there is nothing to add. if (m_localSyncHoldsLock) { m_localSyncHoldsLock = false; + m_externalSyncBusy = false; + updateSyncControls(); return; } - m_syncProgress->setVisible(false); + // Cleared for Idle AND for Unknown. Unknown means /proc/locks could not be + // read, so nothing is observed; leaving the button disabled there would + // strand it permanently on a platform that cannot see the lock at all. + m_externalSyncBusy = false; + updateSyncControls(); // Deliberately reports rather than refreshes. runCurrentQuery() clears the // undo stack, the selection and the message pane, which is right for a @@ -1350,16 +1373,35 @@ void MainWindow::onExternalSyncStateChanged(SyncMonitor::State state) void MainWindow::setSyncBusy(bool busy) { - m_syncProgress->setVisible(busy); - // Disabled rather than left clickable: MailSync::start() already refuses a - // second run, but a button that looks live and does nothing is worse than - // one that shows it is unavailable. - m_syncButton->setEnabled(!busy && m_sync && m_sync->isAvailable()); + m_localSyncBusy = busy; + updateSyncControls(); if (busy) m_statusLabel->setText(tr("Syncing...")); } +void MainWindow::updateSyncControls() +{ + // ONE function of both states, deliberately. Two independent assignments, + // one per sync path, means whichever fires second wins: a background sync + // ending would re-enable the button in the middle of a local run, and a + // local run ending would re-enable it while cron still holds the lock. + const bool busy = m_localSyncBusy || m_externalSyncBusy; + + m_syncProgress->setVisible(busy); + + // Disabled rather than left clickable: MailSync::start() already refuses a + // second run and the script exits 75 when another holds the lock, but a + // button that looks live and does nothing is worse than one that shows it + // is unavailable. + // + // Note this reads Running specifically, not "not Idle". Unknown means + // /proc/locks could not be read and nothing was observed, so the button + // stays usable: permanently disabling it where the lock cannot be seen is + // worse than occasionally offering a run that gets skipped. + m_syncButton->setEnabled(!busy && m_sync && m_sync->isAvailable()); +} + void MainWindow::updatePendingIndicator() { if (m_pendingEdits <= 0) { diff --git a/src/mainwindow.h b/src/mainwindow.h index 66044dc..61722aa 100644 --- a/src/mainwindow.h +++ b/src/mainwindow.h @@ -175,6 +175,14 @@ private: /// says "working, duration unknown", which is the truth. void setSyncBusy(bool busy); + /// Applies the sync progress bar and button state from BOTH sync sources. + /// + /// One function of both, never two assignments: with a local and a + /// background sync each writing the widgets independently, whichever + /// finished second would win and re-enable the button while the other was + /// still running. + void updateSyncControls(); + /// Opens the tag dialog on the current selection and applies its result. /// @@ -223,6 +231,15 @@ private: /// MailSync::isRunning() is already false and can no longer answer "was /// that ours?". bool m_localSyncHoldsLock = false; + + /// True while a sync this window started is running. Half of the input to + /// updateSyncControls(). + bool m_localSyncBusy = false; + + /// True while a sync this window did NOT start holds the lock. The other + /// half. Tracked here rather than read back from SyncMonitor so the state + /// the UI acted on is the state it was told about. + bool m_externalSyncBusy = false; QUndoStack m_undoStack; QLineEdit *m_queryEdit = nullptr; |
