From 2703d4c42710418b0f15a41fa896824e2e1a6170 Mon Sep 17 00:00:00 2001 From: "Danilo M." Date: Mon, 24 Aug 2026 11:13:04 +0200 Subject: feat(ui): give the message pane its own action bar Items 139, 140 and 141, built together because the seam between them is wasted work: 140 needs a container and 141 is that container. The main toolbar had grown to mix two scopes. Sync, Archive, Delete, Mark all read and Undo act on the list or the selection; Compose, Reply and Forward are about one message. With everything in one row the distinction was invisible, and Forward was on no toolbar at all, reachable only from the Message menu, which is item 139. Compose, Reply and Forward now sit on a bar above the message pane, and LEAVE the main toolbar rather than gaining a second home: that is what makes the toolbar's remaining contents mean one thing. Toggle HTML joins them at the right end, separated by an expanding spacer, since changing how a message is displayed is a different scope from acting on it. That layout was the open design question item 141 recorded, and it was settled with the user rather than guessed. The actions are MainWindow's own QAction objects shown a second time, never copies: a duplicate would carry its own enablement and drift from the menu entry updateComposeActions() keeps in step. MessageView::setBarActions() is the seam, so the pane still knows nothing about the window's action map. Two things worth recording: QToolBar has no addStretch(), so the separation is an expanding spacer widget. A test asserting only on action ORDER passes with that spacer deleted, measured, so it asserts on the spacer's size policy instead. noTwoActionsShareAnIcon looked up the toolbar with an unnamed findChild(). There are two toolbars now, so it is pinned to main_toolbar: pointed at the pane's bar it would have asserted that a thread action is absent from a bar that never holds any, and passed while the rule it exists for went unchecked. --- src/mainwindow.cpp | 28 +++++++++++++++++++--------- src/mainwindow.h | 5 +++++ src/messageview.cpp | 43 +++++++++++++++++++++++++++++++++++++++++++ src/messageview.h | 11 +++++++++++ 4 files changed, 78 insertions(+), 9 deletions(-) (limited to 'src') diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index 9a94a2e..fd59f47 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -555,6 +555,9 @@ MainWindow::MainWindow(const Config &config, QWidget *parent) } buildMenus(); + // After buildMenus(), which registers the toolbar and menu entries these + // actions already carry: the bar shows the same objects a second time. + populateMessageBar(); // After buildMenus(): QMainWindow::restoreState() matches toolbars by // object name, so they must already exist or their position is dropped. restoreUiState(); @@ -1967,15 +1970,10 @@ void MainWindow::buildMenus() const int iconSize = m_config.toolbarIconSize(); toolBar->setIconSize(QSize(iconSize, iconSize)); - // First, because composing and replying are what a user reaches for most - // (item 123). These TWO only: the other four are menu-and-key, which is - // what keeps the no-duplicate-icons rule satisfiable, since reply_no_quote - // shares reply's icon and an icon-only toolbar would make the two buttons - // indistinguishable. - toolBar->addAction(m_actions.value(QStringLiteral("compose"))); - toolBar->addAction(m_actions.value(QStringLiteral("reply"))); - toolBar->addSeparator(); - + // Compose, Reply and Forward are NOT here (item 140). They act on a + // message, where everything below acts on the list or on the selection, + // and mixing the two is what made this toolbar read as the place for + // everything. They live on the message pane's own bar instead. QAction *syncAction = m_actions.value(QStringLiteral("sync")); // Carried over from the QPushButton this replaced: with no command // configured the control is disabled, and the tooltip is the only thing @@ -1994,6 +1992,18 @@ void MainWindow::buildMenus() toolBar->addAction(m_actions.value(QStringLiteral("undo"))); } +void MainWindow::populateMessageBar() +{ + // The window's own QActions, shown a second time rather than copied: a + // duplicate QAction would need its own enablement and would drift from the + // menu entry that updateComposeActions() keeps in step. + m_messageView->setBarActions( + { m_actions.value(QStringLiteral("compose")), + m_actions.value(QStringLiteral("reply")), + m_actions.value(QStringLiteral("forward")) }, + { m_actions.value(QStringLiteral("toggle_html")) }); +} + void MainWindow::showShortcutReference() { // Generated from the actions, so it cannot disagree with what the keys diff --git a/src/mainwindow.h b/src/mainwindow.h index ea3ba61..909b04b 100644 --- a/src/mainwindow.h +++ b/src/mainwindow.h @@ -637,6 +637,11 @@ private: void registerActions(); void buildMenus(); + + /// Fills the message pane's own bar with the three message actions and + /// toggle_html (items 139 to 141). Called after buildMenus(), which is + /// what creates the actions it hands over. + void populateMessageBar(); void wireWorker(); /// Asks the worker to re-enumerate the database tags for the completer. diff --git a/src/messageview.cpp b/src/messageview.cpp index 22df6ce..c8395f1 100644 --- a/src/messageview.cpp +++ b/src/messageview.cpp @@ -37,6 +37,9 @@ #include #include #include +#include +#include +#include #include #include #include @@ -456,7 +459,20 @@ MessageView::MessageView(QWidget *parent) menu.exec(globalPos); }); + // The pane's own action bar (items 139 to 141). Empty until MainWindow + // fills it: the actions belong to the window, and MessageView deliberately + // knows nothing about the action map. + m_messageBar = new QToolBar(this); + m_messageBar->setObjectName(QStringLiteral("message_toolbar")); + // The desktop's own button style, for the reason the main toolbar records: + // a hardcoded setToolButtonStyle() overrides the user's "Icon only". + m_messageBar->setToolButtonStyle(static_cast( + style()->styleHint(QStyle::SH_ToolButtonStyle, nullptr, m_messageBar))); + m_messageBar->setMovable(false); + m_messageBar->hide(); + auto *layout = new QVBoxLayout(this); + layout->addWidget(m_messageBar); layout->addLayout(headerRow); layout->addWidget(m_blockedBar); layout->addWidget(m_receiveOnlyRibbon); @@ -566,6 +582,33 @@ void MessageView::applyNoticeBarStyles() } } +void MessageView::setBarActions(const QList &messageActions, + const QList &viewControls) +{ + m_messageBar->clear(); + + for (QAction *action : messageActions) { + if (action) + m_messageBar->addAction(action); + } + + // The stretch is what separates the two scopes, so the view controls end + // up at the right edge. A QToolBar has no addStretch(), so it takes an + // expanding spacer widget. + if (!viewControls.isEmpty()) { + auto *spacer = new QWidget(m_messageBar); + spacer->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); + m_messageBar->addWidget(spacer); + + for (QAction *action : viewControls) { + if (action) + m_messageBar->addAction(action); + } + } + + m_messageBar->setVisible(!m_messageBar->actions().isEmpty()); +} + void MessageView::clear() { m_items.clear(); diff --git a/src/messageview.h b/src/messageview.h index 29101d6..b28dcdc 100644 --- a/src/messageview.h +++ b/src/messageview.h @@ -32,6 +32,7 @@ class QLabel; class QMenu; +class QToolBar; class QWebEnginePage; class QPushButton; class QWebEngineView; @@ -245,6 +246,15 @@ public: /// after three wrong theories. Keep the two questions separate. static void addPaneActions(QMenu *menu, QWebEnginePage *page); + /// Fills the pane's own action bar (items 139 to 141). The actions are + /// MainWindow's own, shown a second time rather than duplicated, so they + /// keep one enablement state and one menu entry. \p viewControls are + /// separated from \p messageActions by a stretch: acting on the message + /// and changing how it is displayed are different scopes, which is the + /// confusion the bar exists to remove one level up. + void setBarActions(const QList &messageActions, + const QList &viewControls); + /// Tells the pane whether the query bar currently holds anything. /// /// The menus need it to grey out "Exclude from search": excluding from an @@ -405,6 +415,7 @@ private: CidSchemeHandler *m_cidHandler = nullptr; QLabel *m_headerLabel = nullptr; + QToolBar *m_messageBar = nullptr; QWidget *m_blockedBar = nullptr; QLabel *m_blockedLabel = nullptr; QLabel *m_receiveOnlyRibbon = nullptr; -- cgit v1.2.3