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);
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()));
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 }
425 });
426}
427
432
433KKeySequenceWidget::ShortcutTypes KKeySequenceWidget::checkForConflictsAgainst() const
434{
435 return d->checkAgainstShortcutTypes;
436}
437
439{
440 d->componentName = componentName;
441}
442
443bool KKeySequenceWidget::multiKeyShortcutsAllowed() const
444{
445 return d->recorder->multiKeyShortcutsAllowed();
446}
447
449{
450 d->recorder->setMultiKeyShortcutsAllowed(allowed);
451}
452
454{
455 d->checkAgainstShortcutTypes = types;
456}
457
459{
460 d->recorder->setModifierlessAllowed(allow);
461}
462
464{
465 if (keySequence.isEmpty()) {
466 return true;
467 }
468 return !(d->conflictWithLocalShortcuts(keySequence) //
469 || d->conflictWithGlobalShortcuts(keySequence) //
470 || d->conflictWithStandardShortcuts(keySequence));
471}
472
474{
475 return d->recorder->modifierlessAllowed();
476}
477
479{
480 return d->recorder->modifierOnlyAllowed();
481}
482
484{
485 d->recorder->setModifierOnlyAllowed(allow);
486}
487
489{
490 d->clearButton->setVisible(show);
491}
492
494{
495 d->checkActionCollections = actionCollections;
496}
497
498// slot
500{
501 d->recorder->setWindow(window()->windowHandle());
502 d->recorder->startRecording();
503}
504
506{
507 return d->keySequence;
508}
509
510// slot
512{
513 if (d->keySequence == seq) {
514 return;
515 }
516 if (validate == Validate && !isKeySequenceAvailable(seq)) {
517 return;
518 }
519 d->keySequence = seq;
520 d->updateShortcutDisplay();
522}
523
524// slot
529
530// slot
532{
533 QSet<KActionCollection *> changedCollections;
534
535 for (QAction *stealAction : std::as_const(d->stealActions)) {
536 // Stealing a shortcut means setting it to an empty one.
537 stealAction->setShortcuts(QList<QKeySequence>());
538
539 // The following code will find the action we are about to
540 // steal from and save it's actioncollection.
541 KActionCollection *parentCollection = nullptr;
542 for (KActionCollection *collection : std::as_const(d->checkActionCollections)) {
543 if (collection->actions().contains(stealAction)) {
544 parentCollection = collection;
545 break;
546 }
547 }
548
549 // Remember the changed collection
550 if (parentCollection) {
551 changedCollections.insert(parentCollection);
552 }
553 }
554
555 for (KActionCollection *col : std::as_const(changedCollections)) {
556 col->writeSettings();
557 }
558
559 d->stealActions.clear();
560}
561
562bool KKeySequenceWidget::event(QEvent *ev)
563{
564 constexpr char _highlight[] = "_kde_highlight_neutral";
565
566 if (ev->type() == QEvent::DynamicPropertyChange) {
567 auto dpev = static_cast<QDynamicPropertyChangeEvent *>(ev);
568 if (dpev->propertyName() == _highlight) {
569 d->keyButton->setProperty(_highlight, property(_highlight));
570 return true;
571 }
572 }
573
574 return QWidget::event(ev);
575}
576
577#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())
Q_INVOKABLE void startRecording()
QKeySequence currentKeySequence
A widget to input a QKeySequence.
~KKeySequenceWidget() override
Destructs the widget.
void setModifierlessAllowed(bool allow)
This only applies to user input, not to setKeySequence().
@ 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 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.
void setCheckActionCollections(const QList< KActionCollection * > &actionCollections)
Set a list of action collections to check against for conflictuous shortcut.
Validation
An enum about validation when setting a key sequence.
@ Validate
Validate key sequence.
void stealShortcut(const QKeySequence &seq, QAction *action)
This signal is emitted after the user agreed to steal a shortcut from an action.
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)
void setDown(bool)
void setIcon(const QIcon &icon)
void setText(const QString &text)
void addWidget(QWidget *widget, int stretch, Qt::Alignment alignment)
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 setContentsMargins(const QMargins &margins)
void append(QList< T > &&value)
void clear()
bool isEmpty() const const
qsizetype size() const const
Q_EMITQ_EMIT
QMetaObject::Connection connect(const QObject *sender, PointerToMemberFunction signal, Functor functor)
QVariant property(const char *name) const const
bool setProperty(const char *name, QVariant &&value)
iterator insert(const T &value)
QString & append(QChar ch)
qsizetype size() const const
StrongFocus
void keySequence(QWidget *widget, const QKeySequence &keySequence)
virtual bool event(QEvent *event) override
void setFocusPolicy(Qt::FocusPolicy policy)
void setFocusProxy(QWidget *w)
void show()
void setToolTip(const QString &)
virtual void setVisible(bool visible)
QWidget * window() const const
QWindow * windowHandle() const const
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Fri Jul 26 2024 11:51:48 by doxygen 1.11.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.