diff options
Diffstat (limited to 'src/plugins/terminal/shortcutmap.cpp')
-rw-r--r-- | src/plugins/terminal/shortcutmap.cpp | 564 |
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 |