File indexing completed on 2024-04-14 03:57:09

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