diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/keymap.cpp | 36 | ||||
| -rw-r--r-- | src/keymap.h | 9 | ||||
| -rw-r--r-- | src/mainwindow.cpp | 45 |
3 files changed, 73 insertions, 17 deletions
diff --git a/src/keymap.cpp b/src/keymap.cpp index 29a5f71..cdd26a1 100644 --- a/src/keymap.cpp +++ b/src/keymap.cpp @@ -18,6 +18,8 @@ #include "keymap.h" +#include <algorithm> + #include <QSettings> QStringList KeyMap::knownActions() @@ -64,6 +66,18 @@ QList<QPair<QString, QString>> KeyMap::defaultBindings() return { { QStringLiteral("Ctrl+J"), QStringLiteral("next_thread") }, { QStringLiteral("Ctrl+K"), QStringLiteral("prev_thread") }, + // Alt, because Shift+Up/Down is QTreeView's built-in extend-selection, + // which multi-row tagging depends on, and plain Up/Down is the view's + // own navigation, which already steps INTO an expanded thread's + // replies and is what gives message-to-message movement for free. + // + // These must stay chords. Every action is a QAction with + // WindowShortcut, dispatched before the focused widget sees the key, + // and Qt withholds only plain LETTERS from editable widgets: a bare + // Up bound here would break the arrow keys in the query bar, the tag + // dialog and the web view at once, exactly as Return did. + { QStringLiteral("Alt+Down"), QStringLiteral("next_thread") }, + { QStringLiteral("Alt+Up"), QStringLiteral("prev_thread") }, { QStringLiteral("Return"), QStringLiteral("open_thread") }, { QStringLiteral("Ctrl+E"), QStringLiteral("archive") }, { QStringLiteral("Ctrl+D"), QStringLiteral("delete") }, @@ -153,6 +167,28 @@ void KeyMap::loadDefaults() m_bindings.insert(normalizeSequence(binding.first), binding.second); } +QList<QKeySequence> KeyMap::sequencesFor(const QString &action) const +{ + const QKeySequence primary = sequenceFor(action); + if (primary.isEmpty()) + return {}; + + QList<QKeySequence> all{ primary }; + QList<QKeySequence> rest; + for (auto it = m_bindings.cbegin(); it != m_bindings.cend(); ++it) { + if (it.value() == action && it.key() != primary) + rest.append(it.key()); + } + // QHash iteration order is unspecified, so the tail is sorted rather than + // left to chance: an action's shortcut list must not reorder between runs. + std::sort(rest.begin(), rest.end(), + [](const QKeySequence &a, const QKeySequence &b) { + return a.toString() < b.toString(); + }); + all += rest; + return all; +} + QKeySequence KeyMap::sequenceFor(const QString &action) const { // Several sequences can reach one action: the built-in default, which diff --git a/src/keymap.h b/src/keymap.h index 1c7df5f..f0addf5 100644 --- a/src/keymap.h +++ b/src/keymap.h @@ -55,6 +55,15 @@ public: /// text, so the menu shows a stable choice rather than a hash-order one. QKeySequence sequenceFor(const QString &action) const; + /// EVERY sequence bound to an action, with sequenceFor()'s choice first. + /// + /// An action can have more than one binding, and setShortcut() keeps only + /// the last: next_thread ships with both Ctrl+J and Alt+Down, and with the + /// singular setter whichever arrived second was silently unreachable. + /// Ordered rather than hash-ordered, so the menu still advertises the same + /// binding sequenceFor() chose. + QList<QKeySequence> sequencesFor(const QString &action) const; + /// The built-in sequence for an action, ignoring any user override. static QKeySequence defaultSequenceFor(const QString &action); diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index 30148d4..57a6988 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -629,9 +629,11 @@ QAction *MainWindow::addAction(const QString &name, const QString &text, // The binding comes from KeyMap, so a [keys] override reaches the menus // and the shortcut reference as well as the keyboard. - const QKeySequence sequence = m_keyMap.sequenceFor(name); - if (!sequence.isEmpty()) - action->setShortcut(sequence); + // Plural: an action can carry more than one binding, and setShortcut() + // keeps only the last one given. next_thread has both Ctrl+J and Alt+Down. + const QList<QKeySequence> sequences = m_keyMap.sequencesFor(name); + if (!sequences.isEmpty()) + action->setShortcuts(sequences); // Shortcuts must work while focus is in the thread list or the message // view, not only on the window itself. @@ -655,23 +657,32 @@ void MainWindow::registerActions() }); addAction(QStringLiteral("next_thread"), tr("&Next thread"), tr("Select the next thread"), [this]() { - // The THREAD after this one, which is not "the next row" once replies - // are expanded: from a thread row the next row may be its own first - // reply, and from a reply row the row number counts siblings, not - // threads. Both are resolved by walking up to the containing thread - // first. - const QModelIndex current = m_threadView->currentIndex(); - const QModelIndex thread = threadRowOf(current); - const int row = thread.isValid() ? thread.row() + 1 : 0; - if (row < m_model->rowCount()) - selectThreadRow(row); + // Walked by INDEX, never by row number. A tree numbers rows per + // parent, so current.row() + 1 names a SIBLING: from the last reply of + // an expanded thread it asks for a row that does not exist, and from a + // thread row it counts top-level threads only by accident (item 60). + // + // The skip loop is what keeps this meaning thread-to-thread while the + // view's own Up/Down still steps message-to-message. + QModelIndex index = m_threadView->indexBelow( + m_threadView->currentIndex()); + while (index.isValid() + && index.data(ThreadListModel::IsMessageRole).toBool()) { + index = m_threadView->indexBelow(index); + } + if (index.isValid()) + selectRowAt(index); }); addAction(QStringLiteral("prev_thread"), tr("&Previous thread"), tr("Select the previous thread"), [this]() { - const QModelIndex current = m_threadView->currentIndex(); - const QModelIndex thread = threadRowOf(current); - if (thread.isValid() && thread.row() > 0) - selectThreadRow(thread.row() - 1); + QModelIndex index = m_threadView->indexAbove( + m_threadView->currentIndex()); + while (index.isValid() + && index.data(ThreadListModel::IsMessageRole).toBool()) { + index = m_threadView->indexAbove(index); + } + if (index.isValid()) + selectRowAt(index); }); addAction(QStringLiteral("open_thread"), tr("&Open thread"), tr("Focus the thread list"), [this]() { |
