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 
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  KeySequenceRecorder *recorder;
101  QHBoxLayout *layout;
102  QPushButton *keyButton;
103  QToolButton *clearButton;
104 
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 to check against for conflict shortcut
114  */
115  QList<QAction *> checkList; // deprecated
116 
117  /**
118  * The list of action collections to check against for conflict shortcut
119  */
120  QList<KActionCollection *> checkActionCollections;
121 
122  /**
123  * The action to steal the shortcut from.
124  */
125  QList<QAction *> stealActions;
126 };
127 
128 KKeySequenceWidgetPrivate::KKeySequenceWidgetPrivate(KKeySequenceWidget *qq)
129  : q(qq)
130  , layout(nullptr)
131  , keyButton(nullptr)
132  , clearButton(nullptr)
133  , componentName()
134  , checkAgainstShortcutTypes(KKeySequenceWidget::LocalShortcuts | KKeySequenceWidget::GlobalShortcuts)
135  , stealActions()
136 {
137 }
138 
139 void KKeySequenceWidgetPrivate::init()
140 {
141  layout = new QHBoxLayout(q);
142  layout->setContentsMargins(0, 0, 0, 0);
143 
144  keyButton = new QPushButton(q);
145  keyButton->setFocusPolicy(Qt::StrongFocus);
146  keyButton->setIcon(QIcon::fromTheme(QStringLiteral("configure")));
147  keyButton->setToolTip(
148  i18nc("@info:tooltip",
149  "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."));
150  layout->addWidget(keyButton);
151 
152  clearButton = new QToolButton(q);
153  layout->addWidget(clearButton);
154 
155  if (qApp->isLeftToRight()) {
156  clearButton->setIcon(QIcon::fromTheme(QStringLiteral("edit-clear-locationbar-rtl")));
157  } else {
158  clearButton->setIcon(QIcon::fromTheme(QStringLiteral("edit-clear-locationbar-ltr")));
159  }
160 
161  recorder = new KeySequenceRecorder(q->window()->windowHandle(), q);
162  recorder->setModifierlessAllowed(false);
163  recorder->setMultiKeyShortcutsAllowed(true);
164 
165  updateShortcutDisplay();
166 }
167 
168 bool KKeySequenceWidgetPrivate::promptStealLocalShortcut(const QList<QAction *> &actions, const QKeySequence &seq)
169 {
170  const int listSize = actions.size();
171 
172  QString title = i18ncp("%1 is the number of conflicts", "Shortcut Conflict", "Shortcut Conflicts", listSize);
173 
174  QString conflictingShortcuts;
175  for (const QAction *action : actions) {
176  conflictingShortcuts += i18n("Shortcut '%1' for action '%2'\n",
177  action->shortcut().toString(QKeySequence::NativeText),
179  }
180  QString message = i18ncp("%1 is the number of ambiguous shortcut clashes (hidden)",
181  "The \"%2\" shortcut is ambiguous with the following shortcut.\n"
182  "Do you want to assign an empty shortcut to this action?\n"
183  "%3",
184  "The \"%2\" shortcut is ambiguous with the following shortcuts.\n"
185  "Do you want to assign an empty shortcut to these actions?\n"
186  "%3",
187  listSize,
189  conflictingShortcuts);
190 
191  return KMessageBox::warningContinueCancel(q, message, title, KGuiItem(i18nc("@action:button", "Reassign"))) == KMessageBox::Continue;
192 }
193 
194 void KKeySequenceWidgetPrivate::wontStealShortcut(QAction *item, const QKeySequence &seq)
195 {
196  QString title(i18nc("@title:window", "Shortcut conflict"));
197  QString msg(
198  i18n("<qt>The '%1' key combination is already used by the <b>%2</b> action.<br>"
199  "Please select a different one.</qt>",
202  KMessageBox::error(q, msg, title);
203 }
204 
205 bool KKeySequenceWidgetPrivate::conflictWithLocalShortcuts(const QKeySequence &keySequence)
206 {
207  if (!(checkAgainstShortcutTypes & KKeySequenceWidget::LocalShortcuts)) {
208  return false;
209  }
210 
211  // We have actions both in the deprecated checkList and the
212  // checkActionCollections list. Add all the actions to a single list to
213  // be able to process them in a single loop below.
214  // Note that this can't be done in setCheckActionCollections(), because we
215  // keep pointers to the action collections, and between the call to
216  // setCheckActionCollections() and this function some actions might already be
217  // removed from the collection again.
218  QList<QAction *> allActions;
219  allActions += checkList;
220  for (KActionCollection *collection : std::as_const(checkActionCollections)) {
221  allActions += collection->actions();
222  }
223 
224  // Because of multikey shortcuts we can have clashes with many shortcuts.
225  //
226  // Example 1:
227  //
228  // Application currently uses 'CTRL-X,a', 'CTRL-X,f' and 'CTRL-X,CTRL-F'
229  // and the user wants to use 'CTRL-X'. 'CTRL-X' will only trigger as
230  // 'activatedAmbiguously()' for obvious reasons.
231  //
232  // Example 2:
233  //
234  // Application currently uses 'CTRL-X'. User wants to use 'CTRL-X,CTRL-F'.
235  // This will shadow 'CTRL-X' for the same reason as above.
236  //
237  // Example 3:
238  //
239  // Some weird combination of Example 1 and 2 with three shortcuts using
240  // 1/2/3 key shortcuts. I think you can imagine.
241  QList<QAction *> conflictingActions;
242 
243  // find conflicting shortcuts with existing actions
244  for (QAction *qaction : std::as_const(allActions)) {
245  if (shortcutsConflictWith(qaction->shortcuts(), keySequence)) {
246  // A conflict with a KAction. If that action is configurable
247  // ask the user what to do. If not reject this keySequence.
248  if (checkActionCollections.first()->isShortcutsConfigurable(qaction)) {
249  conflictingActions.append(qaction);
250  } else {
251  wontStealShortcut(qaction, keySequence);
252  return true;
253  }
254  }
255  }
256 
257  if (conflictingActions.isEmpty()) {
258  // No conflicting shortcuts found.
259  return false;
260  }
261 
262  if (promptStealLocalShortcut(conflictingActions, keySequence)) {
263  stealActions = conflictingActions;
264  // Announce that the user agreed
265  for (QAction *stealAction : std::as_const(stealActions)) {
266  Q_EMIT q->stealShortcut(keySequence, stealAction);
267  }
268  return false;
269  }
270  return true;
271 }
272 
273 #if HAVE_GLOBALACCEL
274 bool KKeySequenceWidgetPrivate::promptStealGlobalShortcut(const std::vector<KeyConflictInfo> &clashing, const QKeySequence &sequence)
275 {
276  QString clashingKeys;
277  for (const auto &[key, shortcutInfo] : clashing) {
278  const QString seqAsString = key.toString();
279  for (const KGlobalShortcutInfo &info : shortcutInfo) {
280  clashingKeys += i18n("Shortcut '%1' in Application '%2' for action '%3'\n", //
281  seqAsString,
282  info.componentFriendlyName(),
283  info.friendlyName());
284  }
285  }
286  const int hashSize = clashing.size();
287 
288  QString message = i18ncp("%1 is the number of conflicts (hidden), %2 is the key sequence of the shortcut that is problematic",
289  "The shortcut '%2' conflicts with the following key combination:\n",
290  "The shortcut '%2' conflicts with the following key combinations:\n",
291  hashSize,
292  sequence.toString());
293  message += clashingKeys;
294 
295  QString title = i18ncp("%1 is the number of shortcuts with which there is a conflict",
296  "Conflict with Registered Global Shortcut",
297  "Conflict with Registered Global Shortcuts",
298  hashSize);
299 
300  return KMessageBox::warningContinueCancel(q, message, title, KGuiItem(i18nc("@action:button", "Reassign"))) == KMessageBox::Continue;
301 }
302 #endif
303 
304 bool KKeySequenceWidgetPrivate::conflictWithGlobalShortcuts(const QKeySequence &keySequence)
305 {
306 #ifdef Q_OS_WIN
307  // on windows F12 is reserved by the debugger at all times, so we can't use it for a global shortcut
308  if (KKeySequenceWidget::GlobalShortcuts && keySequence.toString().contains(QLatin1String("F12"))) {
309  QString title = i18n("Reserved Shortcut");
310  QString message = i18n(
311  "The F12 key is reserved on Windows, so cannot be used for a global shortcut.\n"
312  "Please choose another one.");
313 
314  KMessageBox::error(q, message, title);
315  return false;
316  }
317 #endif
318 #if HAVE_GLOBALACCEL
319  if (!(checkAgainstShortcutTypes & KKeySequenceWidget::GlobalShortcuts)) {
320  return false;
321  }
322  // Global shortcuts are on key+modifier shortcuts. They can clash with
323  // each of the keys of a multi key shortcut.
324  std::vector<KeyConflictInfo> clashing;
325  for (int i = 0; i < keySequence.count(); ++i) {
326  QKeySequence keys(keySequence[i]);
327  if (!KGlobalAccel::isGlobalShortcutAvailable(keySequence, componentName)) {
328  clashing.push_back({keySequence, KGlobalAccel::globalShortcutsByKey(keys)});
329  }
330  }
331  if (clashing.empty()) {
332  return false;
333  }
334 
335  if (!promptStealGlobalShortcut(clashing, keySequence)) {
336  return true;
337  }
338  // The user approved stealing the shortcut. We have to steal
339  // it immediately because KAction::setGlobalShortcut() refuses
340  // to set a global shortcut that is already used. There is no
341  // error it just silently fails. So be nice because this is
342  // most likely the first action that is done in the slot
343  // listening to keySequenceChanged().
345  return false;
346 #else
347  Q_UNUSED(keySequence);
348  return false;
349 #endif
350 }
351 
352 bool KKeySequenceWidgetPrivate::promptstealStandardShortcut(KStandardShortcut::StandardShortcut std, const QKeySequence &seq)
353 {
354  QString title = i18nc("@title:window", "Conflict with Standard Application Shortcut");
355  QString message = i18n(
356  "The '%1' key combination is also used for the standard action "
357  "\"%2\" that some applications use.\n"
358  "Do you really want to use it as a global shortcut as well?",
361 
362  return KMessageBox::warningContinueCancel(q, message, title, KGuiItem(i18nc("@action:button", "Reassign"))) == KMessageBox::Continue;
363 }
364 
365 bool KKeySequenceWidgetPrivate::conflictWithStandardShortcuts(const QKeySequence &seq)
366 {
367  if (!(checkAgainstShortcutTypes & KKeySequenceWidget::StandardShortcuts)) {
368  return false;
369  }
371  if (ssc != KStandardShortcut::AccelNone && !promptstealStandardShortcut(ssc, seq)) {
372  return true;
373  }
374  return false;
375 }
376 
377 void KKeySequenceWidgetPrivate::startRecording()
378 {
379  keyButton->setDown(true);
380  recorder->startRecording();
381  updateShortcutDisplay();
382 }
383 
384 void KKeySequenceWidgetPrivate::doneRecording()
385 {
386  keyButton->setDown(false);
387  stealActions.clear();
388  keyButton->setText(keyButton->text().chopped(strlen(" ...")));
389  q->setKeySequence(recorder->currentKeySequence(), KKeySequenceWidget::Validate);
390  updateShortcutDisplay();
391 }
392 
393 void KKeySequenceWidgetPrivate::updateShortcutDisplay()
394 {
395  QString s;
396  QKeySequence sequence = recorder->isRecording() ? recorder->currentKeySequence() : keySequence;
397  if (!sequence.isEmpty()) {
398  s = sequence.toString(QKeySequence::NativeText);
399  } else if (recorder->isRecording()) {
400  s = i18nc("What the user inputs now will be taken as the new shortcut", "Input");
401  } else {
402  s = i18nc("No shortcut defined", "None");
403  }
404 
405  if (recorder->isRecording()) {
406  // make it clear that input is still going on
407  s.append(QLatin1String(" ..."));
408  }
409 
410  s = QLatin1Char(' ') + s + QLatin1Char(' ');
411  keyButton->setText(s);
412 }
413 
415  : QWidget(parent)
416  , d(new KKeySequenceWidgetPrivate(this))
417 {
418  d->init();
419  setFocusProxy(d->keyButton);
422 
423  connect(d->recorder, &KeySequenceRecorder::currentKeySequenceChanged, this, [this] {
424  d->updateShortcutDisplay();
425  });
426  connect(d->recorder, &KeySequenceRecorder::recordingChanged, this, [this] {
427  if (!d->recorder->isRecording()) {
428  d->doneRecording();
429  }
430  });
431 }
432 
434 {
435  delete d;
436 }
437 
438 KKeySequenceWidget::ShortcutTypes KKeySequenceWidget::checkForConflictsAgainst() const
439 {
440  return d->checkAgainstShortcutTypes;
441 }
442 
444 {
445  d->componentName = componentName;
446 }
447 
448 bool 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 
464 {
465  d->recorder->setModifierlessAllowed(allow);
466 }
467 
469 {
470  if (keySequence.isEmpty()) {
471  return true;
472  }
473  return !(d->conflictWithLocalShortcuts(keySequence) //
474  || d->conflictWithGlobalShortcuts(keySequence) //
475  || d->conflictWithStandardShortcuts(keySequence));
476 }
477 
479 {
480  return d->recorder->modifierlessAllowed();
481 }
482 
484 {
485  d->clearButton->setVisible(show);
486 }
487 
488 #if KXMLGUI_BUILD_DEPRECATED_SINCE(4, 1)
489 void KKeySequenceWidget::setCheckActionList(const QList<QAction *> &checkList) // deprecated
490 {
491  d->checkList = checkList;
492  Q_ASSERT(d->checkActionCollections.isEmpty()); // don't call this method if you call setCheckActionCollections!
493 }
494 #endif
495 
497 {
498  d->checkActionCollections = actionCollections;
499 }
500 
501 // slot
503 {
504  d->recorder->setWindow(window()->windowHandle());
505  d->recorder->startRecording();
506 }
507 
509 {
510  return d->keySequence;
511 }
512 
513 // slot
515 {
516  if (d->keySequence == seq) {
517  return;
518  }
519  if (validate == Validate && !isKeySequenceAvailable(seq)) {
520  return;
521  }
522  d->keySequence = seq;
523  d->updateShortcutDisplay();
525 }
526 
527 // slot
529 {
531 }
532 
533 // slot
535 {
536  QSet<KActionCollection *> changedCollections;
537 
538  for (QAction *stealAction : std::as_const(d->stealActions)) {
539  // Stealing a shortcut means setting it to an empty one.
540  stealAction->setShortcuts(QList<QKeySequence>());
541 
542  // The following code will find the action we are about to
543  // steal from and save it's actioncollection.
544  KActionCollection *parentCollection = nullptr;
545  for (KActionCollection *collection : std::as_const(d->checkActionCollections)) {
546  if (collection->actions().contains(stealAction)) {
547  parentCollection = collection;
548  break;
549  }
550  }
551 
552  // Remember the changed collection
553  if (parentCollection) {
554  changedCollections.insert(parentCollection);
555  }
556  }
557 
558  for (KActionCollection *col : std::as_const(changedCollections)) {
559  col->writeSettings();
560  }
561 
562  d->stealActions.clear();
563 }
564 
565 #include "moc_kkeysequencewidget.cpp"
void append(const T &value)
QWidget * window() const const
void setMultiKeyShortcutsAllowed(bool)
Allow multikey shortcuts?
QKeySequence keySequence
@ StandardShortcuts
Check against standard shortcuts.
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)
Q_EMITQ_EMIT
static QList< KGlobalShortcutInfo > globalShortcutsByKey(const QKeySequence &seq, MatchType type=Equal)
void setCheckActionList(const QList< QAction * > &checkList)
QCA_EXPORT void init()
void setKeySequence(const QKeySequence &seq, Validation val=NoValidate)
Set the key sequence.
void clicked(bool checked)
A widget to input a QKeySequence.
QIcon fromTheme(const QString &name)
void applyStealShortcut()
Actually remove the shortcut that the user wanted to steal, from the action that was using it.
void setFocusProxy(QWidget *w)
~KKeySequenceWidget() override
Destructs the widget.
QKeySequence::SequenceMatch matches(const QKeySequence &seq) const const
void keySequence(QWindow *window, const QKeySequence &keySequence)
QMetaObject::Connection connect(const QObject *sender, const char *signal, const QObject *receiver, const char *method, Qt::ConnectionType type)
void captureKeySequence()
Capture a shortcut from the keyboard.
void setCheckForConflictsAgainst(ShortcutTypes types)
Configure if the widget should check for conflicts with existing shortcuts.
QString i18ncp(const char *context, const char *singular, const char *plural, const TYPE &arg...)
A container for a set of QAction objects.
int size() const const
QString i18n(const char *text, const TYPE &arg...)
static void stealShortcutSystemwide(const QKeySequence &seq)
QString toString(QKeySequence::SequenceFormat format) const const
void setComponentName(const QString &componentName)
If the component using this widget supports shortcuts contexts, it has to set its component name so w...
void keySequenceChanged(const QKeySequence &seq)
This signal is emitted when the current key sequence has changed, be it by user input or programmatic...
void setClearButtonShown(bool show)
Set whether a small button to set an empty key sequence should be displayed next to the main input wi...
bool isEmpty() const const
bool isKeySequenceAvailable(const QKeySequence &seq) const
Checks whether the key sequence seq is available to grab.
void clearKeySequence()
Clear the key sequence.
const QList< QKeySequence > & find()
void error(QWidget *parent, const QString &text, const QString &title, const KGuiItem &buttonOk, Options options=Notify)
void show()
KKeySequenceWidget(QWidget *parent=nullptr)
Constructor.
QString label(StandardShortcut id)
@ LocalShortcuts
Check with local shortcuts.
QWindow * windowHandle() const const
void setModifierlessAllowed(bool allow)
This only applies to user input, not to setKeySequence().
static bool isGlobalShortcutAvailable(const QKeySequence &seq, const QString &component=QString())
static QString removeAcceleratorMarker(const QString &label)
QString i18nc(const char *context, const char *text, const TYPE &arg...)
QSet::iterator insert(const T &value)
@ Validate
Validate key sequence.
@ GlobalShortcuts
Check against global shortcuts.
void setCheckActionCollections(const QList< KActionCollection * > &actionCollections)
Set a list of action collections to check against for conflictuous shortcut.
StrongFocus
bool isEmpty() const const
QString message
QString & append(QChar ch)
Validation
An enum about validation when setting a key sequence.
This file is part of the KDE documentation.
Documentation copyright © 1996-2023 The KDE developers.
Generated on Mon May 8 2023 04:04:23 by doxygen 1.8.17 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.