KXmlGui

kkeysequencewidget.cpp
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(needle) != QKeySequence::NoMatch //
45 || needle.matches(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(0, 0, 0, 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->setModifierlessAllowed(false);
160 recorder->setMultiKeyShortcutsAllowed(true);
161
162 updateShortcutDisplay();
163}
164
165bool KKeySequenceWidgetPrivate::promptStealLocalShortcut(const QList<QAction *> &actions, const QKeySequence &seq)
166{
167 const int listSize = actions.size();
168
169 QString title = i18ncp("%1 is the number of conflicts", "Shortcut Conflict", "Shortcut Conflicts", listSize);
170
171 QString conflictingShortcuts;
172 for (const QAction *action : actions) {
173 conflictingShortcuts += i18n("Shortcut '%1' for action '%2'\n",
174 action->shortcut().toString(QKeySequence::NativeText),
176 }
177 QString message = i18ncp("%1 is the number of ambiguous shortcut clashes (hidden)",
178 "The \"%2\" shortcut is ambiguous with the following shortcut.\n"
179 "Do you want to assign an empty shortcut to this action?\n"
180 "%3",
181 "The \"%2\" shortcut is ambiguous with the following shortcuts.\n"
182 "Do you want to assign an empty shortcut to these actions?\n"
183 "%3",
184 listSize,
186 conflictingShortcuts);
187
188 return KMessageBox::warningContinueCancel(q, message, title, KGuiItem(i18nc("@action:button", "Reassign"))) == KMessageBox::Continue;
189}
190
191void KKeySequenceWidgetPrivate::wontStealShortcut(QAction *item, const QKeySequence &seq)
192{
193 QString title(i18nc("@title:window", "Shortcut conflict"));
194 QString msg(
195 i18n("<qt>The '%1' key combination is already used by the <b>%2</b> action.<br>"
196 "Please select a different one.</qt>",
199 KMessageBox::error(q, msg, title);
200}
201
202bool KKeySequenceWidgetPrivate::conflictWithLocalShortcuts(const QKeySequence &keySequence)
203{
204 if (!(checkAgainstShortcutTypes & KKeySequenceWidget::LocalShortcuts)) {
205 return false;
206 }
207
208 // Add all the actions from the checkActionCollections list to a single list to
209 // be able to process them in a single loop below.
210 // Note that this can't be done in setCheckActionCollections(), because we
211 // keep pointers to the action collections, and between the call to
212 // setCheckActionCollections() and this function some actions might already be
213 // removed from the collection again.
214 QList<QAction *> allActions;
215 for (KActionCollection *collection : std::as_const(checkActionCollections)) {
216 allActions += collection->actions();
217 }
218
219 // Because of multikey shortcuts we can have clashes with many shortcuts.
220 //
221 // Example 1:
222 //
223 // Application currently uses 'CTRL-X,a', 'CTRL-X,f' and 'CTRL-X,CTRL-F'
224 // and the user wants to use 'CTRL-X'. 'CTRL-X' will only trigger as
225 // 'activatedAmbiguously()' for obvious reasons.
226 //
227 // Example 2:
228 //
229 // Application currently uses 'CTRL-X'. User wants to use 'CTRL-X,CTRL-F'.
230 // This will shadow 'CTRL-X' for the same reason as above.
231 //
232 // Example 3:
233 //
234 // Some weird combination of Example 1 and 2 with three shortcuts using
235 // 1/2/3 key shortcuts. I think you can imagine.
236 QList<QAction *> conflictingActions;
237
238 // find conflicting shortcuts with existing actions
239 for (QAction *qaction : std::as_const(allActions)) {
240 if (shortcutsConflictWith(qaction->shortcuts(), keySequence)) {
241 // A conflict with a KAction. If that action is configurable
242 // ask the user what to do. If not reject this keySequence.
244 conflictingActions.append(qaction);
245 } else {
246 wontStealShortcut(qaction, keySequence);
247 return true;
248 }
249 }
250 }
251
252 if (conflictingActions.isEmpty()) {
253 // No conflicting shortcuts found.
254 return false;
255 }
256
257 if (promptStealLocalShortcut(conflictingActions, keySequence)) {
258 stealActions = conflictingActions;
259 // Announce that the user agreed
260 for (QAction *stealAction : std::as_const(stealActions)) {
261 Q_EMIT q->stealShortcut(keySequence, stealAction);
262 }
263 return false;
264 }
265 return true;
266}
267
268#if HAVE_GLOBALACCEL
269bool KKeySequenceWidgetPrivate::promptStealGlobalShortcut(const std::vector<KeyConflictInfo> &clashing, const QKeySequence &sequence)
270{
271 QString clashingKeys;
272 for (const auto &[key, shortcutInfo] : clashing) {
273 const QString seqAsString = key.toString();
274 for (const KGlobalShortcutInfo &info : shortcutInfo) {
275 clashingKeys += i18n("Shortcut '%1' in Application '%2' for action '%3'\n", //
276 seqAsString,
277 info.componentFriendlyName(),
278 info.friendlyName());
279 }
280 }
281 const int hashSize = clashing.size();
282
283 QString message = i18ncp("%1 is the number of conflicts (hidden), %2 is the key sequence of the shortcut that is problematic",
284 "The shortcut '%2' conflicts with the following key combination:\n",
285 "The shortcut '%2' conflicts with the following key combinations:\n",
286 hashSize,
287 sequence.toString());
288 message += clashingKeys;
289
290 QString title = i18ncp("%1 is the number of shortcuts with which there is a conflict",
291 "Conflict with Registered Global Shortcut",
292 "Conflict with Registered Global Shortcuts",
293 hashSize);
294
295 return KMessageBox::warningContinueCancel(q, message, title, KGuiItem(i18nc("@action:button", "Reassign"))) == KMessageBox::Continue;
296}
297#endif
298
299bool KKeySequenceWidgetPrivate::conflictWithGlobalShortcuts(const QKeySequence &keySequence)
300{
301#ifdef Q_OS_WIN
302 // on windows F12 is reserved by the debugger at all times, so we can't use it for a global shortcut
303 if (KKeySequenceWidget::GlobalShortcuts && keySequence.toString().contains(QLatin1String("F12"))) {
304 QString title = i18n("Reserved Shortcut");
305 QString message = i18n(
306 "The F12 key is reserved on Windows, so cannot be used for a global shortcut.\n"
307 "Please choose another one.");
308
309 KMessageBox::error(q, message, title);
310 return false;
311 }
312#endif
313#if HAVE_GLOBALACCEL
314 if (!(checkAgainstShortcutTypes & KKeySequenceWidget::GlobalShortcuts)) {
315 return false;
316 }
317 // Global shortcuts are on key+modifier shortcuts. They can clash with
318 // each of the keys of a multi key shortcut.
319 std::vector<KeyConflictInfo> clashing;
320 for (int i = 0; i < keySequence.count(); ++i) {
321 QKeySequence keys(keySequence[i]);
322 if (!KGlobalAccel::isGlobalShortcutAvailable(keySequence, componentName)) {
323 clashing.push_back({keySequence, KGlobalAccel::globalShortcutsByKey(keys)});
324 }
325 }
326 if (clashing.empty()) {
327 return false;
328 }
329
330 if (!promptStealGlobalShortcut(clashing, keySequence)) {
331 return true;
332 }
333 // The user approved stealing the shortcut. We have to steal
334 // it immediately because KAction::setGlobalShortcut() refuses
335 // to set a global shortcut that is already used. There is no
336 // error it just silently fails. So be nice because this is
337 // most likely the first action that is done in the slot
338 // listening to keySequenceChanged().
340 return false;
341#else
342 Q_UNUSED(keySequence);
343 return false;
344#endif
345}
346
347bool KKeySequenceWidgetPrivate::promptstealStandardShortcut(KStandardShortcut::StandardShortcut std, const QKeySequence &seq)
348{
349 QString title = i18nc("@title:window", "Conflict with Standard Application Shortcut");
350 QString message = i18n(
351 "The '%1' key combination is also used for the standard action "
352 "\"%2\" that some applications use.\n"
353 "Do you really want to use it as a global shortcut as well?",
356
357 return KMessageBox::warningContinueCancel(q, message, title, KGuiItem(i18nc("@action:button", "Reassign"))) == KMessageBox::Continue;
358}
359
360bool KKeySequenceWidgetPrivate::conflictWithStandardShortcuts(const QKeySequence &seq)
361{
362 if (!(checkAgainstShortcutTypes & KKeySequenceWidget::StandardShortcuts)) {
363 return false;
364 }
366 if (ssc != KStandardShortcut::AccelNone && !promptstealStandardShortcut(ssc, seq)) {
367 return true;
368 }
369 return false;
370}
371
372void KKeySequenceWidgetPrivate::startRecording()
373{
374 keyButton->setDown(true);
375 recorder->startRecording();
376 updateShortcutDisplay();
377}
378
379void KKeySequenceWidgetPrivate::doneRecording()
380{
381 keyButton->setDown(false);
382 stealActions.clear();
383 keyButton->setText(keyButton->text().chopped(inputRecordingMarkupSuffix.size()));
384 q->setKeySequence(recorder->currentKeySequence(), KKeySequenceWidget::Validate);
385 updateShortcutDisplay();
386}
387
388void KKeySequenceWidgetPrivate::updateShortcutDisplay()
389{
390 QString s;
391 QKeySequence sequence = recorder->isRecording() ? recorder->currentKeySequence() : keySequence;
392 if (!sequence.isEmpty()) {
394 } else if (recorder->isRecording()) {
395 s = i18nc("What the user inputs now will be taken as the new shortcut", "Input");
396 } else {
397 s = i18nc("No shortcut defined", "None");
398 }
399
400 if (recorder->isRecording()) {
401 // make it clear that input is still going on
402 s.append(inputRecordingMarkupSuffix);
403 }
404
405 s = QLatin1Char(' ') + s + QLatin1Char(' ');
406 keyButton->setText(s);
407}
408
410 : QWidget(parent)
411 , d(new KKeySequenceWidgetPrivate(this))
412{
413 d->init();
414 setFocusProxy(d->keyButton);
417
418 connect(d->recorder, &KKeySequenceRecorder::currentKeySequenceChanged, this, [this] {
419 d->updateShortcutDisplay();
420 });
421 connect(d->recorder, &KKeySequenceRecorder::recordingChanged, this, [this] {
422 if (!d->recorder->isRecording()) {
423 d->doneRecording();
424 }
426 });
427}
428
433
434KKeySequenceWidget::ShortcutTypes KKeySequenceWidget::checkForConflictsAgainst() const
435{
436 return d->checkAgainstShortcutTypes;
437}
438
440{
441 d->componentName = componentName;
442}
443
445{
446 return d->recorder->isRecording();
447}
448
449bool KKeySequenceWidget::multiKeyShortcutsAllowed() const
450{
451 return d->recorder->multiKeyShortcutsAllowed();
452}
453
455{
456 d->recorder->setMultiKeyShortcutsAllowed(allowed);
457}
458
460{
461 d->checkAgainstShortcutTypes = types;
462}
463
464void KKeySequenceWidget::setPatterns(KKeySequenceRecorder::Patterns patterns)
465{
466 d->recorder->setPatterns(patterns);
467}
468
469KKeySequenceRecorder::Patterns KKeySequenceWidget::patterns() const
470{
471 return d->recorder->patterns();
472}
473
475{
476 if (keySequence.isEmpty()) {
477 return true;
478 }
479 return !(d->conflictWithLocalShortcuts(keySequence) //
480 || d->conflictWithGlobalShortcuts(keySequence) //
481 || d->conflictWithStandardShortcuts(keySequence));
482}
483
484#if KXMLGUI_BUILD_DEPRECATED_SINCE(6, 12)
486{
487 return d->recorder->modifierlessAllowed();
488}
489
491{
492 d->recorder->setModifierlessAllowed(allow);
493}
494
496{
497 return d->recorder->modifierOnlyAllowed();
498}
499
501{
502 d->recorder->setModifierOnlyAllowed(allow);
503}
504#endif
505
507{
508 d->clearButton->setVisible(show);
509}
510
512{
513 d->checkActionCollections = actionCollections;
514}
515
516// slot
518{
519 d->recorder->setWindow(window()->windowHandle());
520 d->recorder->startRecording();
521}
522
524{
525 return d->keySequence;
526}
527
528// slot
530{
531 if (d->keySequence == seq) {
532 return;
533 }
534 if (validate == Validate && !isKeySequenceAvailable(seq)) {
535 return;
536 }
537 d->keySequence = seq;
538 d->updateShortcutDisplay();
540}
541
542// slot
547
548// slot
550{
551 QSet<KActionCollection *> changedCollections;
552
553 for (QAction *stealAction : std::as_const(d->stealActions)) {
554 // Stealing a shortcut means setting it to an empty one.
555 stealAction->setShortcuts(QList<QKeySequence>());
556
557 // The following code will find the action we are about to
558 // steal from and save it's actioncollection.
559 KActionCollection *parentCollection = nullptr;
560 for (KActionCollection *collection : std::as_const(d->checkActionCollections)) {
561 if (collection->actions().contains(stealAction)) {
562 parentCollection = collection;
563 break;
564 }
565 }
566
567 // Remember the changed collection
568 if (parentCollection) {
569 changedCollections.insert(parentCollection);
570 }
571 }
572
573 for (KActionCollection *col : std::as_const(changedCollections)) {
574 col->writeSettings();
575 }
576
577 d->stealActions.clear();
578}
579
580bool KKeySequenceWidget::event(QEvent *ev)
581{
582 constexpr char _highlight[] = "_kde_highlight_neutral";
583
584 if (ev->type() == QEvent::DynamicPropertyChange) {
585 auto dpev = static_cast<QDynamicPropertyChangeEvent *>(ev);
586 if (dpev->propertyName() == _highlight) {
587 d->keyButton->setProperty(_highlight, property(_highlight));
588 return true;
589 }
590 }
591
592 return QWidget::event(ev);
593}
594
595#include "moc_kkeysequencewidget.cpp"
A container for a set of QAction objects.
static bool isShortcutsConfigurable(QAction *action)
Returns true if the given action's shortcuts may be configured by the user.
static QList< KGlobalShortcutInfo > globalShortcutsByKey(const QKeySequence &seq, MatchType type=Equal)
static void stealShortcutSystemwide(const QKeySequence &seq)
static bool isGlobalShortcutAvailable(const QKeySequence &seq, const QString &component=QString())
A widget to input a QKeySequence.
~KKeySequenceWidget() override
Destructs the widget.
void setModifierlessAllowed(bool allow)
This only applies to user input, not to setKeySequence().
QFlags< ShortcutType > ShortcutTypes
Stores a combination of ShortcutType values.
@ GlobalShortcuts
Check against global shortcuts.
@ StandardShortcuts
Check against standard shortcuts.
@ LocalShortcuts
Check with local shortcuts.
void clearKeySequence()
Clear the key sequence.
bool isKeySequenceAvailable(const QKeySequence &seq) const
Checks whether the key sequence seq is available to grab.
void setModifierOnlyAllowed(bool allow)
Whether to allow modifier-only key sequences.
void setClearButtonShown(bool show)
Set whether a small button to set an empty key sequence should be displayed next to the main input wi...
void setMultiKeyShortcutsAllowed(bool)
Allow multikey shortcuts?
void keySequenceChanged(const QKeySequence &seq)
This signal is emitted when the current key sequence has changed, be it by user input or programmatic...
void recordingChanged()
This signal is emitted when the user begins or finishes recording a key sequence.
void setComponentName(const QString &componentName)
If the component using this widget supports shortcuts contexts, it has to set its component name so w...
void captureKeySequence()
Capture a shortcut from the keyboard.
KKeySequenceWidget(QWidget *parent=nullptr)
Constructor.
void applyStealShortcut()
Actually remove the shortcut that the user wanted to steal, from the action that was using it.
void setKeySequence(const QKeySequence &seq, Validation val=NoValidate)
Set the key sequence.
KKeySequenceRecorder::Patterns patterns
Specifies the accepted shortcut formats.
void setCheckActionCollections(const QList< KActionCollection * > &actionCollections)
Set a list of action collections to check against for conflictuous shortcut.
void setPatterns(KKeySequenceRecorder::Patterns patterns)
Sets the accepted shortcut patterns to patterns.
Validation
An enum about validation when setting a key sequence.
@ Validate
Validate key sequence.
bool isRecording() const
Returns true if a key sequence is currently being recorded; otherwise returns false.
void setCheckForConflictsAgainst(ShortcutTypes types)
Configure if the widget should check for conflicts with existing shortcuts.
static QString removeAcceleratorMarker(const QString &label)
QString i18nc(const char *context, const char *text, const TYPE &arg...)
QString i18n(const char *text, const TYPE &arg...)
QString i18ncp(const char *context, const char *singular, const char *plural, const TYPE &arg...)
ButtonCode warningContinueCancel(QWidget *parent, const QString &text, const QString &title=QString(), const KGuiItem &buttonContinue=KStandardGuiItem::cont(), const KGuiItem &buttonCancel=KStandardGuiItem::cancel(), const QString &dontAskAgainName=QString(), Options options=Notify)
void error(QWidget *parent, const QString &text, const QString &title, const KGuiItem &buttonOk, Options options=Notify)
const QList< QKeySequence > & find()
QString label(StandardShortcut id)
void clicked(bool checked)
DynamicPropertyChange
Type type() const const
QIcon fromTheme(const QString &name)
bool isEmpty() const const
SequenceMatch matches(const QKeySequence &seq) const const
QString toString(SequenceFormat format) const const
void append(QList< T > &&value)
bool isEmpty() const const
qsizetype size() const const
Q_EMITQ_EMIT
QMetaObject::Connection connect(const QObject *sender, PointerToMemberFunction signal, Functor functor)
QObject * parent() const const
QVariant property(const char *name) const const
bool setProperty(const char *name, QVariant &&value)
void clear()
iterator insert(const T &value)
QString & append(QChar ch)
qsizetype size() const const
StrongFocus
QWidget(QWidget *parent, Qt::WindowFlags f)
virtual bool event(QEvent *event) override
void setFocusProxy(QWidget *w)
void show()
QWidget * window() const const
QWindow * windowHandle() const const
This file is part of the KDE documentation.
Documentation copyright © 1996-2025 The KDE developers.
Generated on Fri Feb 21 2025 11:48:21 by doxygen 1.13.2 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.