/* This file is part of the KDE libraries SPDX-FileCopyrightText: 1998 Mark Donohoe SPDX-FileCopyrightText: 2001 Ellis Whitehead SPDX-FileCopyrightText: 2007 Andreas Hartmetz SPDX-FileCopyrightText: 2020 David Redondo SPDX-License-Identifier: LGPL-2.0-or-later */ #include "config-xmlgui.h" #include "kkeysequencewidget.h" #include "debug.h" #include "kactioncollection.h" #include #include #include #include #include #include #include #include #if HAVE_GLOBALACCEL #include #endif static constexpr QStringView inputRecordingMarkupSuffix(u" …"); static bool shortcutsConflictWith(const QList &shortcuts, const QKeySequence &needle) { if (needle.isEmpty()) { return false; } for (const QKeySequence &sequence : shortcuts) { if (sequence.isEmpty()) { continue; } if (sequence.matches(needle) != QKeySequence::NoMatch // || needle.matches(sequence) != QKeySequence::NoMatch) { return true; } } return false; } class KKeySequenceWidgetPrivate { public: KKeySequenceWidgetPrivate(KKeySequenceWidget *qq); void init(); void updateShortcutDisplay(); void startRecording(); // Conflicts the key sequence @p seq with a current standard shortcut? bool conflictWithStandardShortcuts(const QKeySequence &seq); // Conflicts the key sequence @p seq with a current local shortcut? bool conflictWithLocalShortcuts(const QKeySequence &seq); // Conflicts the key sequence @p seq with a current global shortcut? bool conflictWithGlobalShortcuts(const QKeySequence &seq); bool promptStealLocalShortcut(const QList &actions, const QKeySequence &seq); bool promptstealStandardShortcut(KStandardShortcut::StandardShortcut std, const QKeySequence &seq); #if HAVE_GLOBALACCEL struct KeyConflictInfo { QKeySequence key; QList shortcutInfo; }; bool promptStealGlobalShortcut(const std::vector &shortcuts, const QKeySequence &sequence); #endif void wontStealShortcut(QAction *item, const QKeySequence &seq); bool checkAgainstStandardShortcuts() const { return checkAgainstShortcutTypes & KKeySequenceWidget::StandardShortcuts; } bool checkAgainstGlobalShortcuts() const { return checkAgainstShortcutTypes & KKeySequenceWidget::GlobalShortcuts; } bool checkAgainstLocalShortcuts() const { return checkAgainstShortcutTypes & KKeySequenceWidget::LocalShortcuts; } // private slot void doneRecording(); // members KKeySequenceWidget *const q; KKeySequenceRecorder *recorder; QHBoxLayout *layout; QPushButton *keyButton; QToolButton *clearButton; QKeySequence keySequence; QKeySequence oldKeySequence; QString componentName; //! Check the key sequence against KStandardShortcut::find() KKeySequenceWidget::ShortcutTypes checkAgainstShortcutTypes; /** * The list of action collections to check against for conflict shortcut */ QList checkActionCollections; /** * The action to steal the shortcut from. */ QList stealActions; }; KKeySequenceWidgetPrivate::KKeySequenceWidgetPrivate(KKeySequenceWidget *qq) : q(qq) , layout(nullptr) , keyButton(nullptr) , clearButton(nullptr) , componentName() , checkAgainstShortcutTypes(KKeySequenceWidget::LocalShortcuts | KKeySequenceWidget::GlobalShortcuts) , stealActions() { } void KKeySequenceWidgetPrivate::init() { layout = new QHBoxLayout(q); layout->setContentsMargins(0, 0, 0, 0); keyButton = new QPushButton(q); keyButton->setFocusPolicy(Qt::StrongFocus); keyButton->setIcon(QIcon::fromTheme(QStringLiteral("configure"))); keyButton->setToolTip( i18nc("@info:tooltip", "Click on the button, then enter the shortcut like you would in the program.\nExample for Ctrl+A: hold the Ctrl key and press A.")); layout->addWidget(keyButton); clearButton = new QToolButton(q); layout->addWidget(clearButton); if (qApp->isLeftToRight()) { clearButton->setIcon(QIcon::fromTheme(QStringLiteral("edit-clear-locationbar-rtl"))); } else { clearButton->setIcon(QIcon::fromTheme(QStringLiteral("edit-clear-locationbar-ltr"))); } recorder = new KKeySequenceRecorder(q->window()->windowHandle(), q); recorder->setModifierlessAllowed(false); recorder->setMultiKeyShortcutsAllowed(true); updateShortcutDisplay(); } bool KKeySequenceWidgetPrivate::promptStealLocalShortcut(const QList &actions, const QKeySequence &seq) { const int listSize = actions.size(); QString title = i18ncp("%1 is the number of conflicts", "Shortcut Conflict", "Shortcut Conflicts", listSize); QString conflictingShortcuts; for (const QAction *action : actions) { conflictingShortcuts += i18n("Shortcut '%1' for action '%2'\n", action->shortcut().toString(QKeySequence::NativeText), KLocalizedString::removeAcceleratorMarker(action->text())); } QString message = i18ncp("%1 is the number of ambiguous shortcut clashes (hidden)", "The \"%2\" shortcut is ambiguous with the following shortcut.\n" "Do you want to assign an empty shortcut to this action?\n" "%3", "The \"%2\" shortcut is ambiguous with the following shortcuts.\n" "Do you want to assign an empty shortcut to these actions?\n" "%3", listSize, seq.toString(QKeySequence::NativeText), conflictingShortcuts); return KMessageBox::warningContinueCancel(q, message, title, KGuiItem(i18nc("@action:button", "Reassign"))) == KMessageBox::Continue; } void KKeySequenceWidgetPrivate::wontStealShortcut(QAction *item, const QKeySequence &seq) { QString title(i18nc("@title:window", "Shortcut conflict")); QString msg( i18n("The '%1' key combination is already used by the %2 action.
" "Please select a different one.
", seq.toString(QKeySequence::NativeText), KLocalizedString::removeAcceleratorMarker(item->text()))); KMessageBox::error(q, msg, title); } bool KKeySequenceWidgetPrivate::conflictWithLocalShortcuts(const QKeySequence &keySequence) { if (!(checkAgainstShortcutTypes & KKeySequenceWidget::LocalShortcuts)) { return false; } // Add all the actions from the checkActionCollections list to a single list to // be able to process them in a single loop below. // Note that this can't be done in setCheckActionCollections(), because we // keep pointers to the action collections, and between the call to // setCheckActionCollections() and this function some actions might already be // removed from the collection again. QList allActions; for (KActionCollection *collection : std::as_const(checkActionCollections)) { allActions += collection->actions(); } // Because of multikey shortcuts we can have clashes with many shortcuts. // // Example 1: // // Application currently uses 'CTRL-X,a', 'CTRL-X,f' and 'CTRL-X,CTRL-F' // and the user wants to use 'CTRL-X'. 'CTRL-X' will only trigger as // 'activatedAmbiguously()' for obvious reasons. // // Example 2: // // Application currently uses 'CTRL-X'. User wants to use 'CTRL-X,CTRL-F'. // This will shadow 'CTRL-X' for the same reason as above. // // Example 3: // // Some weird combination of Example 1 and 2 with three shortcuts using // 1/2/3 key shortcuts. I think you can imagine. QList conflictingActions; // find conflicting shortcuts with existing actions for (QAction *qaction : std::as_const(allActions)) { if (shortcutsConflictWith(qaction->shortcuts(), keySequence)) { // A conflict with a KAction. If that action is configurable // ask the user what to do. If not reject this keySequence. if (KActionCollection::isShortcutsConfigurable(qaction)) { conflictingActions.append(qaction); } else { wontStealShortcut(qaction, keySequence); return true; } } } if (conflictingActions.isEmpty()) { // No conflicting shortcuts found. return false; } if (promptStealLocalShortcut(conflictingActions, keySequence)) { stealActions = conflictingActions; // Announce that the user agreed for (QAction *stealAction : std::as_const(stealActions)) { Q_EMIT q->stealShortcut(keySequence, stealAction); } return false; } return true; } #if HAVE_GLOBALACCEL bool KKeySequenceWidgetPrivate::promptStealGlobalShortcut(const std::vector &clashing, const QKeySequence &sequence) { QString clashingKeys; for (const auto &[key, shortcutInfo] : clashing) { const QString seqAsString = key.toString(); for (const KGlobalShortcutInfo &info : shortcutInfo) { clashingKeys += i18n("Shortcut '%1' in Application '%2' for action '%3'\n", // seqAsString, info.componentFriendlyName(), info.friendlyName()); } } const int hashSize = clashing.size(); QString message = i18ncp("%1 is the number of conflicts (hidden), %2 is the key sequence of the shortcut that is problematic", "The shortcut '%2' conflicts with the following key combination:\n", "The shortcut '%2' conflicts with the following key combinations:\n", hashSize, sequence.toString()); message += clashingKeys; QString title = i18ncp("%1 is the number of shortcuts with which there is a conflict", "Conflict with Registered Global Shortcut", "Conflict with Registered Global Shortcuts", hashSize); return KMessageBox::warningContinueCancel(q, message, title, KGuiItem(i18nc("@action:button", "Reassign"))) == KMessageBox::Continue; } #endif bool KKeySequenceWidgetPrivate::conflictWithGlobalShortcuts(const QKeySequence &keySequence) { #ifdef Q_OS_WIN // on windows F12 is reserved by the debugger at all times, so we can't use it for a global shortcut if (KKeySequenceWidget::GlobalShortcuts && keySequence.toString().contains(QLatin1String("F12"))) { QString title = i18n("Reserved Shortcut"); QString message = i18n( "The F12 key is reserved on Windows, so cannot be used for a global shortcut.\n" "Please choose another one."); KMessageBox::error(q, message, title); return false; } #endif #if HAVE_GLOBALACCEL if (!(checkAgainstShortcutTypes & KKeySequenceWidget::GlobalShortcuts)) { return false; } // Global shortcuts are on key+modifier shortcuts. They can clash with // each of the keys of a multi key shortcut. std::vector clashing; for (int i = 0; i < keySequence.count(); ++i) { QKeySequence keys(keySequence[i]); if (!KGlobalAccel::isGlobalShortcutAvailable(keySequence, componentName)) { clashing.push_back({keySequence, KGlobalAccel::globalShortcutsByKey(keys)}); } } if (clashing.empty()) { return false; } if (!promptStealGlobalShortcut(clashing, keySequence)) { return true; } // The user approved stealing the shortcut. We have to steal // it immediately because KAction::setGlobalShortcut() refuses // to set a global shortcut that is already used. There is no // error it just silently fails. So be nice because this is // most likely the first action that is done in the slot // listening to keySequenceChanged(). KGlobalAccel::stealShortcutSystemwide(keySequence); return false; #else Q_UNUSED(keySequence); return false; #endif } bool KKeySequenceWidgetPrivate::promptstealStandardShortcut(KStandardShortcut::StandardShortcut std, const QKeySequence &seq) { QString title = i18nc("@title:window", "Conflict with Standard Application Shortcut"); QString message = i18n( "The '%1' key combination is also used for the standard action " "\"%2\" that some applications use.\n" "Do you really want to use it as a global shortcut as well?", seq.toString(QKeySequence::NativeText), KStandardShortcut::label(std)); return KMessageBox::warningContinueCancel(q, message, title, KGuiItem(i18nc("@action:button", "Reassign"))) == KMessageBox::Continue; } bool KKeySequenceWidgetPrivate::conflictWithStandardShortcuts(const QKeySequence &seq) { if (!(checkAgainstShortcutTypes & KKeySequenceWidget::StandardShortcuts)) { return false; } KStandardShortcut::StandardShortcut ssc = KStandardShortcut::find(seq); if (ssc != KStandardShortcut::AccelNone && !promptstealStandardShortcut(ssc, seq)) { return true; } return false; } void KKeySequenceWidgetPrivate::startRecording() { keyButton->setDown(true); recorder->startRecording(); updateShortcutDisplay(); } void KKeySequenceWidgetPrivate::doneRecording() { keyButton->setDown(false); stealActions.clear(); keyButton->setText(keyButton->text().chopped(inputRecordingMarkupSuffix.size())); q->setKeySequence(recorder->currentKeySequence(), KKeySequenceWidget::Validate); updateShortcutDisplay(); } void KKeySequenceWidgetPrivate::updateShortcutDisplay() { QString s; QKeySequence sequence = recorder->isRecording() ? recorder->currentKeySequence() : keySequence; if (!sequence.isEmpty()) { s = sequence.toString(QKeySequence::NativeText); } else if (recorder->isRecording()) { s = i18nc("What the user inputs now will be taken as the new shortcut", "Input"); } else { s = i18nc("No shortcut defined", "None"); } if (recorder->isRecording()) { // make it clear that input is still going on s.append(inputRecordingMarkupSuffix); } s = QLatin1Char(' ') + s + QLatin1Char(' '); keyButton->setText(s); } KKeySequenceWidget::KKeySequenceWidget(QWidget *parent) : QWidget(parent) , d(new KKeySequenceWidgetPrivate(this)) { d->init(); setFocusProxy(d->keyButton); connect(d->keyButton, &QPushButton::clicked, this, &KKeySequenceWidget::captureKeySequence); connect(d->clearButton, &QToolButton::clicked, this, &KKeySequenceWidget::clearKeySequence); connect(d->recorder, &KKeySequenceRecorder::currentKeySequenceChanged, this, [this] { d->updateShortcutDisplay(); }); connect(d->recorder, &KKeySequenceRecorder::recordingChanged, this, [this] { if (!d->recorder->isRecording()) { d->doneRecording(); } }); } KKeySequenceWidget::~KKeySequenceWidget() { delete d; } KKeySequenceWidget::ShortcutTypes KKeySequenceWidget::checkForConflictsAgainst() const { return d->checkAgainstShortcutTypes; } void KKeySequenceWidget::setComponentName(const QString &componentName) { d->componentName = componentName; } bool KKeySequenceWidget::multiKeyShortcutsAllowed() const { return d->recorder->multiKeyShortcutsAllowed(); } void KKeySequenceWidget::setMultiKeyShortcutsAllowed(bool allowed) { d->recorder->setMultiKeyShortcutsAllowed(allowed); } void KKeySequenceWidget::setCheckForConflictsAgainst(ShortcutTypes types) { d->checkAgainstShortcutTypes = types; } void KKeySequenceWidget::setModifierlessAllowed(bool allow) { d->recorder->setModifierlessAllowed(allow); } bool KKeySequenceWidget::isKeySequenceAvailable(const QKeySequence &keySequence) const { if (keySequence.isEmpty()) { return true; } return !(d->conflictWithLocalShortcuts(keySequence) // || d->conflictWithGlobalShortcuts(keySequence) // || d->conflictWithStandardShortcuts(keySequence)); } bool KKeySequenceWidget::isModifierlessAllowed() { return d->recorder->modifierlessAllowed(); } bool KKeySequenceWidget::modifierOnlyAllowed() const { return d->recorder->modifierOnlyAllowed(); } void KKeySequenceWidget::setModifierOnlyAllowed(bool allow) { d->recorder->setModifierOnlyAllowed(allow); } void KKeySequenceWidget::setClearButtonShown(bool show) { d->clearButton->setVisible(show); } void KKeySequenceWidget::setCheckActionCollections(const QList &actionCollections) { d->checkActionCollections = actionCollections; } // slot void KKeySequenceWidget::captureKeySequence() { d->recorder->setWindow(window()->windowHandle()); d->recorder->startRecording(); } QKeySequence KKeySequenceWidget::keySequence() const { return d->keySequence; } // slot void KKeySequenceWidget::setKeySequence(const QKeySequence &seq, Validation validate) { if (d->keySequence == seq) { return; } if (validate == Validate && !isKeySequenceAvailable(seq)) { return; } d->keySequence = seq; d->updateShortcutDisplay(); Q_EMIT keySequenceChanged(seq); } // slot void KKeySequenceWidget::clearKeySequence() { setKeySequence(QKeySequence()); } // slot void KKeySequenceWidget::applyStealShortcut() { QSet changedCollections; for (QAction *stealAction : std::as_const(d->stealActions)) { // Stealing a shortcut means setting it to an empty one. stealAction->setShortcuts(QList()); // The following code will find the action we are about to // steal from and save it's actioncollection. KActionCollection *parentCollection = nullptr; for (KActionCollection *collection : std::as_const(d->checkActionCollections)) { if (collection->actions().contains(stealAction)) { parentCollection = collection; break; } } // Remember the changed collection if (parentCollection) { changedCollections.insert(parentCollection); } } for (KActionCollection *col : std::as_const(changedCollections)) { col->writeSettings(); } d->stealActions.clear(); } bool KKeySequenceWidget::event(QEvent *ev) { constexpr char _highlight[] = "_kde_highlight_neutral"; if (ev->type() == QEvent::DynamicPropertyChange) { auto dpev = static_cast(ev); if (dpev->propertyName() == _highlight) { d->keyButton->setProperty(_highlight, property(_highlight)); return true; } } return QWidget::event(ev); } #include "moc_kkeysequencewidget.cpp"