File indexing completed on 2024-09-08 12:23:22
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"