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

source code of kxmlgui/src/kkeysequencewidget.cpp