aboutsummaryrefslogtreecommitdiffstats
path: root/src/plugins/terminal/shortcutmap.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'src/plugins/terminal/shortcutmap.cpp')
-rw-r--r--src/plugins/terminal/shortcutmap.cpp564
1 files changed, 564 insertions, 0 deletions
diff --git a/src/plugins/terminal/shortcutmap.cpp b/src/plugins/terminal/shortcutmap.cpp
new file mode 100644
index 0000000000..72023e808a
--- /dev/null
+++ b/src/plugins/terminal/shortcutmap.cpp
@@ -0,0 +1,564 @@
+// Copyright (C) 2023 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
+
+// COPIED FROM qshortcutmap.cpp
+
+#include "shortcutmap.h"
+
+#include <utils/qtcassert.h>
+
+#include <algorithm>
+
+#include <QGuiApplication>
+#include <QKeyEvent>
+#include <QLoggingCategory>
+#include <QWindow>
+
+Q_LOGGING_CATEGORY(lcShortcutMap, "terminal.shortcutmap", QtWarningMsg)
+
+namespace Terminal::Internal {
+
+/* \internal
+ Entry data for ShortcutMap
+ Contains:
+ Keysequence for entry
+ Pointer to parent owning the sequence
+*/
+struct ShortcutEntry
+{
+ ShortcutEntry()
+ : keyseq(0)
+ , context(Qt::WindowShortcut)
+ , enabled(false)
+ , autorepeat(1)
+ , id(0)
+ , owner(nullptr)
+ , contextMatcher(nullptr)
+ {}
+
+ ShortcutEntry(const QKeySequence &k)
+ : keyseq(k)
+ , context(Qt::WindowShortcut)
+ , enabled(false)
+ , autorepeat(1)
+ , id(0)
+ , owner(nullptr)
+ , contextMatcher(nullptr)
+ {}
+
+ ShortcutEntry(QObject *o,
+ const QKeySequence &k,
+ Qt::ShortcutContext c,
+ int i,
+ bool a,
+ ShortcutMap::ContextMatcher m)
+ : keyseq(k)
+ , context(c)
+ , enabled(true)
+ , autorepeat(a)
+ , id(i)
+ , owner(o)
+ , contextMatcher(m)
+ {}
+
+ bool correctContext() const { return contextMatcher(owner, context); }
+
+ bool operator<(const ShortcutEntry &f) const { return keyseq < f.keyseq; }
+
+ QKeySequence keyseq;
+ Qt::ShortcutContext context;
+ bool enabled : 1;
+ bool autorepeat : 1;
+ signed int id;
+ QObject *owner;
+ ShortcutMap::ContextMatcher contextMatcher;
+};
+
+/* \internal
+ Private data for ShortcutMap
+*/
+class ShortcutMapPrivate
+{
+ Q_DECLARE_PUBLIC(ShortcutMap)
+
+public:
+ ShortcutMapPrivate(ShortcutMap *parent)
+ : q_ptr(parent)
+ , currentId(0)
+ , ambigCount(0)
+ , currentState(QKeySequence::NoMatch)
+ {
+ identicals.reserve(10);
+ currentSequences.reserve(10);
+ }
+ ShortcutMap *q_ptr; // Private's parent
+
+ QList<ShortcutEntry> sequences; // All sequences!
+
+ int currentId; // Global shortcut ID number
+ int ambigCount; // Index of last enabled ambiguous dispatch
+ QKeySequence::SequenceMatch currentState;
+ QList<QKeySequence> currentSequences; // Sequence for the current state
+ QList<QKeySequence> newEntries;
+ QKeySequence prevSequence; // Sequence for the previous identical match
+ QList<const ShortcutEntry *> identicals; // Last identical matches
+};
+
+/*! \internal
+ ShortcutMap constructor.
+*/
+ShortcutMap::ShortcutMap()
+ : d_ptr(new ShortcutMapPrivate(this))
+{
+ resetState();
+}
+
+/*! \internal
+ ShortcutMap destructor.
+*/
+ShortcutMap::~ShortcutMap() {}
+
+/*! \internal
+ Adds a shortcut to the global map.
+ Returns the id of the newly added shortcut.
+*/
+int ShortcutMap::addShortcut(QObject *owner,
+ const QKeySequence &key,
+ Qt::ShortcutContext context,
+ ContextMatcher matcher)
+{
+ QTC_ASSERT(owner, return 0); // "ShortcutMap::addShortcut", "All shortcuts need an owner");
+ QTC_ASSERT(!key.isEmpty(),
+ return 0); // "ShortcutMap::addShortcut", "Cannot add keyless shortcuts to map");
+ Q_D(ShortcutMap);
+
+ ShortcutEntry newEntry(owner, key, context, --(d->currentId), true, matcher);
+ const auto it = std::upper_bound(d->sequences.begin(), d->sequences.end(), newEntry);
+ d->sequences.insert(it, newEntry); // Insert sorted
+ qCDebug(lcShortcutMap).nospace() << "ShortcutMap::addShortcut(" << owner << ", " << key << ", "
+ << context << ") added shortcut with ID " << d->currentId;
+ return d->currentId;
+}
+
+/*! \internal
+ Removes a shortcut from the global map.
+ If \a owner is \nullptr, all entries in the map with the key sequence specified
+ is removed. If \a key is null, all sequences for \a owner is removed from
+ the map. If \a id is 0, any identical \a key sequences owned by \a owner
+ are removed.
+ Returns the number of sequences removed from the map.
+*/
+
+int ShortcutMap::removeShortcut(int id, QObject *owner, const QKeySequence &key)
+{
+ Q_D(ShortcutMap);
+ int itemsRemoved = 0;
+ bool allOwners = (owner == nullptr);
+ bool allKeys = key.isEmpty();
+ bool allIds = id == 0;
+
+ auto debug = qScopeGuard([&]() {
+ qCDebug(lcShortcutMap).nospace()
+ << "ShortcutMap::removeShortcut(" << id << ", " << owner << ", " << key << ") removed "
+ << itemsRemoved << " shortcuts(s)";
+ });
+
+ // Special case, remove everything
+ if (allOwners && allKeys && allIds) {
+ itemsRemoved = d->sequences.size();
+ d->sequences.clear();
+ return itemsRemoved;
+ }
+
+ int i = d->sequences.size() - 1;
+ while (i >= 0) {
+ const ShortcutEntry &entry = d->sequences.at(i);
+ int entryId = entry.id;
+ if ((allOwners || entry.owner == owner) && (allIds || entry.id == id)
+ && (allKeys || entry.keyseq == key)) {
+ d->sequences.removeAt(i);
+ ++itemsRemoved;
+ }
+ if (id == entryId)
+ return itemsRemoved;
+ --i;
+ }
+ return itemsRemoved;
+}
+
+/*! \internal
+ Resets the state of the statemachine to NoMatch
+*/
+void ShortcutMap::resetState()
+{
+ Q_D(ShortcutMap);
+ d->currentState = QKeySequence::NoMatch;
+ clearSequence(d->currentSequences);
+}
+
+/*! \internal
+ Returns the current state of the statemachine
+*/
+QKeySequence::SequenceMatch ShortcutMap::state()
+{
+ Q_D(ShortcutMap);
+ return d->currentState;
+}
+
+/*! \internal
+ Uses nextState(QKeyEvent) to check for a grabbed shortcut.
+
+ If so, it is dispatched using dispatchEvent().
+
+ Returns true if a shortcut handled the event.
+
+ \sa nextState, dispatchEvent
+*/
+bool ShortcutMap::tryShortcut(QKeyEvent *e)
+{
+ Q_D(ShortcutMap);
+
+ if (e->key() == Qt::Key_unknown)
+ return false;
+
+ QKeySequence::SequenceMatch previousState = state();
+
+ switch (nextState(e)) {
+ case QKeySequence::NoMatch:
+ // In the case of going from a partial match to no match we handled the
+ // event, since we already stated that we did for the partial match. But
+ // in the normal case of directly going to no match we say we didn't.
+ return previousState == QKeySequence::PartialMatch;
+ case QKeySequence::PartialMatch:
+ // For a partial match we don't know yet if we will handle the shortcut
+ // but we need to say we did, so that we get the follow-up key-presses.
+ return true;
+ case QKeySequence::ExactMatch: {
+ // Save number of identical matches before dispatching
+ // to keep ShortcutMap and tryShortcut reentrant.
+ const int identicalMatches = d->identicals.size();
+ resetState();
+ dispatchEvent(e);
+ // If there are no identicals we've only found disabled shortcuts, and
+ // shouldn't say that we handled the event.
+ return identicalMatches > 0;
+ }
+ }
+#if QT_VERSION >= QT_VERSION_CHECK(6, 5, 0)
+ Q_UNREACHABLE_RETURN(false);
+#else
+ return false;
+#endif
+}
+
+/*! \internal
+ Returns the next state of the statemachine
+ If return value is SequenceMatch::ExactMatch, then a call to matches()
+ will return a QObjects* list of all matching objects for the last matching
+ sequence.
+*/
+QKeySequence::SequenceMatch ShortcutMap::nextState(QKeyEvent *e)
+{
+ Q_D(ShortcutMap);
+ // Modifiers can NOT be shortcuts...
+ if (e->key() >= Qt::Key_Shift && e->key() <= Qt::Key_ScrollLock)
+ return d->currentState;
+
+ QKeySequence::SequenceMatch result = QKeySequence::NoMatch;
+
+ // We start fresh each time..
+ d->identicals.clear();
+
+ result = find(e);
+ if (result == QKeySequence::NoMatch && (e->modifiers() & Qt::KeypadModifier)) {
+ // Try to find a match without keypad modifier
+ result = find(e, Qt::KeypadModifier);
+ }
+ if (result == QKeySequence::NoMatch && e->modifiers() & Qt::ShiftModifier) {
+ // If Shift + Key_Backtab, also try Shift + Qt::Key_Tab
+ if (e->key() == Qt::Key_Backtab) {
+ QKeyEvent pe = QKeyEvent(e->type(), Qt::Key_Tab, e->modifiers(), e->text());
+ result = find(&pe);
+ }
+ }
+
+ // Does the new state require us to clean up?
+ if (result == QKeySequence::NoMatch)
+ clearSequence(d->currentSequences);
+ d->currentState = result;
+
+ qCDebug(lcShortcutMap).nospace() << "ShortcutMap::nextState(" << e << ") = " << result;
+ return result;
+}
+
+/*! \internal
+ Determines if an enabled shortcut has a matching key sequence.
+*/
+bool ShortcutMap::hasShortcutForKeySequence(const QKeySequence &seq) const
+{
+ Q_D(const ShortcutMap);
+ ShortcutEntry entry(seq); // needed for searching
+ const auto itEnd = d->sequences.cend();
+ auto it = std::lower_bound(d->sequences.cbegin(), itEnd, entry);
+
+ for (; it != itEnd; ++it) {
+ if (matches(entry.keyseq, (*it).keyseq) == QKeySequence::ExactMatch
+ && (*it).correctContext() && (*it).enabled) {
+ return true;
+ }
+ }
+
+ //end of the loop: we didn't find anything
+ return false;
+}
+
+/*! \internal
+ Returns the next state of the statemachine, based
+ on the new key event \a e.
+ Matches are appended to the list of identicals,
+ which can be access through matches().
+ \sa matches
+*/
+QKeySequence::SequenceMatch ShortcutMap::find(QKeyEvent *e, int ignoredModifiers)
+{
+ Q_D(ShortcutMap);
+ if (!d->sequences.size())
+ return QKeySequence::NoMatch;
+
+ createNewSequences(e, d->newEntries, ignoredModifiers);
+ qCDebug(lcShortcutMap) << "Possible shortcut key sequences:" << d->newEntries;
+
+ // Should never happen
+ if (d->newEntries == d->currentSequences) {
+ QTC_ASSERT(e->key() != Qt::Key_unknown || e->text().size(), return QKeySequence::NoMatch);
+ //"ShortcutMap::find",
+ // "New sequence to find identical to previous");
+ return QKeySequence::NoMatch;
+ }
+
+ // Looking for new identicals, scrap old
+ d->identicals.clear();
+
+ bool partialFound = false;
+ bool identicalDisabledFound = false;
+ QList<QKeySequence> okEntries;
+ int result = QKeySequence::NoMatch;
+ for (int i = d->newEntries.size() - 1; i >= 0; --i) {
+ ShortcutEntry entry(d->newEntries.at(i)); // needed for searching
+ qCDebug(lcShortcutMap) << "- checking entry" << entry.id << entry.keyseq;
+ const auto itEnd = d->sequences.constEnd();
+ auto it = std::lower_bound(d->sequences.constBegin(), itEnd, entry);
+
+ int oneKSResult = QKeySequence::NoMatch;
+ int tempRes = QKeySequence::NoMatch;
+ do {
+ if (it == itEnd)
+ break;
+ tempRes = matches(entry.keyseq, (*it).keyseq);
+ oneKSResult = qMax(oneKSResult, tempRes);
+ qCDebug(lcShortcutMap) << " - matches returned" << tempRes << "for" << entry.keyseq
+ << it->keyseq << "- correctContext()?" << it->correctContext();
+ if (tempRes != QKeySequence::NoMatch && (*it).correctContext()) {
+ if (tempRes == QKeySequence::ExactMatch) {
+ if ((*it).enabled)
+ d->identicals.append(&*it);
+ else
+ identicalDisabledFound = true;
+ } else if (tempRes == QKeySequence::PartialMatch) {
+ // We don't need partials, if we have identicals
+ if (d->identicals.size())
+ break;
+ // We only care about enabled partials, so we don't consume
+ // key events when all partials are disabled!
+ partialFound |= (*it).enabled;
+ }
+ }
+ ++it;
+ // If we got a valid match on this run, there might still be more keys to check against,
+ // so we'll loop once more. If we get NoMatch, there's guaranteed no more possible
+ // matches in the shortcutmap.
+ } while (tempRes != QKeySequence::NoMatch);
+
+ // If the type of match improves (ergo, NoMatch->Partial, or Partial->Exact), clear the
+ // previous list. If this match is equal or better than the last match, append to the list
+ if (oneKSResult > result) {
+ okEntries.clear();
+ qCDebug(lcShortcutMap)
+ << "Found better match (" << d->newEntries << "), clearing key sequence list";
+ }
+ if (oneKSResult && oneKSResult >= result) {
+ okEntries << d->newEntries.at(i);
+ qCDebug(lcShortcutMap) << "Added ok key sequence" << d->newEntries;
+ }
+ }
+
+ if (d->identicals.size()) {
+ result = QKeySequence::ExactMatch;
+ } else if (partialFound) {
+ result = QKeySequence::PartialMatch;
+ } else if (identicalDisabledFound) {
+ result = QKeySequence::ExactMatch;
+ } else {
+ clearSequence(d->currentSequences);
+ result = QKeySequence::NoMatch;
+ }
+ if (result != QKeySequence::NoMatch)
+ d->currentSequences = okEntries;
+ qCDebug(lcShortcutMap) << "Returning shortcut match == " << result;
+ return QKeySequence::SequenceMatch(result);
+}
+
+/*! \internal
+ Clears \a seq to an empty QKeySequence.
+ Same as doing (the slower)
+ \snippet code/src_gui_kernel_shortcutmap.cpp 0
+*/
+void ShortcutMap::clearSequence(QList<QKeySequence> &ksl)
+{
+ ksl.clear();
+ d_func()->newEntries.clear();
+}
+
+static QList<int> extractKeyFromEvent(QKeyEvent *e)
+{
+ QList<int> result;
+ if (e->key() && (e->key() != Qt::Key_unknown))
+ result << e->keyCombination().toCombined();
+ else if (!e->text().isEmpty())
+ result << int(e->text().at(0).unicode() + (int) e->modifiers());
+ return result;
+}
+
+/*! \internal
+ Alters \a seq to the new sequence state, based on the
+ current sequence state, and the new key event \a e.
+*/
+void ShortcutMap::createNewSequences(QKeyEvent *e, QList<QKeySequence> &ksl, int ignoredModifiers)
+{
+ Q_D(ShortcutMap);
+
+ QList<int> possibleKeys = extractKeyFromEvent(e);
+ qCDebug(lcShortcutMap) << "Creating new sequences for" << e
+ << "with ignoredModifiers=" << Qt::KeyboardModifiers(ignoredModifiers);
+ int pkTotal = possibleKeys.size();
+ if (!pkTotal)
+ return;
+
+ int ssActual = d->currentSequences.size();
+ int ssTotal = qMax(1, ssActual);
+ // Resize to possible permutations of the current sequence(s).
+ ksl.resize(pkTotal * ssTotal);
+
+ int index = ssActual ? d->currentSequences.at(0).count() : 0;
+ for (int pkNum = 0; pkNum < pkTotal; ++pkNum) {
+ for (int ssNum = 0; ssNum < ssTotal; ++ssNum) {
+ int i = (pkNum * ssTotal) + ssNum;
+ QKeySequence &curKsl = ksl[i];
+ if (ssActual) {
+ const QKeySequence &curSeq = d->currentSequences.at(ssNum);
+ curKsl = QKeySequence(curSeq[0], curSeq[1], curSeq[2], curSeq[3]);
+ } else {
+ curKsl = QKeySequence(QKeyCombination::fromCombined(0));
+ }
+
+ std::array<QKeyCombination, 4> cur = {curKsl[0], curKsl[1], curKsl[2], curKsl[3]};
+ cur[index] = QKeyCombination::fromCombined(possibleKeys.at(pkNum) & ~ignoredModifiers);
+ curKsl = QKeySequence(cur[0], cur[1], cur[2], cur[3]);
+ }
+ }
+}
+
+/*! \internal
+ Basically the same function as QKeySequence::matches(const QKeySequence &seq) const
+ only that is specially handles Key_hyphen as Key_Minus, as people mix these up all the time and
+ they conceptually the same.
+*/
+QKeySequence::SequenceMatch ShortcutMap::matches(const QKeySequence &seq1,
+ const QKeySequence &seq2) const
+{
+ uint userN = seq1.count(), seqN = seq2.count();
+
+ if (userN > seqN)
+ return QKeySequence::NoMatch;
+
+ // If equal in length, we have a potential ExactMatch sequence,
+ // else we already know it can only be partial.
+ QKeySequence::SequenceMatch match = (userN == seqN ? QKeySequence::ExactMatch
+ : QKeySequence::PartialMatch);
+
+ for (uint i = 0; i < userN; ++i) {
+ int userKey = seq1[i].toCombined(), sequenceKey = seq2[i].toCombined();
+ if ((userKey & Qt::Key_unknown) == Qt::Key_hyphen)
+ userKey = (userKey & Qt::KeyboardModifierMask) | Qt::Key_Minus;
+ if ((sequenceKey & Qt::Key_unknown) == Qt::Key_hyphen)
+ sequenceKey = (sequenceKey & Qt::KeyboardModifierMask) | Qt::Key_Minus;
+ if (userKey != sequenceKey)
+ return QKeySequence::NoMatch;
+ }
+ return match;
+}
+
+/*! \internal
+ Returns the list of ShortcutEntry's matching the last Identical state.
+*/
+QList<const ShortcutEntry *> ShortcutMap::matches() const
+{
+ Q_D(const ShortcutMap);
+ return d->identicals;
+}
+
+/*! \internal
+ Dispatches QShortcutEvents to widgets who grabbed the matched key sequence.
+*/
+void ShortcutMap::dispatchEvent(QKeyEvent *e)
+{
+ Q_D(ShortcutMap);
+ if (!d->identicals.size())
+ return;
+
+ const QKeySequence &curKey = d->identicals.at(0)->keyseq;
+ if (d->prevSequence != curKey) {
+ d->ambigCount = 0;
+ d->prevSequence = curKey;
+ }
+ // Find next
+ const ShortcutEntry *current = nullptr, *next = nullptr;
+ int i = 0, enabledShortcuts = 0;
+ QList<const ShortcutEntry *> ambiguousShortcuts;
+ while (i < d->identicals.size()) {
+ current = d->identicals.at(i);
+ if (current->enabled || !next) {
+ ++enabledShortcuts;
+ if (lcShortcutMap().isDebugEnabled())
+ ambiguousShortcuts.append(current);
+ if (enabledShortcuts > d->ambigCount + 1)
+ break;
+ next = current;
+ }
+ ++i;
+ }
+ d->ambigCount = (d->identicals.size() == i ? 0 : d->ambigCount + 1);
+ // Don't trigger shortcut if we're autorepeating and the shortcut is
+ // grabbed with not accepting autorepeats.
+ if (!next || (e->isAutoRepeat() && !next->autorepeat))
+ return;
+ // Dispatch next enabled
+ if (lcShortcutMap().isDebugEnabled()) {
+ if (ambiguousShortcuts.size() > 1) {
+ qCDebug(lcShortcutMap)
+ << "The following shortcuts are about to be activated ambiguously:";
+ for (const ShortcutEntry *entry : std::as_const(ambiguousShortcuts))
+ qCDebug(lcShortcutMap).nospace()
+ << "- " << entry->keyseq << " (belonging to " << entry->owner << ")";
+ }
+
+ qCDebug(lcShortcutMap).nospace()
+ << "ShortcutMap::dispatchEvent(): Sending QShortcutEvent(\"" << next->keyseq.toString()
+ << "\", " << next->id << ", " << static_cast<bool>(enabledShortcuts > 1)
+ << ") to object(" << next->owner << ')';
+ }
+ QShortcutEvent se(next->keyseq, next->id, enabledShortcuts > 1);
+ QCoreApplication::sendEvent(const_cast<QObject *>(next->owner), &se);
+}
+
+} // namespace Terminal::Internal