summaryrefslogtreecommitdiffstats
path: root/src/mainwindow.cpp
diff options
context:
space:
mode:
authorDanilo M. <danix@danix.xyz>2026-08-15 16:35:56 +0200
committerDanilo M. <danix@danix.xyz>2026-08-15 16:35:56 +0200
commit6d35f7ec87590fd484ca7640d4d06c06a6d97970 (patch)
treea465275f74b376209683cc63f9ec622f9dcc59aa /src/mainwindow.cpp
parentae0c0ec339dba2ce9f2b27a9ce289feb4719dcea (diff)
downloadqtmaildir-6d35f7ec87590fd484ca7640d4d06c06a6d97970.tar.gz
qtmaildir-6d35f7ec87590fd484ca7640d4d06c06a6d97970.zip
feat(ui): open a thread on its own by double-clicking a row
Double-clicking any row drills into its thread: the list becomes that thread alone, expanded, and the pane shows the double-clicked row's own message. A reply therefore opens its WHOLE thread with itself selected, never itself alone, which is what the user asked for and is not the obvious reading of "open it by itself". This is recoverStaleThread() triggered by a gesture. That function already ran thread:<id>, expanded the thread when the row arrived, selected the target message once the replies landed, and fell back to the root when the message had gone; all three cases are existing paths through it, so the new code resolves a row to a thread id and a message id and hands both over. The row is reached through the INDEX and never through index.row(): a tree numbers rows per parent, so threadAt(row) on a reply answers about an unrelated thread. That is item 88's trap, avoided here by construction. The first click of a double-click arms the mark-read timer, and the handler cancels it, because a gesture that navigates must not mutate mail. The timer is armed again for whichever row the recovery lands on, so only the arming for the row being left is cancelled. Its test asserts the timer was active beforehand, so it cannot pass by the timer never having been armed at all. The expander keeps its own double-click: ThreadListView::mousePressEvent accepts a press inside its rect and returns, so Qt never pairs one into a double-click there. Nothing is built for getting back. The filter buttons already are that, per the user. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Diffstat (limited to 'src/mainwindow.cpp')
-rw-r--r--src/mainwindow.cpp54
1 files changed, 54 insertions, 0 deletions
diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp
index ba803bc..fcf97df 100644
--- a/src/mainwindow.cpp
+++ b/src/mainwindow.cpp
@@ -709,6 +709,9 @@ void MainWindow::buildUi()
&QItemSelectionModel::selectionChanged,
this, &MainWindow::onSelectionChanged);
+ connect(m_threadView, &QAbstractItemView::doubleClicked,
+ this, &MainWindow::onRowDoubleClicked);
+
m_messageView = new MessageView(central);
m_messageView->setTagColors(&m_tagColors);
connect(m_messageView, &MessageView::statusMessage,
@@ -3104,6 +3107,57 @@ void MainWindow::recoverStaleThread(const QString &threadId,
m_recoverMessageId = messageId;
}
+void MainWindow::onRowDoubleClicked(const QModelIndex &index)
+{
+ if (!index.isValid())
+ return;
+
+ // The whole thread in every case, and the double-clicked row's own message
+ // in the pane. A reply therefore drills to its THREAD with itself selected,
+ // never to itself alone: "double click on a reply in a thread should still
+ // load the whole thread expanded in a view by itself, with the reply I
+ // clicked on visible in the right pane" (item 91). An id: query on the
+ // reply is the obvious reading of "open it by itself" and is the wrong one.
+ //
+ // Reached through the INDEX rather than through index.row(): a tree numbers
+ // rows per parent, so a reply's row indexes its siblings and threadAt() on
+ // one answers about an unrelated thread.
+ QString threadId;
+ QString messageId;
+ if (m_model->isMessageRow(index)) {
+ const MessageNode node = m_model->messageAt(index);
+ threadId = node.threadId;
+ messageId = node.messageId;
+ } else {
+ threadId = m_model->data(index, ThreadListModel::ThreadIdRole).toString();
+ // The thread's first message, so the pane opens on it rather than on
+ // nothing. Empty is fine and means the same thing to the recovery: land
+ // on the root, which IS that message.
+ messageId = m_model->data(index, ThreadListModel::MessageIdRole).toString();
+ }
+
+ if (threadId.isEmpty())
+ return;
+
+ // The first click of the double-click already selected this row and armed
+ // the mark-read timer. The user is passing through on their way into the
+ // thread, and a gesture that navigates must not mutate mail, so the timer
+ // goes the same way it does for a multi-row selection.
+ //
+ // Not a correction of the single click's behaviour: the thread is about to
+ // be opened and its message read, which arms the timer again for the row
+ // the recovery selects. What is cancelled is the arming for a row the user
+ // is leaving.
+ m_markReadTimer->stop();
+ m_markReadThreadId.clear();
+
+ // Reuses the stale-thread recovery outright, which already runs thread:<id>,
+ // expands the thread when the row arrives, selects the target message once
+ // the replies land, and falls back to the root when the message has gone.
+ // Every one of item 91's three cases is one of those paths.
+ recoverStaleThread(threadId, messageId);
+}
+
void MainWindow::applyPendingRecovery()
{
if (m_recoverThreadId.isEmpty())