1/*
2 This file is part of the KDE libraries
3 SPDX-FileCopyrightText: 1998 Mark Donohoe <donohoe@kde.org>
4 SPDX-FileCopyrightText: 2001 Ellis Whitehead <ellis@kde.org>
5 SPDX-FileCopyrightText: 2007 Andreas Hartmetz <ahartmetz@gmail.com>
6 SPDX-FileCopyrightText: 2020 David Redondo <kde@david-redondo.de>
7
8 SPDX-License-Identifier: LGPL-2.0-or-later
9*/
10
11#include "config-xmlgui.h"
12
13#include "kkeysequencewidget.h"
14
15#include "debug.h"
16#include "kactioncollection.h"
17
18#include <QAction>
19#include <QApplication>
20#include <QHBoxLayout>
21#include <QHash>
22#include <QToolButton>
23
24#include <KKeySequenceRecorder>
25#include <KLocalizedString>
26#include <KMessageBox>
27#if HAVE_GLOBALACCEL
28#include <KGlobalAccel>
29#endif
30
31static bool shortcutsConflictWith(const QList<QKeySequence> &shortcuts, const QKeySequence &needle)
32{
33 if (needle.isEmpty()) {
34 return false;
35 }
36
37 for (const QKeySequence &sequence : shortcuts) {
38 if (sequence.isEmpty()) {
39 continue;
40 }
41
42 if (sequence.matches(seq: needle) != QKeySequence::NoMatch //
43 || needle.matches(seq: sequence) != QKeySequence::NoMatch) {
44 return true;
45 }
46 }
47
48 return false;
49}
50
51class KKeySequenceWidgetPrivate
52{
53public:
54 KKeySequenceWidgetPrivate(KKeySequenceWidget *qq);
55
56 void init();
57
58 void updateShortcutDisplay();
59 void startRecording();
60
61 // Conflicts the key sequence @p seq with a current standard shortcut?
62 bool conflictWithStandardShortcuts(const QKeySequence &seq);
63 // Conflicts the key sequence @p seq with a current local shortcut?
64 bool conflictWithLocalShortcuts(const QKeySequence &seq);
65 // Conflicts the key sequence @p seq with a current global shortcut?
66 bool conflictWithGlobalShortcuts(const QKeySequence &seq);
67
68 bool promptStealLocalShortcut(const QList<QAction *> &actions, const QKeySequence &seq);
69 bool promptstealStandardShortcut(KStandardShortcut::StandardShortcut std, const QKeySequence &seq);
70
71#if HAVE_GLOBALACCEL
72 struct KeyConflictInfo {
73 QKeySequence key;
74 QList<KGlobalShortcutInfo> shortcutInfo;
75 };
76 bool promptStealGlobalShortcut(const std::vector<KeyConflictInfo> &shortcuts, const QKeySequence &sequence);
77#endif
78 void wontStealShortcut(QAction *item, const QKeySequence &seq);
79
80 bool checkAgainstStandardShortcuts() const
81 {
82 return checkAgainstShortcutTypes & KKeySequenceWidget::StandardShortcuts;
83 }
84
85 bool checkAgainstGlobalShortcuts() const
86 {
87 return checkAgainstShortcutTypes & KKeySequenceWidget::GlobalShortcuts;
88 }
89
90 bool checkAgainstLocalShortcuts() const
91 {
92 return checkAgainstShortcutTypes & KKeySequenceWidget::LocalShortcuts;
93 }
94
95 // private slot
96 void doneRecording();
97
98 // members
99 KKeySequenceWidget *const q;
100 KKeySequenceRecorder *recorder;
101 QHBoxLayout *layout;
102 QPushButton *keyButton;
103 QToolButton *clearButton;
104
105 QKeySequence keySequence;
106 QKeySequence oldKeySequence;
107 QString componentName;
108
109 //! Check the key sequence against KStandardShortcut::find()
110 KKeySequenceWidget::ShortcutTypes checkAgainstShortcutTypes;
111
112 /**
113 * The list of action collections to check against for conflict shortcut
114 */
115 QList<KActionCollection *> checkActionCollections;
116
117 /**
118 * The action to steal the shortcut from.
119 */
120 QList<QAction *> stealActions;
121};
122
123KKeySequenceWidgetPrivate::KKeySequenceWidgetPrivate(KKeySequenceWidget *qq)
124 : q(qq)
125 , layout(nullptr)
126 , keyButton(nullptr)
127 , clearButton(nullptr)
128 , componentName()
129 , checkAgainstShortcutTypes(KKeySequenceWidget::LocalShortcuts | KKeySequenceWidget::GlobalShortcuts)
130 , stealActions()
131{
132}
133
134void KKeySequenceWidgetPrivate::init()
135{
136 layout = new QHBoxLayout(q);
137 layout->setContentsMargins(left: 0, top: 0, right: 0, bottom: 0);
138
139 keyButton = new QPushButton(q);
140 keyButton->setFocusPolicy(Qt::StrongFocus);
141 keyButton->setIcon(QIcon::fromTheme(QStringLiteral("configure")));
142 keyButton->setToolTip(
143 i18nc("@info:tooltip",
144 "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."));
145 layout->addWidget(keyButton);
146
147 clearButton = new QToolButton(q);
148 layout->addWidget(clearButton);
149
150 if (qApp->isLeftToRight()) {
151 clearButton->setIcon(QIcon::fromTheme(QStringLiteral("edit-clear-locationbar-rtl")));
152 } else {
153 clearButton->setIcon(QIcon::fromTheme(QStringLiteral("edit-clear-locationbar-ltr")));
154 }
155
156 recorder = new KKeySequenceRecorder(q->window()->windowHandle(), q);
157 recorder->setModifierlessAllowed(false);
158 recorder->setMultiKeyShortcutsAllowed(true);
159
160 updateShortcutDisplay();
161}
162
163bool KKeySequenceWidgetPrivate::promptStealLocalShortcut(const QList<QAction *> &actions, const QKeySequence &seq)
164{
165 const int listSize = actions.size();
166
167 QString title = i18ncp("%1 is the number of conflicts", "Shortcut Conflict", "Shortcut Conflicts", listSize);
168
169 QString conflictingShortcuts;
170 for (const QAction *action : actions) {
171 conflictingShortcuts += i18n("Shortcut '%1' for action '%2'\n",
172 action->shortcut().toString(QKeySequence::NativeText),
173 KLocalizedString::removeAcceleratorMarker(action->text()));
174 }
175 QString message = i18ncp("%1 is the number of ambiguous shortcut clashes (hidden)",
176 "The \"%2\" shortcut is ambiguous with the following shortcut.\n"
177 "Do you want to assign an empty shortcut to this action?\n"
178 "%3",
179 "The \"%2\" shortcut is ambiguous with the following shortcuts.\n"
180 "Do you want to assign an empty shortcut to these actions?\n"
181 "%3",
182 listSize,
183 seq.toString(QKeySequence::NativeText),
184 conflictingShortcuts);
185
186 return KMessageBox::warningContinueCancel(parent: q, text: message, title, buttonContinue: KGuiItem(i18nc("@action:button", "Reassign"))) == KMessageBox::Continue;
187}
188
189void KKeySequenceWidgetPrivate::wontStealShortcut(QAction *item, const QKeySequence &seq)
190{
191 QString title(i18nc("@title:window", "Shortcut conflict"));
192 QString msg(
193 i18n("<qt>The '%1' key combination is already used by the <b>%2</b> action.<br>"
194 "Please select a different one.</qt>",
195 seq.toString(QKeySequence::NativeText),
196 KLocalizedString::removeAcceleratorMarker(item->text())));
197 KMessageBox::error(parent: q, text: msg, title);
198}
199
200bool KKeySequenceWidgetPrivate::conflictWithLocalShortcuts(const QKeySequence &keySequence)
201{
202 if (!(checkAgainstShortcutTypes & KKeySequenceWidget::LocalShortcuts)) {
203 return false;
204 }
205
206 // Add all the actions from the checkActionCollections list to a single list to
207 // be able to process them in a single loop below.
208 // Note that this can't be done in setCheckActionCollections(), because we
209 // keep pointers to the action collections, and between the call to
210 // setCheckActionCollections() and this function some actions might already be
211 // removed from the collection again.
212 QList<QAction *> allActions;
213 for (KActionCollection *collection : std::as_const(t&: checkActionCollections)) {
214 allActions += collection->actions();
215 }
216
217 // Because of multikey shortcuts we can have clashes with many shortcuts.
218 //
219 // Example 1:
220 //
221 // Application currently uses 'CTRL-X,a', 'CTRL-X,f' and 'CTRL-X,CTRL-F'
222 // and the user wants to use 'CTRL-X'. 'CTRL-X' will only trigger as
223 // 'activatedAmbiguously()' for obvious reasons.
224 //
225 // Example 2:
226 //
227 // Application currently uses 'CTRL-X'. User wants to use 'CTRL-X,CTRL-F'.
228 // This will shadow 'CTRL-X' for the same reason as above.
229 //
230 // Example 3:
231 //
232 // Some weird combination of Example 1 and 2 with three shortcuts using
233 // 1/2/3 key shortcuts. I think you can imagine.
234 QList<QAction *> conflictingActions;
235
236 // find conflicting shortcuts with existing actions
237 for (QAction *qaction : std::as_const(t&: allActions)) {
238 if (shortcutsConflictWith(shortcuts: qaction->shortcuts(), needle: keySequence)) {
239 // A conflict with a KAction. If that action is configurable
240 // ask the user what to do. If not reject this keySequence.
241 if (KActionCollection::isShortcutsConfigurable(action: qaction)) {
242 conflictingActions.append(t: qaction);
243 } else {
244 wontStealShortcut(item: qaction, seq: keySequence);
245 return true;
246 }
247 }
248 }
249
250 if (conflictingActions.isEmpty()) {
251 // No conflicting shortcuts found.
252 return false;
253 }
254
255 if (promptStealLocalShortcut(actions: conflictingActions, seq: keySequence)) {
256 stealActions = conflictingActions;
257 // Announce that the user agreed
258 for (QAction *stealAction : std::as_const(t&: stealActions)) {
259 Q_EMIT q->stealShortcut(seq: keySequence, action: stealAction);
260 }
261 return false;
262 }
263 return true;
264}
265
266#if HAVE_GLOBALACCEL
267bool KKeySequenceWidgetPrivate::promptStealGlobalShortcut(const std::vector<KeyConflictInfo> &clashing, const QKeySequence &sequence)
268{
269 QString clashingKeys;
270 for (const auto &[key, shortcutInfo] : clashing) {
271 const QString seqAsString = key.toString();
272 for (const KGlobalShortcutInfo &info : shortcutInfo) {
273 clashingKeys += i18n("Shortcut '%1' in Application '%2' for action '%3'\n", //
274 seqAsString,
275 info.componentFriendlyName(),
276 info.friendlyName());
277 }
278 }
279 const int hashSize = clashing.size();
280
281 QString message = i18ncp("%1 is the number of conflicts (hidden), %2 is the key sequence of the shortcut that is problematic",
282 "The shortcut '%2' conflicts with the following key combination:\n",
283 "The shortcut '%2' conflicts with the following key combinations:\n",
284 hashSize,
285 sequence.toString());
286 message += clashingKeys;
287
288 QString title = i18ncp("%1 is the number of shortcuts with which there is a conflict",
289 "Conflict with Registered Global Shortcut",
290 "Conflict with Registered Global Shortcuts",
291 hashSize);
292
293 return KMessageBox::warningContinueCancel(parent: q, text: message, title, buttonContinue: KGuiItem(i18nc("@action:button", "Reassign"))) == KMessageBox::Continue;
294}
295#endif
296
297bool KKeySequenceWidgetPrivate::conflictWithGlobalShortcuts(const QKeySequence &keySequence)
298{
299#ifdef Q_OS_WIN
300 // on windows F12 is reserved by the debugger at all times, so we can't use it for a global shortcut
301 if (KKeySequenceWidget::GlobalShortcuts && keySequence.toString().contains(QLatin1String("F12"))) {
302 QString title = i18n("Reserved Shortcut");
303 QString message = i18n(
304 "The F12 key is reserved on Windows, so cannot be used for a global shortcut.\n"
305 "Please choose another one.");
306
307 KMessageBox::error(q, message, title);
308 return false;
309 }
310#endif
311#if HAVE_GLOBALACCEL
312 if (!(checkAgainstShortcutTypes & KKeySequenceWidget::GlobalShortcuts)) {
313 return false;
314 }
315 // Global shortcuts are on key+modifier shortcuts. They can clash with
316 // each of the keys of a multi key shortcut.
317 std::vector<KeyConflictInfo> clashing;
318 for (int i = 0; i < keySequence.count(); ++i) {
319 QKeySequence keys(keySequence[i]);
320 if (!KGlobalAccel::isGlobalShortcutAvailable(seq: keySequence, component: componentName)) {
321 clashing.push_back(x: {.key: keySequence, .shortcutInfo: KGlobalAccel::globalShortcutsByKey(seq: keys)});
322 }
323 }
324 if (clashing.empty()) {
325 return false;
326 }
327
328 if (!promptStealGlobalShortcut(clashing, sequence: keySequence)) {
329 return true;
330 }
331 // The user approved stealing the shortcut. We have to steal
332 // it immediately because KAction::setGlobalShortcut() refuses
333 // to set a global shortcut that is already used. There is no
334 // error it just silently fails. So be nice because this is
335 // most likely the first action that is done in the slot
336 // listening to keySequenceChanged().
337 KGlobalAccel::stealShortcutSystemwide(seq: keySequence);
338 return false;
339#else
340 Q_UNUSED(keySequence);
341 return false;
342#endif
343}
344
345bool KKeySequenceWidgetPrivate::promptstealStandardShortcut(KStandardShortcut::StandardShortcut std, const QKeySequence &seq)
346{
347 QString title = i18nc("@title:window", "Conflict with Standard Application Shortcut");
348 QString message = i18n(
349 "The '%1' key combination is also used for the standard action "
350 "\"%2\" that some applications use.\n"
351 "Do you really want to use it as a global shortcut as well?",
352 seq.toString(QKeySequence::NativeText),
353 KStandardShortcut::label(std));
354
355 return KMessageBox::warningContinueCancel(parent: q, text: message, title, buttonContinue: KGuiItem(i18nc("@action:button", "Reassign"))) == KMessageBox::Continue;
356}
357
358bool KKeySequenceWidgetPrivate::conflictWithStandardShortcuts(const QKeySequence &seq)
359{
360 if (!(checkAgainstShortcutTypes & KKeySequenceWidget::StandardShortcuts)) {
361 return false;
362 }
363 KStandardShortcut::StandardShortcut ssc = KStandardShortcut::find(keySeq: seq);
364 if (ssc != KStandardShortcut::AccelNone && !promptstealStandardShortcut(std: ssc, seq)) {
365 return true;
366 }
367 return false;
368}
369
370void KKeySequenceWidgetPrivate::startRecording()
371{
372 keyButton->setDown(true);
373 recorder->startRecording();
374 updateShortcutDisplay();
375}
376
377void KKeySequenceWidgetPrivate::doneRecording()
378{
379 keyButton->setDown(false);
380 stealActions.clear();
381 keyButton->setText(keyButton->text().chopped(n: strlen(s: " ...")));
382 q->setKeySequence(seq: recorder->currentKeySequence(), val: KKeySequenceWidget::Validate);
383 updateShortcutDisplay();
384}
385
386void KKeySequenceWidgetPrivate::updateShortcutDisplay()
387{
388 QString s;
389 QKeySequence sequence = recorder->isRecording() ? recorder->currentKeySequence() : keySequence;
390 if (!sequence.isEmpty()) {
391 s = sequence.toString(format: QKeySequence::NativeText);
392 } else if (recorder->isRecording()) {
393 s = i18nc("What the user inputs now will be taken as the new shortcut", "Input");
394 } else {
395 s = i18nc("No shortcut defined", "None");
396 }
397
398 if (recorder->isRecording()) {
399 // make it clear that input is still going on
400 s.append(s: QLatin1String(" ..."));
401 }
402
403 s = QLatin1Char(' ') + s + QLatin1Char(' ');
404 keyButton->setText(s);
405}
406
407KKeySequenceWidget::KKeySequenceWidget(QWidget *parent)
408 : QWidget(parent)
409 , d(new KKeySequenceWidgetPrivate(this))
410{
411 d->init();
412 setFocusProxy(d->keyButton);
413 connect(sender: d->keyButton, signal: &QPushButton::clicked, context: this, slot: &KKeySequenceWidget::captureKeySequence);
414 connect(sender: d->clearButton, signal: &QToolButton::clicked, context: this, slot: &KKeySequenceWidget::clearKeySequence);
415
416 connect(sender: d->recorder, signal: &KKeySequenceRecorder::currentKeySequenceChanged, context: this, slot: [this] {
417 d->updateShortcutDisplay();
418 });
419 connect(sender: d->recorder, signal: &KKeySequenceRecorder::recordingChanged, context: this, slot: [this] {
420 if (!d->recorder->isRecording()) {
421 d->doneRecording();
422 }
423 });
424}
425
426KKeySequenceWidget::~KKeySequenceWidget()
427{
428 delete d;
429}
430
431KKeySequenceWidget::ShortcutTypes KKeySequenceWidget::checkForConflictsAgainst() const
432{
433 return d->checkAgainstShortcutTypes;
434}
435
436void KKeySequenceWidget::setComponentName(const QString &componentName)
437{
438 d->componentName = componentName;
439}
440
441bool KKeySequenceWidget::multiKeyShortcutsAllowed() const
442{
443 return d->recorder->multiKeyShortcutsAllowed();
444}
445
446void KKeySequenceWidget::setMultiKeyShortcutsAllowed(bool allowed)
447{
448 d->recorder->setMultiKeyShortcutsAllowed(allowed);
449}
450
451void KKeySequenceWidget::setCheckForConflictsAgainst(ShortcutTypes types)
452{
453 d->checkAgainstShortcutTypes = types;
454}
455
456void KKeySequenceWidget::setModifierlessAllowed(bool allow)
457{
458 d->recorder->setModifierlessAllowed(allow);
459}
460
461bool KKeySequenceWidget::isKeySequenceAvailable(const QKeySequence &keySequence) const
462{
463 if (keySequence.isEmpty()) {
464 return true;
465 }
466 return !(d->conflictWithLocalShortcuts(keySequence) //
467 || d->conflictWithGlobalShortcuts(keySequence) //
468 || d->conflictWithStandardShortcuts(seq: keySequence));
469}
470
471bool KKeySequenceWidget::isModifierlessAllowed()
472{
473 return d->recorder->modifierlessAllowed();
474}
475
476bool KKeySequenceWidget::modifierOnlyAllowed() const
477{
478 return d->recorder->modifierOnlyAllowed();
479}
480
481void KKeySequenceWidget::setModifierOnlyAllowed(bool allow)
482{
483 d->recorder->setModifierOnlyAllowed(allow);
484}
485
486void KKeySequenceWidget::setClearButtonShown(bool show)
487{
488 d->clearButton->setVisible(show);
489}
490
491void KKeySequenceWidget::setCheckActionCollections(const QList<KActionCollection *> &actionCollections)
492{
493 d->checkActionCollections = actionCollections;
494}
495
496// slot
497void KKeySequenceWidget::captureKeySequence()
498{
499 d->recorder->setWindow(window()->windowHandle());
500 d->recorder->startRecording();
501}
502
503QKeySequence KKeySequenceWidget::keySequence() const
504{
505 return d->keySequence;
506}
507
508// slot
509void KKeySequenceWidget::setKeySequence(const QKeySequence &seq, Validation validate)
510{
511 if (d->keySequence == seq) {
512 return;
513 }
514 if (validate == Validate && !isKeySequenceAvailable(keySequence: seq)) {
515 return;
516 }
517 d->keySequence = seq;
518 d->updateShortcutDisplay();
519 Q_EMIT keySequenceChanged(seq);
520}
521
522// slot
523void KKeySequenceWidget::clearKeySequence()
524{
525 setKeySequence(seq: QKeySequence());
526}
527
528// slot
529void KKeySequenceWidget::applyStealShortcut()
530{
531 QSet<KActionCollection *> changedCollections;
532
533 for (QAction *stealAction : std::as_const(t&: d->stealActions)) {
534 // Stealing a shortcut means setting it to an empty one.
535 stealAction->setShortcuts(QList<QKeySequence>());
536
537 // The following code will find the action we are about to
538 // steal from and save it's actioncollection.
539 KActionCollection *parentCollection = nullptr;
540 for (KActionCollection *collection : std::as_const(t&: d->checkActionCollections)) {
541 if (collection->actions().contains(t: stealAction)) {
542 parentCollection = collection;
543 break;
544 }
545 }
546
547 // Remember the changed collection
548 if (parentCollection) {
549 changedCollections.insert(value: parentCollection);
550 }
551 }
552
553 for (KActionCollection *col : std::as_const(t&: changedCollections)) {
554 col->writeSettings();
555 }
556
557 d->stealActions.clear();
558}
559
560bool KKeySequenceWidget::event(QEvent *ev)
561{
562 constexpr char _highlight[] = "_kde_highlight_neutral";
563
564 if (ev->type() == QEvent::DynamicPropertyChange) {
565 auto dpev = static_cast<QDynamicPropertyChangeEvent *>(ev);
566 if (dpev->propertyName() == _highlight) {
567 d->keyButton->setProperty(name: _highlight, value: property(name: _highlight));
568 return true;
569 }
570 }
571
572 return QWidget::event(event: ev);
573}
574
575#include "moc_kkeysequencewidget.cpp"
576

source code of kxmlgui/src/kkeysequencewidget.cpp