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 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(needle) != QKeySequence::NoMatch //
43 || needle.matches(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(0, 0, 0, 0);
138
139 keyButton = new QPushButton(q);
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),
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,
184 conflictingShortcuts);
185
186 return KMessageBox::warningContinueCancel(q, message, title, 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>",
197 KMessageBox::error(q, 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(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(allActions)) {
238 if (shortcutsConflictWith(qaction->shortcuts(), keySequence)) {
239 // A conflict with a KAction. If that action is configurable
240 // ask the user what to do. If not reject this keySequence.
242 conflictingActions.append(qaction);
243 } else {
244 wontStealShortcut(qaction, 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(conflictingActions, keySequence)) {
256 stealActions = conflictingActions;
257 // Announce that the user agreed
258 for (QAction *stealAction : std::as_const(stealActions)) {
259 Q_EMIT q->stealShortcut(keySequence, 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(q, message, title, 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(keySequence, componentName)) {
321 clashing.push_back({keySequence, KGlobalAccel::globalShortcutsByKey(keys)});
322 }
323 }
324 if (clashing.empty()) {
325 return false;
326 }
327
328 if (!promptStealGlobalShortcut(clashing, 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().
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?",
354
355 return KMessageBox::warningContinueCancel(q, message, title, 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 }
364 if (ssc != KStandardShortcut::AccelNone && !promptstealStandardShortcut(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(strlen(" ...")));
383 updateShortcutDisplay();
384}
385
386void KKeySequenceWidgetPrivate::updateShortcutDisplay()
387{
388 QString s;
389 QKeySequence sequence = recorder->isRecording() ? recorder->currentKeySequence() : keySequence;
390 if (!sequence.isEmpty()) {
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(QLatin1String(" ..."));
401 }
402
403 s = QLatin1Char(' ') + s + QLatin1Char(' ');
404 keyButton->setText(s);
405}
406
408 : QWidget(parent)
409 , d(new KKeySequenceWidgetPrivate(this))
410{
411 d->init();
412 setFocusProxy(d->keyButton);
415
416 connect(d->recorder, &KKeySequenceRecorder::currentKeySequenceChanged, this, [this] {
417 d->updateShortcutDisplay();
418 });
419 connect(d->recorder, &KKeySequenceRecorder::recordingChanged, this, [this] {
420 if (!d->recorder->isRecording()) {
421 d->doneRecording();
422 }
423 });
424}
425
430
431KKeySequenceWidget::ShortcutTypes KKeySequenceWidget::checkForConflictsAgainst() const
432{
433 return d->checkAgainstShortcutTypes;
434}
435
437{
438 d->componentName = componentName;
439}
440
441bool KKeySequenceWidget::multiKeyShortcutsAllowed() const
442{
443 return d->recorder->multiKeyShortcutsAllowed();
444}
445
447{
448 d->recorder->setMultiKeyShortcutsAllowed(allowed);
449}
450
452{
453 d->checkAgainstShortcutTypes = types;
454}
455
457{
458 d->recorder->setModifierlessAllowed(allow);
459}
460
462{
463 if (keySequence.isEmpty()) {
464 return true;
465 }
466 return !(d->conflictWithLocalShortcuts(keySequence) //
467 || d->conflictWithGlobalShortcuts(keySequence) //
468 || d->conflictWithStandardShortcuts(keySequence));
469}
470
472{
473 return d->recorder->modifierlessAllowed();
474}
475
477{
478 d->clearButton->setVisible(show);
479}
480
482{
483 d->checkActionCollections = actionCollections;
484}
485
486// slot
488{
489 d->recorder->setWindow(window()->windowHandle());
490 d->recorder->startRecording();
491}
492
494{
495 return d->keySequence;
496}
497
498// slot
500{
501 if (d->keySequence == seq) {
502 return;
503 }
504 if (validate == Validate && !isKeySequenceAvailable(seq)) {
505 return;
506 }
507 d->keySequence = seq;
508 d->updateShortcutDisplay();
510}
511
512// slot
517
518// slot
520{
522
523 for (QAction *stealAction : std::as_const(d->stealActions)) {
524 // Stealing a shortcut means setting it to an empty one.
525 stealAction->setShortcuts(QList<QKeySequence>());
526
527 // The following code will find the action we are about to
528 // steal from and save it's actioncollection.
529 KActionCollection *parentCollection = nullptr;
530 for (KActionCollection *collection : std::as_const(d->checkActionCollections)) {
531 if (collection->actions().contains(stealAction)) {
532 parentCollection = collection;
533 break;
534 }
535 }
536
537 // Remember the changed collection
538 if (parentCollection) {
539 changedCollections.insert(parentCollection);
540 }
541 }
542
543 for (KActionCollection *col : std::as_const(changedCollections)) {
544 col->writeSettings();
545 }
546
547 d->stealActions.clear();
548}
549
550bool KKeySequenceWidget::event(QEvent *ev)
551{
552 constexpr char _highlight[] = "_kde_highlight_neutral";
553
554 if (ev->type() == QEvent::DynamicPropertyChange) {
555 auto dpev = static_cast<QDynamicPropertyChangeEvent *>(ev);
556 if (dpev->propertyName() == _highlight) {
557 d->keyButton->setProperty(_highlight, property(_highlight));
558 return true;
559 }
560 }
561
562 return QWidget::event(ev);
563}
564
565#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 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
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()
iterator insert(const_iterator before, parameter_type value)
bool isEmpty() const const
qsizetype size() const const
Q_EMITQ_EMIT
QMetaObject::Connection connect(const QObject *sender, PointerToMemberFunction signal, Functor functor)
QList< T > findChildren(Qt::FindChildOptions options) const const
QVariant property(const char *name) const const
bool setProperty(const char *name, QVariant &&value)
QString & append(QChar ch)
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 Tue Mar 26 2024 11:21:12 by doxygen 1.10.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.