Files
qt-creator/src/plugins/terminal/shortcutmap.cpp
Marcus Tillmanns a3853ecc44 Terminal: Q_ASSERT workaround
Change-Id: I7e1bf06db437b523e8b06695c3b12f2562a73ce8
Reviewed-by: Eike Ziller <eike.ziller@qt.io>
Reviewed-by: David Schulz <david.schulz@qt.io>
2023-10-27 10:13:02 +00:00

570 lines
19 KiB
C++

// 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 <QAction>
#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)
{
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: {
resetState();
return dispatchEvent(e);
}
}
#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.
*/
bool ShortcutMap::dispatchEvent(QKeyEvent *e)
{
Q_D(ShortcutMap);
if (!d->identicals.size())
return false;
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 false;
// 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 << ')';
}
if (auto action = qobject_cast<QAction *>(next->owner)) {
// We call the action here ourselves instead of relying on sending a ShortCut event,
// as the action will try to match the shortcut id to the global shortcutmap.
// This triggers an annoying Q_ASSERT when linking against a debug Qt. Calling trigger
// directly circumvents this.
action->trigger();
return action->isEnabled();
} else {
QShortcutEvent se(next->keyseq, next->id, enabledShortcuts > 1);
QCoreApplication::sendEvent(const_cast<QObject *>(next->owner), &se);
}
return true;
}
} // namespace Terminal::Internal