File indexing completed on 2024-09-15 03:41:57
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"