summaryrefslogtreecommitdiffstats
path: root/src/mainwindow.cpp
diff options
context:
space:
mode:
authorDanilo M. <danix@danix.xyz>2026-08-21 20:12:30 +0200
committerDanilo M. <danix@danix.xyz>2026-08-21 20:12:30 +0200
commit84e3205ddcba3263e6c07fa314437318309d4b76 (patch)
treee120d487e85687a94274677a04c9c09f8123a3b2 /src/mainwindow.cpp
parent95ae5dfe2df7858ad957b353dc0ae1d7af3b4832 (diff)
downloadqtmaildir-84e3205ddcba3263e6c07fa314437318309d4b76.tar.gz
qtmaildir-84e3205ddcba3263e6c07fa314437318309d4b76.zip
feat(compose): register the six compose actions, item 123
Handlers are empty for now; this commit is the registration, so the three coverage tests guard every later task rather than being satisfied at the end. Two corrections to the spec, both found in the code rather than assumed. It calls for a new top-level Message menu and one already exists, so these join it; two menus named Message would be a defect. And it says every action needs a binding, which item 132 changed while this was being planned: save_message ships with no chord, since it is the rarely-used escape hatch and menu reachability is now the rule that must hold. reply_no_quote shares reply's icon and is added to the no-duplicate-icons exception list for the same reason the five thread actions are: it never reaches the toolbar, and a menu entry always carries its text. That list is renamed menuOnlySharedIconActions, after the property that earns the exemption rather than the tier that first needed it. Bindings are provisional. The user intends to rework them, and Ctrl+Alt+R for reply_no_quote is an imperfect fit since that tier elsewhere means a wider scope rather than a variant. The six labels went through a mnemonic pass that nothing enforced before. Four of them collided inside the Message menu on first writing, and the whole class was invisible to a green suite: Qt does not error on a duplicate mnemonic, it cycles the highlight instead of activating, so the key simply stops working. Item 57 had already decided this rule by rejecting a label that would have collided, but it lived in prose and in one test's comment, which is precisely why it was broken again here. noMenuHasTwoEntriesSharingAMnemonic() enforces it now, scoped per menu since a mnemonic resolves among the open menu's entries, and keyed on QKeySequence::mnemonic() rather than on parsing & by hand, because && is a literal ampersand and only Qt answers which key it will dispatch. Three pre-existing collisions are a named freeze list rather than a silent fix or a narrowed test: Alt+R three ways and Alt+S twice in Message, Alt+O in View. Renaming entries a user has had in their fingers since 0.1.0 belongs to the shortcuts rework, and the freeze is written as exact groups so a new entry joining any of them still fails. Two of the test's own design choices came from mutation checks that failed for the right reason while reporting the wrong thing. Reporting collisions as pairs was order-dependent, so a new colliding entry re-keyed a frozen pair and the fresh defect read as "a frozen collision no longer happens"; matching frozen entries by whole string broke the same way, since a growing group stopped matching its frozen text. It reports whole groups and matches on menu plus key. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FXF741wz4SY7j5dqvAxMU5
Diffstat (limited to 'src/mainwindow.cpp')
-rw-r--r--src/mainwindow.cpp87
1 files changed, 87 insertions, 0 deletions
diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp
index dc416ca..5155c09 100644
--- a/src/mainwindow.cpp
+++ b/src/mainwindow.cpp
@@ -758,6 +758,27 @@ void MainWindow::buildUi()
setWindowTitle(QStringLiteral("qtmaildir %1").arg(QTMAILDIR_VERSION));
}
+// The six compose handlers, empty until the composer exists (item 123).
+//
+// Deliberately empty rather than absent. Registering the actions first means
+// everyKnownActionIsRegistered, everyActionCarriesAnIcon and
+// everyActionIsReachableFromAMenu cover them while the composer is being
+// built; a menu entry that does nothing yet is a smaller defect than an action
+// nobody can reach, which is what those tests exist to catch.
+void MainWindow::composeNew()
+{
+}
+
+void MainWindow::composeReply(ComposeContext::Kind kind, bool quote)
+{
+ Q_UNUSED(kind);
+ Q_UNUSED(quote);
+}
+
+void MainWindow::saveDisplayedMessage()
+{
+}
+
QAction *MainWindow::addAction(const QString &name, const QString &text,
const QString &description,
const std::function<void()> &handler)
@@ -1124,6 +1145,34 @@ void MainWindow::registerActions()
addAction(QStringLiteral("quit"), tr("&Quit"),
tr("Quit qtmaildir"), [this]() { close(); });
+ // Compose and send (item 123). The handlers are empty: this is the
+ // registration, so the three coverage tests
+ // (everyKnownActionIsRegistered, everyActionCarriesAnIcon and
+ // everyActionIsReachableFromAMenu) cover the composer from the first
+ // commit rather than being satisfied once it is finished.
+ //
+ // Reply and reply-without-quoting are the same Kind with and without a
+ // seeded body, which is why the quoting is a parameter rather than a
+ // fourth Kind: the recipients, the subject prefix and the threading
+ // headers are identical, and only the body differs.
+ addAction(QStringLiteral("compose"), tr("&New message"),
+ tr("Compose a new message"), [this]() { composeNew(); });
+ addAction(QStringLiteral("reply"), tr("Re&ply"),
+ tr("Reply to the displayed message"),
+ [this]() { composeReply(ComposeContext::Kind::Reply, true); });
+ addAction(QStringLiteral("reply_all"), tr("Reply to a&ll"),
+ tr("Reply to the sender and every other recipient"),
+ [this]() { composeReply(ComposeContext::Kind::ReplyAll, true); });
+ addAction(QStringLiteral("reply_no_quote"), tr("Reply without &quoting"),
+ tr("Reply with an empty body"),
+ [this]() { composeReply(ComposeContext::Kind::Reply, false); });
+ addAction(QStringLiteral("forward"), tr("&Forward"),
+ tr("Forward the displayed message"),
+ [this]() { composeReply(ComposeContext::Kind::Forward, true); });
+ addAction(QStringLiteral("save_message"), tr("Sa&ve message as..."),
+ tr("Write the raw message to a file"),
+ [this]() { saveDisplayedMessage(); });
+
// A binding the user wrote for an action that does not exist would be
// silently dead. KeyMap warns about unknown names, but only a check here
// catches the reverse: a known action nothing implements.
@@ -1154,6 +1203,18 @@ void MainWindow::buildMenus()
editMenu->addAction(m_actions.value(QStringLiteral("select_all")));
auto *messageMenu = menuBar()->addMenu(tr("&Message"));
+ // Composing sits above organising (item 123). The spec called for a new
+ // top-level Message menu and this one already existed, so the six join it:
+ // two menus named Message would be a defect.
+ messageMenu->addAction(m_actions.value(QStringLiteral("compose")));
+ messageMenu->addSeparator();
+ messageMenu->addAction(m_actions.value(QStringLiteral("reply")));
+ messageMenu->addAction(m_actions.value(QStringLiteral("reply_all")));
+ messageMenu->addAction(m_actions.value(QStringLiteral("reply_no_quote")));
+ messageMenu->addAction(m_actions.value(QStringLiteral("forward")));
+ messageMenu->addSeparator();
+ messageMenu->addAction(m_actions.value(QStringLiteral("save_message")));
+ messageMenu->addSeparator();
messageMenu->addAction(m_actions.value(QStringLiteral("archive")));
messageMenu->addAction(m_actions.value(QStringLiteral("delete")));
// Beside Delete, whose inverse it is. Greyed outside the trash view
@@ -1282,6 +1343,22 @@ void MainWindow::buildMenus()
{ QStringLiteral("spam_thread"), QStringLiteral("mail-mark-junk") },
{ QStringLiteral("toggle_unread_thread"), QStringLiteral("mail-mark-unread") },
{ QStringLiteral("flag_thread"), QStringLiteral("mail-mark-important") },
+
+ // Compose and send (item 123). reply_no_quote SHARES reply's icon for
+ // the same reason the five above share theirs: it never reaches the
+ // toolbar, it is a menu entry that always carries its text, and
+ // "Reply without quoting" beside the reply icon is the honest pairing.
+ // It is named in the exception list in noTwoActionsShareAnIcon(), so
+ // putting it on the toolbar fails that test rather than passing
+ // silently.
+ { QStringLiteral("compose"), QStringLiteral("mail-message-new") },
+ { QStringLiteral("reply"), QStringLiteral("mail-reply-sender") },
+ { QStringLiteral("reply_all"), QStringLiteral("mail-reply-all") },
+ { QStringLiteral("reply_no_quote"), QStringLiteral("mail-reply-sender") },
+ { QStringLiteral("forward"), QStringLiteral("mail-forward") },
+ // NOT bookmark-new, which save_query uses: this really does write a
+ // file the user names, which is exactly what the disk shape means.
+ { QStringLiteral("save_message"), QStringLiteral("document-save-as") },
};
for (auto it = themeIcons.cbegin(); it != themeIcons.cend(); ++it) {
QAction *action = m_actions.value(it.key());
@@ -1340,6 +1417,16 @@ void MainWindow::buildMenus()
// anything this code can see.
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();
+
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