summaryrefslogtreecommitdiffstats
path: root/src/keymap.cpp
diff options
context:
space:
mode:
authorDanilo M. <danix@danix.xyz>2026-08-03 15:42:40 +0200
committerDanilo M. <danix@danix.xyz>2026-08-03 15:42:40 +0200
commiteb35acda4207392c10b4b1192b3b057bcad99a47 (patch)
treec962e91fc502ec57d92df386e773033fe42fd17b /src/keymap.cpp
parentafd20c9527fa77ba60901707c7bc73b2af926a67 (diff)
parentf62ced3c2c85675e746bff7ef8aca5c75c9737e0 (diff)
downloadqtmaildir-eb35acda4207392c10b4b1192b3b057bcad99a47.tar.gz
qtmaildir-eb35acda4207392c10b4b1192b3b057bcad99a47.zip
Merge branch 'feature/qaction-menus'
Menus, a toolbar and a generated shortcut reference, built on converting the action registry from a hash of callbacks to QActions. Along the way: three default key bindings that had never fired, a shortcut dialog taller than the screen, thread list columns that could not be resized, no visible feedback that a tag action had landed, and a tags column so wide it was unreadable. Backlog items 3, 8, 9, 13 and 14 done; 11 partly.
Diffstat (limited to 'src/keymap.cpp')
-rw-r--r--src/keymap.cpp128
1 files changed, 106 insertions, 22 deletions
diff --git a/src/keymap.cpp b/src/keymap.cpp
index 39991dc..42ccd40 100644
--- a/src/keymap.cpp
+++ b/src/keymap.cpp
@@ -41,25 +41,111 @@ QStringList KeyMap::knownActions()
};
}
-void KeyMap::loadDefaults()
+QList<QPair<QString, QString>> KeyMap::defaultBindings()
{
- const QHash<QString, QString> defaults = {
- { QStringLiteral("j"), QStringLiteral("next_thread") },
- { QStringLiteral("k"), QStringLiteral("prev_thread") },
- { QStringLiteral("Return"), QStringLiteral("open_thread") },
- { QStringLiteral("a"), QStringLiteral("archive") },
- { QStringLiteral("d"), QStringLiteral("delete") },
- { QStringLiteral("N"), QStringLiteral("toggle_unread") },
- { QStringLiteral("F"), QStringLiteral("flag") },
- { QStringLiteral("/"), QStringLiteral("focus_query") },
- { QStringLiteral("h"), QStringLiteral("toggle_html") },
- { QStringLiteral("u"), QStringLiteral("undo") },
- { QStringLiteral("G"), QStringLiteral("sync") },
- { QStringLiteral("Ctrl+Q"), QStringLiteral("quit") },
+ // Modifier shortcuts throughout, rather than the bare letters of 0.1.0.
+ // Two reasons. A bare capital never worked: "N" parses to plain Key_N
+ // while typing a capital emits Shift+N, so toggle_unread, flag and sync
+ // were dead keys. And a single letter cannot be a QAction shortcut in a
+ // menu without stealing that letter from every text field in the window.
+ //
+ // Ordered as the menus present them; a QList keeps that order, which a
+ // QHash would not.
+ return {
+ { QStringLiteral("Ctrl+J"), QStringLiteral("next_thread") },
+ { QStringLiteral("Ctrl+K"), QStringLiteral("prev_thread") },
+ { QStringLiteral("Return"), QStringLiteral("open_thread") },
+ { QStringLiteral("Ctrl+E"), QStringLiteral("archive") },
+ { QStringLiteral("Ctrl+D"), QStringLiteral("delete") },
+ { QStringLiteral("Ctrl+Shift+S"), QStringLiteral("spam") },
+ { QStringLiteral("Ctrl+U"), QStringLiteral("toggle_unread") },
+ { QStringLiteral("Ctrl+I"), QStringLiteral("flag") },
+ { QStringLiteral("Ctrl+L"), QStringLiteral("focus_query") },
+ { QStringLiteral("Ctrl+H"), QStringLiteral("toggle_html") },
+ { QStringLiteral("Ctrl+M"), QStringLiteral("load_remote") },
+ { QStringLiteral("Ctrl+Z"), QStringLiteral("undo") },
+ { QStringLiteral("Ctrl+G"), QStringLiteral("sync") },
+ { QStringLiteral("Ctrl+Q"), QStringLiteral("quit") },
};
+}
+
+QStringList KeyMap::defaultActions()
+{
+ QStringList actions;
+ const auto bindings = defaultBindings();
+ actions.reserve(bindings.size());
+ for (const auto &binding : bindings)
+ actions.append(binding.second);
+ return actions;
+}
+
+QKeySequence KeyMap::normalizeSequence(const QString &text)
+{
+ const QKeySequence sequence = QKeySequence::fromString(text);
+
+ // fromString() does not return an empty sequence for unparseable input;
+ // it returns a non-empty one whose toString() is empty (verified on
+ // Qt 6.11). Both checks are needed to detect garbage.
+ if (sequence.isEmpty() || sequence.toString().isEmpty())
+ return {};
+
+ // A bare uppercase letter, no modifiers: the user wrote "N" meaning the
+ // key they press to type a capital N, which is Shift+N. fromString()
+ // folded the case away, so put the Shift back.
+ if (text.size() == 1 && text.at(0).isUpper() && text.at(0).isLetter())
+ return QKeySequence(sequence[0].key() | Qt::SHIFT);
+
+ return sequence;
+}
+
+void KeyMap::loadDefaults()
+{
+ for (const auto &binding : defaultBindings())
+ m_bindings.insert(normalizeSequence(binding.first), binding.second);
+}
+
+QKeySequence KeyMap::sequenceFor(const QString &action) const
+{
+ // Several sequences can reach one action: the built-in default, which
+ // loadOverrides() does not remove, plus whatever the user added. Their
+ // binding is the one to show and to put on the QAction, or configuring
+ // "Ctrl+Alt+A = archive" would leave the menu still advertising Ctrl+E.
+ //
+ // QHash iteration order is unspecified, so ties are broken on the text
+ // rather than left to chance.
+ const QKeySequence builtIn = defaultSequenceFor(action);
+ QKeySequence best;
+ bool bestIsBuiltIn = false;
+
+ for (auto it = m_bindings.cbegin(); it != m_bindings.cend(); ++it) {
+ if (it.value() != action)
+ continue;
+
+ const bool isBuiltIn = !builtIn.isEmpty() && it.key() == builtIn;
+ if (best.isEmpty()) {
+ best = it.key();
+ bestIsBuiltIn = isBuiltIn;
+ continue;
+ }
+ // A user binding always beats the default.
+ if (bestIsBuiltIn && !isBuiltIn) {
+ best = it.key();
+ bestIsBuiltIn = false;
+ } else if (bestIsBuiltIn == isBuiltIn
+ && it.key().toString() < best.toString()) {
+ best = it.key();
+ }
+ }
+ return best;
+}
- for (auto it = defaults.cbegin(); it != defaults.cend(); ++it)
- m_bindings.insert(QKeySequence::fromString(it.key()), it.value());
+QKeySequence KeyMap::defaultSequenceFor(const QString &action)
+{
+ for (const auto &binding : defaultBindings()) {
+ if (binding.second == action)
+ return normalizeSequence(binding.first);
+ }
+ return {};
}
void KeyMap::loadOverrides(QSettings &settings)
@@ -79,12 +165,10 @@ void KeyMap::loadOverrides(QSettings &settings)
for (const QString &key : keys) {
const QString action = settings.value(key).toString();
- const QKeySequence sequence = QKeySequence::fromString(key);
- // QKeySequence::fromString() does not return an empty sequence for
- // unparseable input; it returns a non-empty sequence whose
- // toString() is empty (verified on Qt 6.11). Use that to detect
- // garbage input instead.
- if (sequence.isEmpty() || sequence.toString().isEmpty()) {
+ // Shares the defaults' normalization, so a hand-written "N" binds the
+ // key the user actually presses rather than one nothing emits.
+ const QKeySequence sequence = normalizeSequence(key);
+ if (sequence.isEmpty()) {
m_warnings.append(
QStringLiteral("Unparseable key sequence '%1' in [keys]").arg(key));
continue;