KGuiAddons

kkeysequencerecorder.cpp
1/*
2 SPDX-FileCopyrightText: 1998 Mark Donohoe <donohoe@kde.org>
3 SPDX-FileCopyrightText: 2001 Ellis Whitehead <ellis@kde.org>
4 SPDX-FileCopyrightText: 2007 Andreas Hartmetz <ahartmetz@gmail.com>
5 SPDX-FileCopyrightText: 2020 David Redondo <kde@david-redondo.de>
6
7 SPDX-License-Identifier: LGPL-2.0-or-later
8*/
9
10#include "kkeysequencerecorder.h"
11
12#include "keyboardgrabber_p.h"
13#include "kguiaddons_debug.h"
14#include "shortcutinhibition_p.h"
15#include "waylandinhibition_p.h"
16
17#include <QGuiApplication>
18#include <QKeyEvent>
19#include <QPointer>
20#include <QTimer>
21#include <QWindow>
22
23#include <array>
24
25/// Singleton whose only purpose is to tell us about other sequence recorders getting started
26class KKeySequenceRecorderGlobal : public QObject
27{
29public:
30 static KKeySequenceRecorderGlobal *self()
31 {
32 static KKeySequenceRecorderGlobal s_self;
33 return &s_self;
34 }
35
37 void sequenceRecordingStarted();
38};
39
40class KKeySequenceRecorderPrivate : public QObject
41{
43public:
44 // Copy of QKeySequencePrivate::MaxKeyCount from private header
45 enum { MaxKeyCount = 4 };
46
47 KKeySequenceRecorderPrivate(KKeySequenceRecorder *qq);
48
49 void controlModifierlessTimeout();
50 bool eventFilter(QObject *watched, QEvent *event) override;
51 void handleKeyPress(QKeyEvent *event);
52 void handleKeyRelease(QKeyEvent *event);
53 void finishRecording();
54 void receivedRecording();
55
57 QKeySequence m_currentKeySequence;
58 QKeySequence m_previousKeySequence;
59 QPointer<QWindow> m_window;
60 bool m_isRecording;
61 bool m_multiKeyShortcutsAllowed;
62 bool m_modifierlessAllowed;
63 bool m_modifierOnlyAllowed = false;
64
65 Qt::KeyboardModifiers m_currentModifiers;
66 QTimer m_modifierlessTimer;
67 std::unique_ptr<ShortcutInhibition> m_inhibition;
68};
69
71
72// Copied here from KKeyServer
73static bool isShiftAsModifierAllowed(int keyQt)
74{
75 // remove any modifiers
76 keyQt &= ~Qt::KeyboardModifierMask;
77
78 // Shift only works as a modifier with certain keys. It's not possible
79 // to enter the SHIFT+5 key sequence for me because this is handled as
80 // '%' by qt on my keyboard.
81 // The working keys are all hardcoded here :-(
82 if (keyQt >= Qt::Key_F1 && keyQt <= Qt::Key_F35) {
83 return true;
84 }
85
86 if (QChar::isLetter(keyQt)) {
87 return true;
88 }
89
90 switch (keyQt) {
91 case Qt::Key_Return:
92 case Qt::Key_Space:
94 case Qt::Key_Tab:
95 case Qt::Key_Backtab:
96 case Qt::Key_Escape:
97 case Qt::Key_Print:
99 case Qt::Key_Pause:
100 case Qt::Key_PageUp:
101 case Qt::Key_PageDown:
102 case Qt::Key_Insert:
103 case Qt::Key_Delete:
104 case Qt::Key_Home:
105 case Qt::Key_End:
106 case Qt::Key_Up:
107 case Qt::Key_Down:
108 case Qt::Key_Left:
109 case Qt::Key_Right:
110 case Qt::Key_Enter:
111 case Qt::Key_SysReq:
112 case Qt::Key_CapsLock:
113 case Qt::Key_NumLock:
114 case Qt::Key_Help:
115 case Qt::Key_Back:
116 case Qt::Key_Forward:
117 case Qt::Key_Stop:
118 case Qt::Key_Refresh:
121 case Qt::Key_OpenUrl:
122 case Qt::Key_HomePage:
123 case Qt::Key_Search:
126 case Qt::Key_VolumeUp:
128 case Qt::Key_BassUp:
129 case Qt::Key_BassDown:
130 case Qt::Key_TrebleUp:
141 case Qt::Key_Memo:
142 case Qt::Key_ToDoList:
143 case Qt::Key_Calendar:
146 case Qt::Key_Standby:
152 case Qt::Key_PowerOff:
153 case Qt::Key_WakeUp:
154 case Qt::Key_Eject:
156 case Qt::Key_WWW:
157 case Qt::Key_Sleep:
159 case Qt::Key_Shop:
160 case Qt::Key_History:
162 case Qt::Key_HotLinks:
164 case Qt::Key_Finance:
170 case Qt::Key_Book:
171 case Qt::Key_CD:
172 case Qt::Key_Clear:
174 case Qt::Key_Close:
175 case Qt::Key_Copy:
176 case Qt::Key_Cut:
177 case Qt::Key_Display:
178 case Qt::Key_DOS:
180 case Qt::Key_Excel:
181 case Qt::Key_Explorer:
182 case Qt::Key_Game:
183 case Qt::Key_Go:
184 case Qt::Key_iTouch:
185 case Qt::Key_LogOff:
186 case Qt::Key_Market:
187 case Qt::Key_Meeting:
188 case Qt::Key_MenuKB:
189 case Qt::Key_MenuPB:
190 case Qt::Key_MySites:
191 case Qt::Key_News:
193 case Qt::Key_Option:
194 case Qt::Key_Paste:
195 case Qt::Key_Phone:
196 case Qt::Key_Reply:
197 case Qt::Key_Reload:
201 case Qt::Key_Save:
202 case Qt::Key_Send:
203 case Qt::Key_Spell:
205 case Qt::Key_Support:
206 case Qt::Key_TaskPane:
207 case Qt::Key_Terminal:
208 case Qt::Key_Tools:
209 case Qt::Key_Travel:
210 case Qt::Key_Video:
211 case Qt::Key_Word:
212 case Qt::Key_Xfer:
213 case Qt::Key_ZoomIn:
214 case Qt::Key_ZoomOut:
215 case Qt::Key_Away:
217 case Qt::Key_WebCam:
219 case Qt::Key_Pictures:
220 case Qt::Key_Music:
221 case Qt::Key_Battery:
223 case Qt::Key_WLAN:
224 case Qt::Key_UWB:
228 case Qt::Key_Subtitle:
230 case Qt::Key_Time:
231 case Qt::Key_Select:
232 case Qt::Key_View:
233 case Qt::Key_TopMenu:
234 case Qt::Key_Suspend:
236 case Qt::Key_Launch0:
237 case Qt::Key_Launch1:
238 case Qt::Key_Launch2:
239 case Qt::Key_Launch3:
240 case Qt::Key_Launch4:
241 case Qt::Key_Launch5:
242 case Qt::Key_Launch6:
243 case Qt::Key_Launch7:
244 case Qt::Key_Launch8:
245 case Qt::Key_Launch9:
246 case Qt::Key_LaunchA:
247 case Qt::Key_LaunchB:
248 case Qt::Key_LaunchC:
249 case Qt::Key_LaunchD:
250 case Qt::Key_LaunchE:
251 case Qt::Key_LaunchF:
252 return true;
253
254 default:
255 return false;
256 }
257}
258
259static bool isOkWhenModifierless(int key)
260{
261 // this whole function is a hack, but especially the first line of code
262 if (QKeySequence(key).toString().length() == 1) {
263 return false;
264 }
265
266 switch (key) {
267 case Qt::Key_Return:
268 case Qt::Key_Space:
269 case Qt::Key_Tab:
270 case Qt::Key_Backtab: // does this ever happen?
272 case Qt::Key_Delete:
273 return false;
274 default:
275 return true;
276 }
277}
278
279static QKeySequence appendToSequence(const QKeySequence &sequence, int key)
280{
281 if (sequence.count() >= KKeySequenceRecorderPrivate::MaxKeyCount) {
282 qCWarning(KGUIADDONS_LOG) << "Cannot append to a key to a sequence which is already of length" << sequence.count();
283 return sequence;
284 }
285
286 std::array<int, KKeySequenceRecorderPrivate::MaxKeyCount> keys{sequence[0].toCombined(),
287 sequence[1].toCombined(),
288 sequence[2].toCombined(),
289 sequence[3].toCombined()};
290 // When the user presses Mod(s)+Alt+Print, the SysReq event is fired only
291 // when the Alt key is released. Before we get the Mod(s)+SysReq event, we
292 // first get a Mod(s)+Alt event, which we have to ignore.
293 // Known limitation: only works when the Alt key is released before the Mod(s) key(s).
296 if (sequence.count() > 0 && (sequence[sequence.count() - 1].toCombined() & ~Qt::KeyboardModifierMask) == Qt::Key_Alt) {
297 keys[sequence.count() - 1] = key;
298 return QKeySequence(keys[0], keys[1], keys[2], keys[3]);
299 }
300 }
301 keys[sequence.count()] = key;
302 return QKeySequence(keys[0], keys[1], keys[2], keys[3]);
303}
304
305KKeySequenceRecorderPrivate::KKeySequenceRecorderPrivate(KKeySequenceRecorder *qq)
306 : QObject(qq)
307 , q(qq)
308{
309}
310
311void KKeySequenceRecorderPrivate::controlModifierlessTimeout()
312{
313 if (m_currentKeySequence != 0 && !m_currentModifiers) {
314 // No modifier key pressed currently. Start the timeout
315 m_modifierlessTimer.start(600);
316 } else {
317 // A modifier is pressed. Stop the timeout
318 m_modifierlessTimer.stop();
319 }
320}
321
322bool KKeySequenceRecorderPrivate::eventFilter(QObject *watched, QEvent *event)
323{
324 if (!m_isRecording) {
325 return QObject::eventFilter(watched, event);
326 }
327
328 if (event->type() == QEvent::ShortcutOverride || event->type() == QEvent::ContextMenu) {
329 event->accept();
330 return true;
331 }
332 if (event->type() == QEvent::KeyRelease) {
333 handleKeyRelease(static_cast<QKeyEvent *>(event));
334 return true;
335 }
336 if (event->type() == QEvent::KeyPress) {
337 handleKeyPress(static_cast<QKeyEvent *>(event));
338 return true;
339 }
340 return QObject::eventFilter(watched, event);
341}
342
343void KKeySequenceRecorderPrivate::handleKeyPress(QKeyEvent *event)
344{
345 m_currentModifiers = event->modifiers() & modifierMask;
346 int key = event->key();
347 switch (key) {
348 case -1:
349 qCWarning(KGUIADDONS_LOG) << "Got unknown key";
350 // Old behavior was to stop recording here instead of continuing like this
351 return;
352 case 0:
353 break;
354 case Qt::Key_AltGr:
355 // or else we get unicode salad
356 break;
357 case Qt::Key_Super_L:
358 case Qt::Key_Super_R:
359 // Qt doesn't properly recognize Super_L/Super_R as MetaModifier
360 m_currentModifiers |= Qt::MetaModifier;
361 Q_FALLTHROUGH();
362 case Qt::Key_Shift:
363 case Qt::Key_Control:
364 case Qt::Key_Alt:
365 case Qt::Key_Meta:
366 controlModifierlessTimeout();
367 Q_EMIT q->currentKeySequenceChanged();
368 break;
369 default:
370 if (m_currentKeySequence.count() == 0 && !(m_currentModifiers & ~Qt::ShiftModifier)) {
371 // It's the first key and no modifier pressed. Check if this is allowed
372 if (!(isOkWhenModifierless(key) || m_modifierlessAllowed)) {
373 // No it's not
374 return;
375 }
376 }
377
378 // We now have a valid key press.
379 if ((key == Qt::Key_Backtab) && (m_currentModifiers & Qt::ShiftModifier)) {
380 key = QKeyCombination(Qt::Key_Tab).toCombined() | m_currentModifiers;
381 } else if (isShiftAsModifierAllowed(key)) {
382 key |= m_currentModifiers;
383 } else {
384 key |= (m_currentModifiers & ~Qt::ShiftModifier);
385 }
386
387 m_currentKeySequence = appendToSequence(m_currentKeySequence, key);
388 Q_EMIT q->currentKeySequenceChanged();
389 // Now we are in a critical region (race), where recording is still
390 // ongoing, but key sequence has already changed (potentially) to the
391 // longest. But we still want currentKeySequenceChanged to trigger
392 // before gotKeySequence, so there's only so much we can do about it.
393 if ((!m_multiKeyShortcutsAllowed) || (m_currentKeySequence.count() == MaxKeyCount)) {
394 finishRecording();
395 break;
396 }
397 controlModifierlessTimeout();
398 }
399 event->accept();
400}
401
402void KKeySequenceRecorderPrivate::handleKeyRelease(QKeyEvent *event)
403{
404 Qt::KeyboardModifiers modifiers = event->modifiers() & modifierMask;
405
406 /* The modifier release event (e.g. Qt::Key_Shift) also has the modifier
407 flag set so we were interpreting the "Shift" press as "Shift + Shift".
408 This function makes it so we just take the key part but not the modifier
409 if we are doing this one alone. */
410 const auto justKey = [&](Qt::KeyboardModifiers modifier) {
411 modifiers &= ~modifier;
412 if (m_currentKeySequence.isEmpty() && m_modifierOnlyAllowed) {
413 m_currentKeySequence = appendToSequence(m_currentKeySequence, event->key());
414 }
415 };
416 switch (event->key()) {
417 case -1:
418 return;
419 case Qt::Key_Super_L:
420 case Qt::Key_Super_R:
421 case Qt::Key_Meta:
422 justKey(Qt::MetaModifier);
423 break;
424 case Qt::Key_Shift:
425 justKey(Qt::ShiftModifier);
426 break;
427 case Qt::Key_Control:
428 justKey(Qt::ControlModifier);
429 break;
430 case Qt::Key_Alt:
431 justKey(Qt::AltModifier);
432 break;
433 }
434
435 if ((modifiers & m_currentModifiers) < m_currentModifiers) {
436 m_currentModifiers = modifiers;
437 controlModifierlessTimeout();
438 Q_EMIT q->currentKeySequenceChanged();
439 }
440}
441
442void KKeySequenceRecorderPrivate::receivedRecording()
443{
444 m_modifierlessTimer.stop();
445 m_isRecording = false;
446 m_currentModifiers = Qt::NoModifier;
447 if (m_inhibition) {
448 m_inhibition->disableInhibition();
449 }
450 QObject::disconnect(KKeySequenceRecorderGlobal::self(), &KKeySequenceRecorderGlobal::sequenceRecordingStarted, q, &KKeySequenceRecorder::cancelRecording);
451 Q_EMIT q->recordingChanged();
452}
453
454void KKeySequenceRecorderPrivate::finishRecording()
455{
456 receivedRecording();
457 Q_EMIT q->gotKeySequence(m_currentKeySequence);
458}
459
461 : QObject(parent)
462 , d(new KKeySequenceRecorderPrivate(this))
463{
464 d->m_isRecording = false;
465 d->m_modifierlessAllowed = false;
466 d->m_multiKeyShortcutsAllowed = true;
467
468 setWindow(window);
469 connect(&d->m_modifierlessTimer, &QTimer::timeout, d.get(), &KKeySequenceRecorderPrivate::finishRecording);
470}
471
472KKeySequenceRecorder::~KKeySequenceRecorder() noexcept
473{
474 if (d->m_inhibition && d->m_inhibition->shortcutsAreInhibited()) {
475 d->m_inhibition->disableInhibition();
476 }
477}
478
480{
481 d->m_previousKeySequence = d->m_currentKeySequence;
482
483 KKeySequenceRecorderGlobal::self()->sequenceRecordingStarted();
484 connect(KKeySequenceRecorderGlobal::self(),
485 &KKeySequenceRecorderGlobal::sequenceRecordingStarted,
486 this,
489
490 if (!d->m_window) {
491 qCWarning(KGUIADDONS_LOG) << "Cannot record without a window";
492 return;
493 }
494 d->m_isRecording = true;
495 d->m_currentKeySequence = QKeySequence();
496 if (d->m_inhibition) {
497 d->m_inhibition->enableInhibition();
498 }
499 Q_EMIT recordingChanged();
500 Q_EMIT currentKeySequenceChanged();
501}
502
504{
505 setCurrentKeySequence(d->m_previousKeySequence);
506 d->receivedRecording();
507 Q_ASSERT(!isRecording());
508}
509
511{
512 return d->m_isRecording;
513}
514
516{
517 // We need a check for count() here because there's a race between the
518 // state of recording and a length of currentKeySequence.
519 if (d->m_isRecording && d->m_currentKeySequence.count() < KKeySequenceRecorderPrivate::MaxKeyCount) {
520 return appendToSequence(d->m_currentKeySequence, d->m_currentModifiers);
521 } else {
522 return d->m_currentKeySequence;
523 }
524}
525
526void KKeySequenceRecorder::setCurrentKeySequence(const QKeySequence &sequence)
527{
528 if (d->m_currentKeySequence == sequence) {
529 return;
530 }
531 d->m_currentKeySequence = sequence;
532 Q_EMIT currentKeySequenceChanged();
533}
534
536{
537 return d->m_window;
538}
539
540void KKeySequenceRecorder::setWindow(QWindow *window)
541{
542 if (window == d->m_window) {
543 return;
544 }
545
546 if (d->m_window) {
547 d->m_window->removeEventFilter(d.get());
548 }
549
550 if (window) {
551 window->installEventFilter(d.get());
552 qCDebug(KGUIADDONS_LOG) << "listening for events in" << window;
553 }
554
555 if (qGuiApp->platformName() == QLatin1String("wayland")) {
556#ifdef WITH_WAYLAND
557 d->m_inhibition.reset(new WaylandInhibition(window));
558#endif
559 } else {
560 d->m_inhibition.reset(new KeyboardGrabber(window));
561 }
562
563 d->m_window = window;
564
565 Q_EMIT windowChanged();
566}
567
569{
570 return d->m_multiKeyShortcutsAllowed;
571}
572
573void KKeySequenceRecorder::setMultiKeyShortcutsAllowed(bool allowed)
574{
575 if (allowed == d->m_multiKeyShortcutsAllowed) {
576 return;
577 }
578 d->m_multiKeyShortcutsAllowed = allowed;
579 Q_EMIT multiKeyShortcutsAllowedChanged();
580}
581
583{
584 return d->m_modifierlessAllowed;
585}
586
587void KKeySequenceRecorder::setModifierlessAllowed(bool allowed)
588{
589 if (allowed == d->m_modifierlessAllowed) {
590 return;
591 }
592 d->m_modifierlessAllowed = allowed;
593 Q_EMIT modifierlessAllowedChanged();
594}
595
597{
598 return d->m_modifierOnlyAllowed;
599}
600
601void KKeySequenceRecorder::setModifierOnlyAllowed(bool allowed)
602{
603 if (allowed == d->m_modifierOnlyAllowed) {
604 return;
605 }
606 d->m_modifierOnlyAllowed = allowed;
607 Q_EMIT modifierOnlyAllowedChanged();
608}
609
610#include "kkeysequencerecorder.moc"
611#include "moc_kkeysequencerecorder.cpp"
Record a QKeySequence by listening to key events in a window.
Q_INVOKABLE void startRecording()
Start recording.
bool multiKeyShortcutsAllowed
Controls the amount of key combinations that are captured until recording stops and gotKeySequence is...
void gotKeySequence(const QKeySequence &keySequence)
This signal is emitted when a key sequence has been recorded.
bool modifierOnlyAllowed
It makes it acceptable for the key sequence to be just a modifier (e.g.
KKeySequenceRecorder(QWindow *window, QObject *parent=nullptr)
Constructor.
QKeySequence currentKeySequence
The recorded key sequence.
bool isRecording
Whether key events are currently recorded.
QWindow * window
The window in which the key events are happening that should be recorded.
void cancelRecording()
Stops the recording session.
bool modifierlessAllowed
If key presses of "plain" keys without a modifier are considered to be a valid finished key combinati...
char * toString(const EngineQuery &query)
bool isShiftAsModifierAllowed(int keyQt)
bool isLetter() const const
ShortcutOverride
int toCombined() const const
int count() const const
bool isEmpty() const const
Q_EMITQ_EMIT
Q_OBJECTQ_OBJECT
Q_SIGNALSQ_SIGNALS
QMetaObject::Connection connect(const QObject *sender, PointerToMemberFunction signal, Functor functor)
bool disconnect(const QMetaObject::Connection &connection)
virtual bool event(QEvent *e)
virtual bool eventFilter(QObject *watched, QEvent *event)
void installEventFilter(QObject *filterObj)
UniqueConnection
typedef KeyboardModifiers
void start()
void stop()
void timeout()
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Tue Mar 26 2024 11:14:40 by doxygen 1.10.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.