diff options
| author | Danilo M. <danix@danix.xyz> | 2026-08-07 18:22:45 +0200 |
|---|---|---|
| committer | Danilo M. <danix@danix.xyz> | 2026-08-07 18:22:45 +0200 |
| commit | 48c243d7aee3382713f7a4653be2179d313b432c (patch) | |
| tree | b5eb63907e3f7cff18815844baccce71f4492dc1 /src | |
| parent | d62a9eb9694beee1674f9c63f9a77029a81627ee (diff) | |
| download | qtmaildir-48c243d7aee3382713f7a4653be2179d313b432c.tar.gz qtmaildir-48c243d7aee3382713f7a4653be2179d313b432c.zip | |
feat(ui): make Escape deselect the row as well as blank the pane
Blanking the pane while leaving the row highlighted reads as half an
action, and deselecting is what Escape means nearly everywhere else. The
user asked for two actions rather than a changed one, so clear_pane keeps
its behaviour and moves to Shift+Esc; clear_selection takes Escape and
does both. Shift+Esc rather than unbound because every action carries a
default and a test enforces it.
Clearing the selection re-adopts the thread it just cleared, unless done
in exactly the right way. clearSelection() leaves currentIndex() valid,
so onSelectionChanged takes its "one or fewer rows" branch, sees a
current row whose id differs from m_currentThreadId, and calls
onThreadSelected for it. Clearing the selection before blanking lets that
run while the id still matches, so nothing reloads, and clearing current
stops a later collapse-to-one-row reaching the same row. All four
arrangements were tried; only this one passes.
The first version of the test could not distinguish any of them. It
asserted showingPlaceholder(), which passes regardless because this
fixture has no worker, so loadThread never replies and the pane is never
repainted. currentThreadId() and currentIndex() are observable without
one, and asserting those is what made the test discriminate.
Diffstat (limited to 'src')
| -rw-r--r-- | src/keymap.cpp | 13 | ||||
| -rw-r--r-- | src/mainwindow.cpp | 33 |
2 files changed, 45 insertions, 1 deletions
diff --git a/src/keymap.cpp b/src/keymap.cpp index a16a700..29a5f71 100644 --- a/src/keymap.cpp +++ b/src/keymap.cpp @@ -38,6 +38,7 @@ QStringList KeyMap::knownActions() QStringLiteral("complete_query"), QStringLiteral("select_all"), QStringLiteral("clear_pane"), + QStringLiteral("clear_selection"), QStringLiteral("toggle_html"), QStringLiteral("load_remote"), QStringLiteral("message_details"), @@ -85,7 +86,17 @@ QList<QPair<QString, QString>> KeyMap::defaultBindings() // 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") }, + // + // It clears the SELECTION as well as the pane (item 50). Deselecting is + // what Escape means nearly everywhere else, and blanking a pane while + // leaving the row highlighted reads as half an action. + // + // clear_pane keeps the narrower behaviour on Shift+Esc: same key, and + // the modifier reads as "less than the plain one". It needs SOME + // default rather than being left unbound, since every action carries + // one and everyActionHasAShortcut enforces exactly that. + { QStringLiteral("Esc"), QStringLiteral("clear_selection") }, + { QStringLiteral("Shift+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 5af6624..3f8a53d 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -787,6 +787,39 @@ void MainWindow::registerActions() m_markReadTimer->stop(); m_markReadThreadId.clear(); }); + addAction(QStringLiteral("clear_selection"), tr("Clear &selection"), + tr("Blank the message pane and deselect every thread"), + [this]() { + // Item 50, and the user's wording was "two actions instead of one": + // clear_pane above still blanks without touching the selection, this + // one does both. Esc defaults here, since deselecting is what Esc means + // nearly everywhere else. + // + // BOTH LINES BELOW ARE LOAD-BEARING, AND SO IS THEIR PLACE ABOVE THE + // BLANKING. clearSelection() leaves currentIndex() VALID, and + // onSelectionChanged() then takes its "one or fewer rows" branch, finds + // a current row whose id differs from m_currentThreadId, and calls + // onThreadSelected for it: the thread is re-adopted and a loadThread + // sent for the row that was just being cleared. + // + // Clearing the selection FIRST means that runs while m_currentThreadId + // still names the displayed thread, so the ids match and nothing is + // reloaded; setCurrentIndex() then stops any later collapse-to-one-row + // reaching the same row again. + // + // All four arrangements were tried against + // clearSelectionBlanksThePaneAndDeselects, and only this one passes: + // dropping setCurrentIndex() fails, and moving either line after the + // blanking fails. + m_threadView->clearSelection(); + m_threadView->setCurrentIndex(QModelIndex()); + + m_currentThreadId.clear(); + m_messageView->clear(); + showPlaceholderPane(); + 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 |
