diff options
| author | Danilo M. <danix@danix.xyz> | 2026-08-03 14:08:59 +0200 |
|---|---|---|
| committer | Danilo M. <danix@danix.xyz> | 2026-08-03 14:08:59 +0200 |
| commit | 188287f14f981ed3e8a08bc1514bb2ba6bb76809 (patch) | |
| tree | f5d0404514ecc4daa7553b726a2c3047b5c690fb /src | |
| parent | afd20c9527fa77ba60901707c7bc73b2af926a67 (diff) | |
| download | qtmaildir-188287f14f981ed3e8a08bc1514bb2ba6bb76809.tar.gz qtmaildir-188287f14f981ed3e8a08bc1514bb2ba6bb76809.zip | |
fix: bind shortcuts users can actually press
Typing a capital emits Shift+<key>, but QKeySequence::fromString() folds
the case of a bare letter away: "N" parsed to plain Key_N, a combination
no keystroke produces. The N, F and G defaults (toggle_unread, flag and
sync) therefore never fired, and neither would any hand-written capital
in [keys].
normalizeSequence() rewrites a bare capital to Shift+<letter> and is
shared by the defaults and the override pass. As a side effect "y" and
"Y" become distinct keys rather than a collision that dropped one.
Defaults move to modifier shortcuts throughout. A single letter cannot
be a QAction shortcut without stealing that letter from every text field
in the window, and the menus in the next commit need real accelerators.
defaultBindings() is now the one source of truth for them.
Diffstat (limited to 'src')
| -rw-r--r-- | src/keymap.cpp | 112 | ||||
| -rw-r--r-- | src/keymap.h | 27 |
2 files changed, 117 insertions, 22 deletions
diff --git a/src/keymap.cpp b/src/keymap.cpp index 39991dc..e53c2bf 100644 --- a/src/keymap.cpp +++ b/src/keymap.cpp @@ -41,25 +41,95 @@ 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; +} - for (auto it = defaults.cbegin(); it != defaults.cend(); ++it) - m_bindings.insert(QKeySequence::fromString(it.key()), it.value()); +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 point at one action (a default the user did not + // remove, plus their own addition). QHash iteration order is unspecified, + // so pick deterministically rather than taking whichever comes first. + QKeySequence best; + for (auto it = m_bindings.cbegin(); it != m_bindings.cend(); ++it) { + if (it.value() != action) + continue; + const QString candidate = it.key().toString(); + if (best.isEmpty() || candidate.size() < best.toString().size() + || (candidate.size() == best.toString().size() + && candidate < best.toString())) { + best = it.key(); + } + } + return best; +} + +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 +149,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; diff --git a/src/keymap.h b/src/keymap.h index 564eb10..1c7df5f 100644 --- a/src/keymap.h +++ b/src/keymap.h @@ -20,6 +20,8 @@ #include <QHash> #include <QKeySequence> +#include <QList> +#include <QPair> #include <QStringList> class QSettings; @@ -33,6 +35,11 @@ public: /// anything not in this set, so a typo in the config cannot bind silently. static QStringList knownActions(); + /// The built-in bindings, in menu order: {sequence, action}. The single + /// source of truth for the defaults, so the menus, the shortcut reference + /// and loadDefaults() cannot disagree about them. + static QList<QPair<QString, QString>> defaultBindings(); + void loadDefaults(); /// Reads the [keys] group. Invalid sequences and unknown action names are @@ -42,6 +49,26 @@ public: /// Empty string when nothing is bound. QString actionFor(const QKeySequence &sequence) const; + /// The sequence currently bound to an action, empty if none. The reverse + /// of actionFor(): menus need a shortcut for an action they already know. + /// When several sequences are bound to one action, returns the shortest + /// text, so the menu shows a stable choice rather than a hash-order one. + QKeySequence sequenceFor(const QString &action) const; + + /// The built-in sequence for an action, ignoring any user override. + static QKeySequence defaultSequenceFor(const QString &action); + + /// Every action name carrying a built-in binding. + static QStringList defaultActions(); + + /// Normalizes a configured key string into the sequence a real keypress + /// produces. QKeySequence::fromString() discards the case of a bare + /// letter, so "N" parses to plain Key_N, which no keystroke ever emits: + /// typing a capital sends Shift+N. A bare uppercase letter is therefore + /// rewritten to Shift+<letter>. Returns an empty sequence for input + /// fromString() cannot parse. + static QKeySequence normalizeSequence(const QString &text); + QStringList warnings() const { return m_warnings; } private: |
