KXmlGui

kkeysequencewidget.cpp
1 /*
2  This file is part of the KDE libraries
3  SPDX-FileCopyrightText: 1998 Mark Donohoe <[email protected]>
4  SPDX-FileCopyrightText: 2001 Ellis Whitehead <[email protected]>
5  SPDX-FileCopyrightText: 2007 Andreas Hartmetz <[email protected]>
6  SPDX-FileCopyrightText: 2020 David Redondo <[email protected]>
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 <KLocalizedString>
25 #include <KMessageBox>
26 #include <KeySequenceRecorder>
27 #if HAVE_GLOBALACCEL
28 #include <KGlobalAccel>
29 #endif
30 
31 static 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 
51 class KKeySequenceWidgetPrivate
52 {
53 public:
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 #if HAVE_GLOBALACCEL
71  bool promptStealGlobalShortcut(const QHash<QKeySequence, QList<KGlobalShortcutInfo>> &shortcuts, const QKeySequence &sequence);
72 #endif
73  void wontStealShortcut(QAction *item, const QKeySequence &seq);
74 
75  bool checkAgainstStandardShortcuts() const
76  {
77  return checkAgainstShortcutTypes & KKeySequenceWidget::StandardShortcuts;
78  }
79 
80  bool checkAgainstGlobalShortcuts() const
81  {
82  return checkAgainstShortcutTypes & KKeySequenceWidget::GlobalShortcuts;
83  }
84 
85  bool checkAgainstLocalShortcuts() const
86  {
87  return checkAgainstShortcutTypes & KKeySequenceWidget::LocalShortcuts;
88  }
89 
90  // private slot
91  void doneRecording();
92 
93  // members
94  KKeySequenceWidget *const q;
95  KeySequenceRecorder *recorder;
96  QHBoxLayout *layout;
97  QPushButton *keyButton;
98  QToolButton *clearButton;
99 
101  QKeySequence oldKeySequence;
102  QString componentName;
103 
104  //! Check the key sequence against KStandardShortcut::find()
105  KKeySequenceWidget::ShortcutTypes checkAgainstShortcutTypes;
106 
107  /**
108  * The list of action to check against for conflict shortcut
109  */
110  QList<QAction *> checkList; // deprecated
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 
123 KKeySequenceWidgetPrivate::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 
134 void KKeySequenceWidgetPrivate::init()
135 {
136  layout = new QHBoxLayout(q);
137  layout->setContentsMargins(0, 0, 0, 0);
138 
139  keyButton = new QPushButton(q);
140  keyButton->setFocusPolicy(Qt::StrongFocus);
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 KeySequenceRecorder(q->window()->windowHandle(), q);
157  recorder->setModifierlessAllowed(false);
158  recorder->setMultiKeyShortcutsAllowed(true);
159 
160  updateShortcutDisplay();
161 }
162 
163 bool 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",
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 
189 void 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::sorry(q, msg, title);
198 }
199 
200 bool KKeySequenceWidgetPrivate::conflictWithLocalShortcuts(const QKeySequence &keySequence)
201 {
202  if (!(checkAgainstShortcutTypes & KKeySequenceWidget::LocalShortcuts)) {
203  return false;
204  }
205 
206  // We have actions both in the deprecated checkList and the
207  // checkActionCollections list. Add all the actions 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  allActions += checkList;
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.
243  if (checkActionCollections.first()->isShortcutsConfigurable(qaction)) {
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
269 bool KKeySequenceWidgetPrivate::promptStealGlobalShortcut(const QHash<QKeySequence, QList<KGlobalShortcutInfo>> &clashing, const QKeySequence &sequence)
270 {
271  QString clashingKeys;
272  for (auto it = clashing.begin(); it != clashing.end(); ++it) {
273  const auto seqAsString = it.key().toString();
274  for (const KGlobalShortcutInfo &info : it.value()) {
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 
299 bool 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
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::sorry(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.
320  for (int i = 0; i < keySequence.count(); ++i) {
321  QKeySequence keys(keySequence[i]);
322  if (!KGlobalAccel::isGlobalShortcutAvailable(keySequence, componentName)) {
323  clashing.insert(keySequence, KGlobalAccel::getGlobalShortcutsByKey(keys));
324  }
325  }
326  if (clashing.isEmpty()) {
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().
339  for (int i = 0; i < keySequence.count(); ++i) {
341  }
342  return false;
343 #else
344  Q_UNUSED(keySequence);
345  return false;
346 #endif
347 }
348 
349 bool KKeySequenceWidgetPrivate::promptstealStandardShortcut(KStandardShortcut::StandardShortcut std, const QKeySequence &seq)
350 {
351  QString title = i18nc("@title:window", "Conflict with Standard Application Shortcut");
352  QString message = i18n(
353  "The '%1' key combination is also used for the standard action "
354  "\"%2\" that some applications use.\n"
355  "Do you really want to use it as a global shortcut as well?",
358 
359  return KMessageBox::warningContinueCancel(q, message, title, KGuiItem(i18nc("@action:button", "Reassign"))) == KMessageBox::Continue;
360 }
361 
362 bool KKeySequenceWidgetPrivate::conflictWithStandardShortcuts(const QKeySequence &seq)
363 {
364  if (!(checkAgainstShortcutTypes & KKeySequenceWidget::StandardShortcuts)) {
365  return false;
366  }
368  if (ssc != KStandardShortcut::AccelNone && !promptstealStandardShortcut(ssc, seq)) {
369  return true;
370  }
371  return false;
372 }
373 
374 void KKeySequenceWidgetPrivate::startRecording()
375 {
376  keyButton->setDown(true);
377  recorder->startRecording();
378  updateShortcutDisplay();
379 }
380 
381 void KKeySequenceWidgetPrivate::doneRecording()
382 {
383  keyButton->setDown(false);
384  stealActions.clear();
385  keyButton->setText(keyButton->text().chopped(strlen(" ...")));
386  q->setKeySequence(recorder->currentKeySequence(), KKeySequenceWidget::Validate);
387  updateShortcutDisplay();
388 }
389 
390 void KKeySequenceWidgetPrivate::updateShortcutDisplay()
391 {
392  QString s;
393  QKeySequence sequence = recorder->isRecording() ? recorder->currentKeySequence() : keySequence;
394  if (!sequence.isEmpty()) {
395  s = sequence.toString(QKeySequence::NativeText);
396  } else if (recorder->isRecording()) {
397  s = i18nc("What the user inputs now will be taken as the new shortcut", "Input");
398  } else {
399  s = i18nc("No shortcut defined", "None");
400  }
401 
402  if (recorder->isRecording()) {
403  // make it clear that input is still going on
404  s.append(QLatin1String(" ..."));
405  }
406 
407  s = QLatin1Char(' ') + s + QLatin1Char(' ');
408  keyButton->setText(s);
409 }
410 
412  : QWidget(parent)
413  , d(new KKeySequenceWidgetPrivate(this))
414 {
415  d->init();
416  setFocusProxy(d->keyButton);
419 
420  connect(d->recorder, &KeySequenceRecorder::currentKeySequenceChanged, this, [this] {
421  d->updateShortcutDisplay();
422  });
423  connect(d->recorder, &KeySequenceRecorder::recordingChanged, this, [this] {
424  if (!d->recorder->isRecording()) {
425  d->doneRecording();
426  }
427  });
428 }
429 
431 {
432  delete d;
433 }
434 
436 {
437  return d->checkAgainstShortcutTypes;
438 }
439 
441 {
442  d->componentName = componentName;
443 }
444 
445 bool KKeySequenceWidget::multiKeyShortcutsAllowed() const
446 {
447  return d->recorder->multiKeyShortcutsAllowed();
448 }
449 
451 {
452  d->recorder->setMultiKeyShortcutsAllowed(allowed);
453 }
454 
456 {
457  d->checkAgainstShortcutTypes = types;
458 }
459 
461 {
462  d->recorder->setModifierlessAllowed(allow);
463 }
464 
466 {
467  if (keySequence.isEmpty()) {
468  return true;
469  }
470  return !(d->conflictWithLocalShortcuts(keySequence) //
471  || d->conflictWithGlobalShortcuts(keySequence) //
472  || d->conflictWithStandardShortcuts(keySequence));
473 }
474 
476 {
477  return d->recorder->modifierlessAllowed();
478 }
479 
481 {
482  d->clearButton->setVisible(show);
483 }
484 
485 #if KXMLGUI_BUILD_DEPRECATED_SINCE(4, 1)
486 void KKeySequenceWidget::setCheckActionList(const QList<QAction *> &checkList) // deprecated
487 {
488  d->checkList = checkList;
489  Q_ASSERT(d->checkActionCollections.isEmpty()); // don't call this method if you call setCheckActionCollections!
490 }
491 #endif
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
526 {
528 }
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 
562 #include "moc_kkeysequencewidget.cpp"
KCOREADDONS_EXPORT void message(KMessage::MessageType messageType, const QString &text, const QString &caption=QString())
A widget to input a QKeySequence.
A container for a set of QAction objects.
void setComponentName(const QString &componentName)
If the component using this widget supports shortcuts contexts, it has to set its component name so w...
QString & append(QChar ch)
StrongFocus
QHash::iterator insert(const Key &key, const T &value)
Check against global shortcuts.
QWidget * window() const const
int count() const const
void setModifierlessAllowed(bool allow)
This only applies to user input, not to setKeySequence().
static QString removeAcceleratorMarker(const QString &label)
int size() const const
StandardShortcut find(const QKeySequence &keySeq)
void setMultiKeyShortcutsAllowed(bool)
Allow multikey shortcuts?
QSet::iterator insert(const T &value)
KKeySequenceWidget(QWidget *parent=nullptr)
Constructor.
int size() const const
Check with local shortcuts.
void append(const T &value)
QKeySequence::SequenceMatch matches(const QKeySequence &seq) const const
ShortcutTypes checkForConflictsAgainst() const
The shortcut types we check for conflicts.
ButtonCode warningContinueCancel(QWidget *parent, const QString &text, const QString &caption=QString(), const KGuiItem &buttonContinue=KStandardGuiItem::cont(), const KGuiItem &buttonCancel=KStandardGuiItem::cancel(), const QString &dontAskAgainName=QString(), Options options=Notify)
void setKeySequence(const QKeySequence &seq, Validation val=NoValidate)
Set the key sequence.
void keySequenceChanged(const QKeySequence &seq)
This signal is emitted when the current key sequence has changed, be it by user input or programmatic...
QString i18nc(const char *context, const char *text, const TYPE &arg...)
QString label(StandardShortcut id)
bool isEmpty() const const
void setFocusProxy(QWidget *w)
void clicked(bool checked)
void clearKeySequence()
Clear the key sequence.
virtual ~KKeySequenceWidget()
Destructs the widget.
void captureKeySequence()
Capture a shortcut from the keyboard.
Validation
An enum about validation when setting a key sequence.
void applyStealShortcut()
Actually remove the shortcut that the user wanted to steal, from the action that was using it...
bool contains(QChar ch, Qt::CaseSensitivity cs) const const
Check against standard shortcuts.
bool isEmpty() const const
QString i18ncp(const char *context, const char *singular, const char *plural, const TYPE &arg...)
QCA_EXPORT void init()
QWindow * windowHandle() const const
void setCheckActionCollections(const QList< KActionCollection * > &actionCollections)
Set a list of action collections to check against for conflictuous shortcut.
void setCheckActionList(const QList< QAction * > &checkList)
QString i18n(const char *text, const TYPE &arg...)
static QList< KGlobalShortcutInfo > getGlobalShortcutsByKey(const QKeySequence &seq)
QString toString(QKeySequence::SequenceFormat format) const const
bool isEmpty() const const
QAction * action(MenuId id) const
Returns the QAction * associated with the given parameter Will return a nullptr if menu() has not bee...
Definition: khelpmenu.cpp:201
QKeySequence keySequence() const
Return the currently selected key sequence.
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 keySequence(QWindow *window, const QKeySequence &keySequence)
QIcon fromTheme(const QString &name)
void setCheckForConflictsAgainst(ShortcutTypes types)
Configure if the widget should check for conflicts with existing shortcuts.
void show()
bool isKeySequenceAvailable(const QKeySequence &seq) const
Checks whether the key sequence seq is available to grab.
QMetaObject::Connection connect(const QObject *sender, const char *signal, const QObject *receiver, const char *method, Qt::ConnectionType type)
QObject * parent() const const
Validate key sequence.
static void stealShortcutSystemwide(const QKeySequence &seq)
static bool isGlobalShortcutAvailable(const QKeySequence &seq, const QString &component=QString())
Q_EMITQ_EMIT
void sorry(QWidget *parent, const QString &text, const QString &caption=QString(), Options options=Notify)
This file is part of the KDE documentation.
Documentation copyright © 1996-2021 The KDE developers.
Generated on Sat Oct 16 2021 22:52:25 by doxygen 1.8.11 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.