aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/mainwindow.cpp96
-rw-r--r--src/messageview.cpp22
2 files changed, 110 insertions, 8 deletions
diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp
index eb41da9..42fe1bf 100644
--- a/src/mainwindow.cpp
+++ b/src/mainwindow.cpp
@@ -1298,7 +1298,17 @@ void MainWindow::updateComposeActions()
// cannot send. save_message is deliberately NOT in this list: it is the
// escape hatch for exactly that case, writing the raw message to a file
// that can be attached to a new message from an account that can send.
- const QString replyAccount = accountForCurrentMessage();
+ // Everything below describes the DISPLAYED message, and every route that
+ // blanks the pane clears these two ids while leaving currentIndex() valid
+ // on a row from the previous result. Answering from the index instead
+ // would describe a message that is no longer on screen: it left Reply
+ // enabled over an empty pane, and, once this function refilled the pane's
+ // bar, put the wrong buttons there in both directions.
+ const bool showing =
+ !m_currentMessageId.isEmpty() || !m_currentThreadId.isEmpty();
+
+ const QString replyAccount =
+ showing ? accountForCurrentMessage() : QString();
const bool canReply = !replyAccount.isEmpty()
&& m_config.account(replyAccount).canSend();
@@ -1320,7 +1330,13 @@ void MainWindow::updateComposeActions()
// notmuch surfaces the Maildir D flag as one, and a message flagged by
// another client sits in the inbox rather than in the drafts folder.
if (QAction *edit = m_actions.value(QStringLiteral("edit_draft")))
- edit->setEnabled(currentMessageIsADraft());
+ edit->setEnabled(showing && currentMessageIsADraft());
+
+ // The pane's bar shows Edit draft in place of the reply pair on a draft
+ // (item 157), so it is refilled here rather than once at construction:
+ // this runs on every selection change, which is the only thing that can
+ // move a draft into or out of the pane.
+ populateMessageBar();
// The ribbon appears only when an account was identified AND it cannot
// send. An unidentified account is not a receive-only one: it is a message
@@ -2116,14 +2132,63 @@ void MainWindow::populateMessageBar()
// Reply and Forward only: Compose needs no message and sits on the main
// toolbar with the other window-wide actions.
//
+ // A draft swaps that pair for Edit draft (item 157). It is the same rule
+ // items 139 to 141 settled, applied one level down: the bar carries what
+ // the DISPLAYED message affords, and a draft affords neither answering a
+ // sender it does not have nor passing on a message that is not finished.
+ // The view controls are unchanged by the swap, since how the pane renders
+ // is not a property of what the message is.
+ //
+ // Called from updateComposeActions() as well as at construction, so it
+ // follows the selection. That is also why the actions are looked up fresh
+ // rather than cached: the bar is refilled, never rebuilt.
+ //
// Slightly smaller than the main toolbar's icons, deriving from the
// configured size rather than hardcoding one, so the bar stays subordinate
// to the chrome above it however the user sets that key.
const int iconSize = qMax(16, (m_config.toolbarIconSize() * 7) / 8);
+
+ // Gated on whether a message is DISPLAYED, not on which row is current.
+ // The two disagree on every route that blanks the pane without moving the
+ // selection: running a query leaves currentIndex() valid on a row from the
+ // previous result, so a bar keyed on it kept offering Edit draft over an
+ // empty pane after leaving the Drafts filter, and the reply pair after
+ // arriving at it. This is item 150's trap exactly, one level up, and the
+ // test that missed it moved row to row, which is the one gesture that
+ // cannot expose it.
+ //
+ // m_currentMessageId is cleared with the pane by every one of those
+ // routes, so it is the only thing that tracks what the bar describes.
+ // The bar always carries a message half. Whether it is SEEN is
+ // MessageView's question, not this one: it hides the whole bar over an
+ // empty pane, alongside the subject and the details button, so this only
+ // ever decides what a displayed message affords.
+ //
+ // Only a displayed DRAFT swaps the pair, and "displayed" is the operative
+ // word: keyed on m_currentMessageId rather than on currentIndex(), which
+ // stays valid on a row from the previous result after a query and made the
+ // bar describe a message that was no longer on screen, in both directions.
+ // That is item 150's trap one level up, and the first version of this test
+ // could not see it because it moved row to row, the one gesture that
+ // always changes both.
+ // currentMessageIsADraft() alone, with no displayed-message guard beside
+ // it: a query calls m_model->clear(), which invalidates currentIndex(),
+ // so the predicate is already false whenever the pane is blank. Measured,
+ // after writing that guard and finding no reachable state where it
+ // changed the answer. The guard that IS load-bearing sits one level up in
+ // updateComposeActions(), where accountForCurrentMessage() would otherwise
+ // answer about a row this query is discarding.
+ QList<QAction *> messageActions;
+ if (currentMessageIsADraft()) {
+ messageActions = { m_actions.value(QStringLiteral("edit_draft")) };
+ } else {
+ messageActions = { m_actions.value(QStringLiteral("reply")),
+ m_actions.value(QStringLiteral("forward")) };
+ }
+
m_messageView->setBarActions(
- { m_actions.value(QStringLiteral("reply")),
- m_actions.value(QStringLiteral("forward")) },
- { m_actions.value(QStringLiteral("toggle_html")) }, iconSize);
+ messageActions, { m_actions.value(QStringLiteral("toggle_html")) },
+ iconSize);
}
void MainWindow::showShortcutReference()
@@ -2566,6 +2631,22 @@ void MainWindow::showPlaceholderPane()
{
m_messageView->showPlaceholder(placeholderHelpers());
+ // Every route that blanks the pane comes through here, which is why the
+ // bar is refilled here rather than at each of them: item 150 was the same
+ // defect one level down and was fixed by finding the one shared site.
+ // The ids this reads are cleared by the callers around the same point;
+ // the order between the two was measured and no test can tell it apart,
+ // since a query invalidates currentIndex() anyway. Left as it was found.
+ //
+ // updateComposeActions() rather than populateMessageBar() alone, because
+ // the ENABLEMENT was stale here too and had been since before the bar
+ // existed: it ran only from the two selection handlers, so a query that
+ // blanked the pane left Reply and Forward enabled over nothing. Invisible
+ // while they lived on the main toolbar among other always-on actions, and
+ // plain once they sat over an empty pane. It calls populateMessageBar()
+ // last, so the bar still gets refilled.
+ updateComposeActions();
+
QMetaObject::invokeMethod(m_worker, "requestCounts", Qt::QueuedConnection,
Q_ARG(QStringList, placeholderQueries()),
Q_ARG(quint64, ++m_countsGeneration));
@@ -3171,8 +3252,6 @@ void MainWindow::runQuery(FlatResult flat, AccountScope scope)
++m_generation;
m_model->clear();
- m_messageView->clear();
- showPlaceholderPane();
// Cleared WITH the pane, not merely alongside it. These three name what the
// pane is showing, and both selection handlers use them to decide whether a
@@ -3185,6 +3264,9 @@ void MainWindow::runQuery(FlatResult flat, AccountScope scope)
// the thread is current at the moment the query replaces the view, and its
// one card opens onto the placeholder. A query returning any OTHER thread
// hides it, which is why it took a screenshot to find.
+ m_messageView->clear();
+ showPlaceholderPane();
+
m_currentThreadId.clear();
m_currentMessageId.clear();
m_currentMessageThreadId.clear();
diff --git a/src/messageview.cpp b/src/messageview.cpp
index 4c2a9d0..86e40eb 100644
--- a/src/messageview.cpp
+++ b/src/messageview.cpp
@@ -617,7 +617,18 @@ void MessageView::setBarActions(const QList<QAction *> &messageActions,
}
}
- m_messageBar->setVisible(!m_messageBar->actions().isEmpty());
+ // Hidden over an empty pane whatever it holds, so it comes and goes with
+ // the subject and the details button rather than hovering over the logo.
+ // The user's call, and the reason is consistency with those two: a bar
+ // that persists was the only piece of header furniture that did.
+ //
+ // This guard covers the HIDING only. MainWindow refills the bar from
+ // showPlaceholderPane(), which every route that blanks the pane passes
+ // through, so the refill re-reads m_items and this line answers. Nothing
+ // refills it when a message ARRIVES, so updateHeader() shows it there;
+ // see the note beside the details button, which it rides with.
+ m_messageBar->setVisible(!m_messageBar->actions().isEmpty()
+ && !m_items.isEmpty());
}
void MessageView::clear()
@@ -754,6 +765,14 @@ void MessageView::updateHeader()
// scratch a few lines down.
m_headerOffers.clear();
+ // The bar rides with the details button, but only the SHOWING half belongs
+ // here. Hiding is covered by setBarActions(), since MainWindow refills the
+ // bar on every route that blanks the pane, and a hide() in the empty
+ // branch below was measured to change nothing. Nothing refills the bar
+ // when a message ARRIVES, though, so without the show() below it stayed
+ // hidden for the first message opened after any blanking and appeared on
+ // the second, when m_items still held the first: one selection behind for
+ // as long as the view lasted.
if (m_items.isEmpty()) {
m_headerLabel->clear();
m_detailsButton->hide();
@@ -761,6 +780,7 @@ void MessageView::updateHeader()
}
m_detailsButton->show();
+ m_messageBar->setVisible(!m_messageBar->actions().isEmpty());
// The thread's subject comes from its first message; later replies carry
// Re: prefixes that add nothing.