File indexing completed on 2024-03-24 15:40:45

0001 /*
0002     This file is part of the KDE libraries
0003     SPDX-FileCopyrightText: 1998 Mark Donohoe <donohoe@kde.org>
0004     SPDX-FileCopyrightText: 2001 Ellis Whitehead <ellis@kde.org>
0005     SPDX-FileCopyrightText: 2007 Andreas Hartmetz <ahartmetz@gmail.com>
0006     SPDX-FileCopyrightText: 2020 David Redondo <kde@david-redondo.de>
0007 
0008     SPDX-License-Identifier: LGPL-2.0-or-later
0009 */
0010 
0011 #include "config-xmlgui.h"
0012 
0013 #include "kkeysequencewidget.h"
0014 
0015 #include "debug.h"
0016 #include "kactioncollection.h"
0017 
0018 #include <QAction>
0019 #include <QApplication>
0020 #include <QHBoxLayout>
0021 #include <QHash>
0022 #include <QToolButton>
0023 
0024 #include <KLocalizedString>
0025 #include <KMessageBox>
0026 #include <KeySequenceRecorder>
0027 #if HAVE_GLOBALACCEL
0028 #include <KGlobalAccel>
0029 #endif
0030 
0031 static bool shortcutsConflictWith(const QList<QKeySequence> &shortcuts, const QKeySequence &needle)
0032 {
0033     if (needle.isEmpty()) {
0034         return false;
0035     }
0036 
0037     for (const QKeySequence &sequence : shortcuts) {
0038         if (sequence.isEmpty()) {
0039             continue;
0040         }
0041 
0042         if (sequence.matches(needle) != QKeySequence::NoMatch //
0043             || needle.matches(sequence) != QKeySequence::NoMatch) {
0044             return true;
0045         }
0046     }
0047 
0048     return false;
0049 }
0050 
0051 class KKeySequenceWidgetPrivate
0052 {
0053 public:
0054     KKeySequenceWidgetPrivate(KKeySequenceWidget *qq);
0055 
0056     void init();
0057 
0058     void updateShortcutDisplay();
0059     void startRecording();
0060 
0061     // Conflicts the key sequence @p seq with a current standard shortcut?
0062     bool conflictWithStandardShortcuts(const QKeySequence &seq);
0063     // Conflicts the key sequence @p seq with a current local shortcut?
0064     bool conflictWithLocalShortcuts(const QKeySequence &seq);
0065     // Conflicts the key sequence @p seq with a current global shortcut?
0066     bool conflictWithGlobalShortcuts(const QKeySequence &seq);
0067 
0068     bool promptStealLocalShortcut(const QList<QAction *> &actions, const QKeySequence &seq);
0069     bool promptstealStandardShortcut(KStandardShortcut::StandardShortcut std, const QKeySequence &seq);
0070 
0071 #if HAVE_GLOBALACCEL
0072     struct KeyConflictInfo {
0073         QKeySequence key;
0074         QList<KGlobalShortcutInfo> shortcutInfo;
0075     };
0076     bool promptStealGlobalShortcut(const std::vector<KeyConflictInfo> &shortcuts, const QKeySequence &sequence);
0077 #endif
0078     void wontStealShortcut(QAction *item, const QKeySequence &seq);
0079 
0080     bool checkAgainstStandardShortcuts() const
0081     {
0082         return checkAgainstShortcutTypes & KKeySequenceWidget::StandardShortcuts;
0083     }
0084 
0085     bool checkAgainstGlobalShortcuts() const
0086     {
0087         return checkAgainstShortcutTypes & KKeySequenceWidget::GlobalShortcuts;
0088     }
0089 
0090     bool checkAgainstLocalShortcuts() const
0091     {
0092         return checkAgainstShortcutTypes & KKeySequenceWidget::LocalShortcuts;
0093     }
0094 
0095     // private slot
0096     void doneRecording();
0097 
0098     // members
0099     KKeySequenceWidget *const q;
0100     KeySequenceRecorder *recorder;
0101     QHBoxLayout *layout;
0102     QPushButton *keyButton;
0103     QToolButton *clearButton;
0104 
0105     QKeySequence keySequence;
0106     QKeySequence oldKeySequence;
0107     QString componentName;
0108 
0109     //! Check the key sequence against KStandardShortcut::find()
0110     KKeySequenceWidget::ShortcutTypes checkAgainstShortcutTypes;
0111 
0112     /**
0113      * The list of action to check against for conflict shortcut
0114      */
0115     QList<QAction *> checkList; // deprecated
0116 
0117     /**
0118      * The list of action collections to check against for conflict shortcut
0119      */
0120     QList<KActionCollection *> checkActionCollections;
0121 
0122     /**
0123      * The action to steal the shortcut from.
0124      */
0125     QList<QAction *> stealActions;
0126 };
0127 
0128 KKeySequenceWidgetPrivate::KKeySequenceWidgetPrivate(KKeySequenceWidget *qq)
0129     : q(qq)
0130     , layout(nullptr)
0131     , keyButton(nullptr)
0132     , clearButton(nullptr)
0133     , componentName()
0134     , checkAgainstShortcutTypes(KKeySequenceWidget::LocalShortcuts | KKeySequenceWidget::GlobalShortcuts)
0135     , stealActions()
0136 {
0137 }
0138 
0139 void KKeySequenceWidgetPrivate::init()
0140 {
0141     layout = new QHBoxLayout(q);
0142     layout->setContentsMargins(0, 0, 0, 0);
0143 
0144     keyButton = new QPushButton(q);
0145     keyButton->setFocusPolicy(Qt::StrongFocus);
0146     keyButton->setIcon(QIcon::fromTheme(QStringLiteral("configure")));
0147     keyButton->setToolTip(
0148         i18nc("@info:tooltip",
0149               "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."));
0150     layout->addWidget(keyButton);
0151 
0152     clearButton = new QToolButton(q);
0153     layout->addWidget(clearButton);
0154 
0155     if (qApp->isLeftToRight()) {
0156         clearButton->setIcon(QIcon::fromTheme(QStringLiteral("edit-clear-locationbar-rtl")));
0157     } else {
0158         clearButton->setIcon(QIcon::fromTheme(QStringLiteral("edit-clear-locationbar-ltr")));
0159     }
0160 
0161     recorder = new KeySequenceRecorder(q->window()->windowHandle(), q);
0162     recorder->setModifierlessAllowed(false);
0163     recorder->setMultiKeyShortcutsAllowed(true);
0164 
0165     updateShortcutDisplay();
0166 }
0167 
0168 bool KKeySequenceWidgetPrivate::promptStealLocalShortcut(const QList<QAction *> &actions, const QKeySequence &seq)
0169 {
0170     const int listSize = actions.size();
0171 
0172     QString title = i18ncp("%1 is the number of conflicts", "Shortcut Conflict", "Shortcut Conflicts", listSize);
0173 
0174     QString conflictingShortcuts;
0175     for (const QAction *action : actions) {
0176         conflictingShortcuts += i18n("Shortcut '%1' for action '%2'\n",
0177                                      action->shortcut().toString(QKeySequence::NativeText),
0178                                      KLocalizedString::removeAcceleratorMarker(action->text()));
0179     }
0180     QString message = i18ncp("%1 is the number of ambiguous shortcut clashes (hidden)",
0181                              "The \"%2\" shortcut is ambiguous with the following shortcut.\n"
0182                              "Do you want to assign an empty shortcut to this action?\n"
0183                              "%3",
0184                              "The \"%2\" shortcut is ambiguous with the following shortcuts.\n"
0185                              "Do you want to assign an empty shortcut to these actions?\n"
0186                              "%3",
0187                              listSize,
0188                              seq.toString(QKeySequence::NativeText),
0189                              conflictingShortcuts);
0190 
0191     return KMessageBox::warningContinueCancel(q, message, title, KGuiItem(i18nc("@action:button", "Reassign"))) == KMessageBox::Continue;
0192 }
0193 
0194 void KKeySequenceWidgetPrivate::wontStealShortcut(QAction *item, const QKeySequence &seq)
0195 {
0196     QString title(i18nc("@title:window", "Shortcut conflict"));
0197     QString msg(
0198         i18n("<qt>The '%1' key combination is already used by the <b>%2</b> action.<br>"
0199              "Please select a different one.</qt>",
0200              seq.toString(QKeySequence::NativeText),
0201              KLocalizedString::removeAcceleratorMarker(item->text())));
0202     KMessageBox::error(q, msg, title);
0203 }
0204 
0205 bool KKeySequenceWidgetPrivate::conflictWithLocalShortcuts(const QKeySequence &keySequence)
0206 {
0207     if (!(checkAgainstShortcutTypes & KKeySequenceWidget::LocalShortcuts)) {
0208         return false;
0209     }
0210 
0211     // We have actions both in the deprecated checkList and the
0212     // checkActionCollections list. Add all the actions to a single list to
0213     // be able to process them in a single loop below.
0214     // Note that this can't be done in setCheckActionCollections(), because we
0215     // keep pointers to the action collections, and between the call to
0216     // setCheckActionCollections() and this function some actions might already be
0217     // removed from the collection again.
0218     QList<QAction *> allActions;
0219     allActions += checkList;
0220     for (KActionCollection *collection : std::as_const(checkActionCollections)) {
0221         allActions += collection->actions();
0222     }
0223 
0224     // Because of multikey shortcuts we can have clashes with many shortcuts.
0225     //
0226     // Example 1:
0227     //
0228     // Application currently uses 'CTRL-X,a', 'CTRL-X,f' and 'CTRL-X,CTRL-F'
0229     // and the user wants to use 'CTRL-X'. 'CTRL-X' will only trigger as
0230     // 'activatedAmbiguously()' for obvious reasons.
0231     //
0232     // Example 2:
0233     //
0234     // Application currently uses 'CTRL-X'. User wants to use 'CTRL-X,CTRL-F'.
0235     // This will shadow 'CTRL-X' for the same reason as above.
0236     //
0237     // Example 3:
0238     //
0239     // Some weird combination of Example 1 and 2 with three shortcuts using
0240     // 1/2/3 key shortcuts. I think you can imagine.
0241     QList<QAction *> conflictingActions;
0242 
0243     // find conflicting shortcuts with existing actions
0244     for (QAction *qaction : std::as_const(allActions)) {
0245         if (shortcutsConflictWith(qaction->shortcuts(), keySequence)) {
0246             // A conflict with a KAction. If that action is configurable
0247             // ask the user what to do. If not reject this keySequence.
0248             if (checkActionCollections.first()->isShortcutsConfigurable(qaction)) {
0249                 conflictingActions.append(qaction);
0250             } else {
0251                 wontStealShortcut(qaction, keySequence);
0252                 return true;
0253             }
0254         }
0255     }
0256 
0257     if (conflictingActions.isEmpty()) {
0258         // No conflicting shortcuts found.
0259         return false;
0260     }
0261 
0262     if (promptStealLocalShortcut(conflictingActions, keySequence)) {
0263         stealActions = conflictingActions;
0264         // Announce that the user agreed
0265         for (QAction *stealAction : std::as_const(stealActions)) {
0266             Q_EMIT q->stealShortcut(keySequence, stealAction);
0267         }
0268         return false;
0269     }
0270     return true;
0271 }
0272 
0273 #if HAVE_GLOBALACCEL
0274 bool KKeySequenceWidgetPrivate::promptStealGlobalShortcut(const std::vector<KeyConflictInfo> &clashing, const QKeySequence &sequence)
0275 {
0276     QString clashingKeys;
0277     for (const auto &[key, shortcutInfo] : clashing) {
0278         const QString seqAsString = key.toString();
0279         for (const KGlobalShortcutInfo &info : shortcutInfo) {
0280             clashingKeys += i18n("Shortcut '%1' in Application '%2' for action '%3'\n", //
0281                                  seqAsString,
0282                                  info.componentFriendlyName(),
0283                                  info.friendlyName());
0284         }
0285     }
0286     const int hashSize = clashing.size();
0287 
0288     QString message = i18ncp("%1 is the number of conflicts (hidden), %2 is the key sequence of the shortcut that is problematic",
0289                              "The shortcut '%2' conflicts with the following key combination:\n",
0290                              "The shortcut '%2' conflicts with the following key combinations:\n",
0291                              hashSize,
0292                              sequence.toString());
0293     message += clashingKeys;
0294 
0295     QString title = i18ncp("%1 is the number of shortcuts with which there is a conflict",
0296                            "Conflict with Registered Global Shortcut",
0297                            "Conflict with Registered Global Shortcuts",
0298                            hashSize);
0299 
0300     return KMessageBox::warningContinueCancel(q, message, title, KGuiItem(i18nc("@action:button", "Reassign"))) == KMessageBox::Continue;
0301 }
0302 #endif
0303 
0304 bool KKeySequenceWidgetPrivate::conflictWithGlobalShortcuts(const QKeySequence &keySequence)
0305 {
0306 #ifdef Q_OS_WIN
0307     // on windows F12 is reserved by the debugger at all times, so we can't use it for a global shortcut
0308     if (KKeySequenceWidget::GlobalShortcuts && keySequence.toString().contains(QLatin1String("F12"))) {
0309         QString title = i18n("Reserved Shortcut");
0310         QString message = i18n(
0311             "The F12 key is reserved on Windows, so cannot be used for a global shortcut.\n"
0312             "Please choose another one.");
0313 
0314         KMessageBox::error(q, message, title);
0315         return false;
0316     }
0317 #endif
0318 #if HAVE_GLOBALACCEL
0319     if (!(checkAgainstShortcutTypes & KKeySequenceWidget::GlobalShortcuts)) {
0320         return false;
0321     }
0322     // Global shortcuts are on key+modifier shortcuts. They can clash with
0323     // each of the keys of a multi key shortcut.
0324     std::vector<KeyConflictInfo> clashing;
0325     for (int i = 0; i < keySequence.count(); ++i) {
0326         QKeySequence keys(keySequence[i]);
0327         if (!KGlobalAccel::isGlobalShortcutAvailable(keySequence, componentName)) {
0328             clashing.push_back({keySequence, KGlobalAccel::globalShortcutsByKey(keys)});
0329         }
0330     }
0331     if (clashing.empty()) {
0332         return false;
0333     }
0334 
0335     if (!promptStealGlobalShortcut(clashing, keySequence)) {
0336         return true;
0337     }
0338     // The user approved stealing the shortcut. We have to steal
0339     // it immediately because KAction::setGlobalShortcut() refuses
0340     // to set a global shortcut that is already used. There is no
0341     // error it just silently fails. So be nice because this is
0342     // most likely the first action that is done in the slot
0343     // listening to keySequenceChanged().
0344     KGlobalAccel::stealShortcutSystemwide(keySequence);
0345     return false;
0346 #else
0347     Q_UNUSED(keySequence);
0348     return false;
0349 #endif
0350 }
0351 
0352 bool KKeySequenceWidgetPrivate::promptstealStandardShortcut(KStandardShortcut::StandardShortcut std, const QKeySequence &seq)
0353 {
0354     QString title = i18nc("@title:window", "Conflict with Standard Application Shortcut");
0355     QString message = i18n(
0356         "The '%1' key combination is also used for the standard action "
0357         "\"%2\" that some applications use.\n"
0358         "Do you really want to use it as a global shortcut as well?",
0359         seq.toString(QKeySequence::NativeText),
0360         KStandardShortcut::label(std));
0361 
0362     return KMessageBox::warningContinueCancel(q, message, title, KGuiItem(i18nc("@action:button", "Reassign"))) == KMessageBox::Continue;
0363 }
0364 
0365 bool KKeySequenceWidgetPrivate::conflictWithStandardShortcuts(const QKeySequence &seq)
0366 {
0367     if (!(checkAgainstShortcutTypes & KKeySequenceWidget::StandardShortcuts)) {
0368         return false;
0369     }
0370     KStandardShortcut::StandardShortcut ssc = KStandardShortcut::find(seq);
0371     if (ssc != KStandardShortcut::AccelNone && !promptstealStandardShortcut(ssc, seq)) {
0372         return true;
0373     }
0374     return false;
0375 }
0376 
0377 void KKeySequenceWidgetPrivate::startRecording()
0378 {
0379     keyButton->setDown(true);
0380     recorder->startRecording();
0381     updateShortcutDisplay();
0382 }
0383 
0384 void KKeySequenceWidgetPrivate::doneRecording()
0385 {
0386     keyButton->setDown(false);
0387     stealActions.clear();
0388     keyButton->setText(keyButton->text().chopped(strlen(" ...")));
0389     q->setKeySequence(recorder->currentKeySequence(), KKeySequenceWidget::Validate);
0390     updateShortcutDisplay();
0391 }
0392 
0393 void KKeySequenceWidgetPrivate::updateShortcutDisplay()
0394 {
0395     QString s;
0396     QKeySequence sequence = recorder->isRecording() ? recorder->currentKeySequence() : keySequence;
0397     if (!sequence.isEmpty()) {
0398         s = sequence.toString(QKeySequence::NativeText);
0399     } else if (recorder->isRecording()) {
0400         s = i18nc("What the user inputs now will be taken as the new shortcut", "Input");
0401     } else {
0402         s = i18nc("No shortcut defined", "None");
0403     }
0404 
0405     if (recorder->isRecording()) {
0406         // make it clear that input is still going on
0407         s.append(QLatin1String(" ..."));
0408     }
0409 
0410     s = QLatin1Char(' ') + s + QLatin1Char(' ');
0411     keyButton->setText(s);
0412 }
0413 
0414 KKeySequenceWidget::KKeySequenceWidget(QWidget *parent)
0415     : QWidget(parent)
0416     , d(new KKeySequenceWidgetPrivate(this))
0417 {
0418     d->init();
0419     setFocusProxy(d->keyButton);
0420     connect(d->keyButton, &QPushButton::clicked, this, &KKeySequenceWidget::captureKeySequence);
0421     connect(d->clearButton, &QToolButton::clicked, this, &KKeySequenceWidget::clearKeySequence);
0422 
0423     connect(d->recorder, &KeySequenceRecorder::currentKeySequenceChanged, this, [this] {
0424         d->updateShortcutDisplay();
0425     });
0426     connect(d->recorder, &KeySequenceRecorder::recordingChanged, this, [this] {
0427         if (!d->recorder->isRecording()) {
0428             d->doneRecording();
0429         }
0430     });
0431 }
0432 
0433 KKeySequenceWidget::~KKeySequenceWidget()
0434 {
0435     delete d;
0436 }
0437 
0438 KKeySequenceWidget::ShortcutTypes KKeySequenceWidget::checkForConflictsAgainst() const
0439 {
0440     return d->checkAgainstShortcutTypes;
0441 }
0442 
0443 void KKeySequenceWidget::setComponentName(const QString &componentName)
0444 {
0445     d->componentName = componentName;
0446 }
0447 
0448 bool KKeySequenceWidget::multiKeyShortcutsAllowed() const
0449 {
0450     return d->recorder->multiKeyShortcutsAllowed();
0451 }
0452 
0453 void KKeySequenceWidget::setMultiKeyShortcutsAllowed(bool allowed)
0454 {
0455     d->recorder->setMultiKeyShortcutsAllowed(allowed);
0456 }
0457 
0458 void KKeySequenceWidget::setCheckForConflictsAgainst(ShortcutTypes types)
0459 {
0460     d->checkAgainstShortcutTypes = types;
0461 }
0462 
0463 void KKeySequenceWidget::setModifierlessAllowed(bool allow)
0464 {
0465     d->recorder->setModifierlessAllowed(allow);
0466 }
0467 
0468 bool KKeySequenceWidget::isKeySequenceAvailable(const QKeySequence &keySequence) const
0469 {
0470     if (keySequence.isEmpty()) {
0471         return true;
0472     }
0473     return !(d->conflictWithLocalShortcuts(keySequence) //
0474              || d->conflictWithGlobalShortcuts(keySequence) //
0475              || d->conflictWithStandardShortcuts(keySequence));
0476 }
0477 
0478 bool KKeySequenceWidget::isModifierlessAllowed()
0479 {
0480     return d->recorder->modifierlessAllowed();
0481 }
0482 
0483 void KKeySequenceWidget::setClearButtonShown(bool show)
0484 {
0485     d->clearButton->setVisible(show);
0486 }
0487 
0488 #if KXMLGUI_BUILD_DEPRECATED_SINCE(4, 1)
0489 void KKeySequenceWidget::setCheckActionList(const QList<QAction *> &checkList) // deprecated
0490 {
0491     d->checkList = checkList;
0492     Q_ASSERT(d->checkActionCollections.isEmpty()); // don't call this method if you call setCheckActionCollections!
0493 }
0494 #endif
0495 
0496 void KKeySequenceWidget::setCheckActionCollections(const QList<KActionCollection *> &actionCollections)
0497 {
0498     d->checkActionCollections = actionCollections;
0499 }
0500 
0501 // slot
0502 void KKeySequenceWidget::captureKeySequence()
0503 {
0504     d->recorder->setWindow(window()->windowHandle());
0505     d->recorder->startRecording();
0506 }
0507 
0508 QKeySequence KKeySequenceWidget::keySequence() const
0509 {
0510     return d->keySequence;
0511 }
0512 
0513 // slot
0514 void KKeySequenceWidget::setKeySequence(const QKeySequence &seq, Validation validate)
0515 {
0516     if (d->keySequence == seq) {
0517         return;
0518     }
0519     if (validate == Validate && !isKeySequenceAvailable(seq)) {
0520         return;
0521     }
0522     d->keySequence = seq;
0523     d->updateShortcutDisplay();
0524     Q_EMIT keySequenceChanged(seq);
0525 }
0526 
0527 // slot
0528 void KKeySequenceWidget::clearKeySequence()
0529 {
0530     setKeySequence(QKeySequence());
0531 }
0532 
0533 // slot
0534 void KKeySequenceWidget::applyStealShortcut()
0535 {
0536     QSet<KActionCollection *> changedCollections;
0537 
0538     for (QAction *stealAction : std::as_const(d->stealActions)) {
0539         // Stealing a shortcut means setting it to an empty one.
0540         stealAction->setShortcuts(QList<QKeySequence>());
0541 
0542         // The following code will find the action we are about to
0543         // steal from and save it's actioncollection.
0544         KActionCollection *parentCollection = nullptr;
0545         for (KActionCollection *collection : std::as_const(d->checkActionCollections)) {
0546             if (collection->actions().contains(stealAction)) {
0547                 parentCollection = collection;
0548                 break;
0549             }
0550         }
0551 
0552         // Remember the changed collection
0553         if (parentCollection) {
0554             changedCollections.insert(parentCollection);
0555         }
0556     }
0557 
0558     for (KActionCollection *col : std::as_const(changedCollections)) {
0559         col->writeSettings();
0560     }
0561 
0562     d->stealActions.clear();
0563 }
0564 
0565 #include "moc_kkeysequencewidget.cpp"