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->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),
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,
185 conflictingShortcuts);
186
187 return KMessageBox::warningContinueCancel(q, message, title, 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>",
198 KMessageBox::error(q, 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(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(allActions)) {
239 if (shortcutsConflictWith(qaction->shortcuts(), keySequence)) {
240 // A conflict with a KAction. If that action is configurable
241 // ask the user what to do. If not reject this keySequence.
243 conflictingActions.append(qaction);
244 } else {
245 wontStealShortcut(qaction, 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(conflictingActions, keySequence)) {
257 stealActions = conflictingActions;
258 // Announce that the user agreed
259 for (QAction *stealAction : std::as_const(stealActions)) {
260 Q_EMIT q->stealShortcut(keySequence, 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(q, message, title, 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(keySequence, componentName)) {
322 clashing.push_back({keySequence, KGlobalAccel::globalShortcutsByKey(keys)});
323 }
324 }
325 if (clashing.empty()) {
326 return false;
327 }
328
329 if (!promptStealGlobalShortcut(clashing, 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().
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?",
355
356 return KMessageBox::warningContinueCancel(q, message, title, 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 }
365 if (ssc != KStandardShortcut::AccelNone && !promptstealStandardShortcut(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(inputRecordingMarkupSuffix.size()));
383 q->setKeySequence(recorder->currentKeySequence(), 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()) {
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(inputRecordingMarkupSuffix);
402 }
403
404 s = QLatin1Char(' ') + s + QLatin1Char(' ');
405 keyButton->setText(s);
406}
407
409 : QWidget(parent)
410 , d(new KKeySequenceWidgetPrivate(this))
411{
412 d->init();
413 setFocusProxy(d->keyButton);
416
417 connect(d->recorder, &KKeySequenceRecorder::currentKeySequenceChanged, this, [this] {
418 d->updateShortcutDisplay();
419 });
420 connect(d->recorder, &KKeySequenceRecorder::recordingChanged, this, [this] {
421 if (!d->recorder->isRecording()) {
422 d->doneRecording();
423 }
425 });
426}
427
432
433KKeySequenceWidget::ShortcutTypes KKeySequenceWidget::checkForConflictsAgainst() const
434{
435 return d->checkAgainstShortcutTypes;
436}
437
439{
440 d->componentName = componentName;
441}
442
444{
445 return d->recorder->isRecording();
446}
447
448bool KKeySequenceWidget::multiKeyShortcutsAllowed() const
449{
450 return d->recorder->multiKeyShortcutsAllowed();
451}
452
454{
455 d->recorder->setMultiKeyShortcutsAllowed(allowed);
456}
457
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
474{
475 if (keySequence.isEmpty()) {
476 return true;
477 }
478 return !(d->conflictWithLocalShortcuts(keySequence) //
479 || d->conflictWithGlobalShortcuts(keySequence) //
480 || d->conflictWithStandardShortcuts(keySequence));
481}
482
483#if KXMLGUI_BUILD_DEPRECATED_SINCE(6, 12)
485{
486 return d->recorder->patterns() & KKeySequenceRecorder::Key;
487}
488
490{
491 if (allow) {
493 } else {
495 }
496}
497
502
511#endif
512
514{
515 d->clearButton->setVisible(show);
516}
517
519{
520 d->checkActionCollections = actionCollections;
521}
522
523// slot
525{
526 d->recorder->setWindow(window()->windowHandle());
527 d->recorder->startRecording();
528}
529
531{
532 return d->keySequence;
533}
534
535// slot
537{
538 if (d->keySequence == seq) {
539 return;
540 }
541 if (validate == Validate && !isKeySequenceAvailable(seq)) {
542 return;
543 }
544 d->keySequence = seq;
545 d->updateShortcutDisplay();
547}
548
549// slot
554
555// slot
557{
558 QSet<KActionCollection *> changedCollections;
559
560 for (QAction *stealAction : std::as_const(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(d->checkActionCollections)) {
568 if (collection->actions().contains(stealAction)) {
569 parentCollection = collection;
570 break;
571 }
572 }
573
574 // Remember the changed collection
575 if (parentCollection) {
576 changedCollections.insert(parentCollection);
577 }
578 }
579
580 for (KActionCollection *col : std::as_const(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(_highlight, property(_highlight));
595 return true;
596 }
597 }
598
599 return QWidget::event(ev);
600}
601
602#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 Apr 11 2025 11:49:55 by doxygen 1.13.2 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.