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#include <chrono>
25
26/// Singleton whose only purpose is to tell us about other sequence recorders getting started
27class KKeySequenceRecorderGlobal : public QObject
28{
30public:
31 static KKeySequenceRecorderGlobal *self()
32 {
33 static KKeySequenceRecorderGlobal s_self;
34 return &s_self;
35 }
36
38 void sequenceRecordingStarted();
39};
40
41class KKeySequenceRecorderPrivate : public QObject
42{
44public:
45 // Copy of QKeySequencePrivate::MaxKeyCount from private header
46 enum { MaxKeyCount = 4 };
47
48 KKeySequenceRecorderPrivate(KKeySequenceRecorder *qq);
49
50 void controlModifierlessTimeout();
51 bool eventFilter(QObject *watched, QEvent *event) override;
52 void handleKeyPress(QKeyEvent *event);
53 void handleKeyRelease(QKeyEvent *event);
54 void finishRecording();
55 void receivedRecording();
56
57 KKeySequenceRecorder *q;
58 QKeySequence m_currentKeySequence;
59 QKeySequence m_previousKeySequence;
60 QPointer<QWindow> m_window;
61 bool m_isRecording;
62 bool m_multiKeyShortcutsAllowed;
63 bool m_modifierlessAllowed;
64 bool m_modifierOnlyAllowed = false;
65
66 Qt::KeyboardModifiers m_currentModifiers;
67 QTimer m_modifierlessTimer;
68 std::unique_ptr<ShortcutInhibition> m_inhibition;
69 // For use in modifier only shortcuts
70 Qt::KeyboardModifiers m_lastPressedModifiers;
71 bool m_isReleasingModifierOnly = false;
72 std::chrono::nanoseconds m_modifierFirstReleaseTime;
73};
74
76
77// Copied here from KKeyServer
78static bool isShiftAsModifierAllowed(int keyQt)
79{
80 // remove any modifiers
81 keyQt &= ~Qt::KeyboardModifierMask;
82
83 // Shift only works as a modifier with certain keys. It's not possible
84 // to enter the SHIFT+5 key sequence for me because this is handled as
85 // '%' by qt on my keyboard.
86 // The working keys are all hardcoded here :-(
87 if (keyQt >= Qt::Key_F1 && keyQt <= Qt::Key_F35) {
88 return true;
89 }
90
91 if (QChar::isLetter(keyQt)) {
92 return true;
93 }
94
95 switch (keyQt) {
96 case Qt::Key_Return:
97 case Qt::Key_Space:
99 case Qt::Key_Tab:
100 case Qt::Key_Backtab:
101 case Qt::Key_Escape:
102 case Qt::Key_Print:
104 case Qt::Key_Pause:
105 case Qt::Key_PageUp:
106 case Qt::Key_PageDown:
107 case Qt::Key_Insert:
108 case Qt::Key_Delete:
109 case Qt::Key_Home:
110 case Qt::Key_End:
111 case Qt::Key_Up:
112 case Qt::Key_Down:
113 case Qt::Key_Left:
114 case Qt::Key_Right:
115 case Qt::Key_Enter:
116 case Qt::Key_SysReq:
117 case Qt::Key_CapsLock:
118 case Qt::Key_NumLock:
119 case Qt::Key_Help:
120 case Qt::Key_Back:
121 case Qt::Key_Forward:
122 case Qt::Key_Stop:
123 case Qt::Key_Refresh:
126 case Qt::Key_OpenUrl:
127 case Qt::Key_HomePage:
128 case Qt::Key_Search:
131 case Qt::Key_VolumeUp:
133 case Qt::Key_BassUp:
134 case Qt::Key_BassDown:
135 case Qt::Key_TrebleUp:
146 case Qt::Key_Memo:
147 case Qt::Key_ToDoList:
148 case Qt::Key_Calendar:
151 case Qt::Key_Standby:
157 case Qt::Key_PowerOff:
158 case Qt::Key_WakeUp:
159 case Qt::Key_Eject:
161 case Qt::Key_WWW:
162 case Qt::Key_Sleep:
164 case Qt::Key_Shop:
165 case Qt::Key_History:
167 case Qt::Key_HotLinks:
169 case Qt::Key_Finance:
175 case Qt::Key_Book:
176 case Qt::Key_CD:
177 case Qt::Key_Clear:
179 case Qt::Key_Close:
180 case Qt::Key_Copy:
181 case Qt::Key_Cut:
182 case Qt::Key_Display:
183 case Qt::Key_DOS:
185 case Qt::Key_Excel:
186 case Qt::Key_Explorer:
187 case Qt::Key_Game:
188 case Qt::Key_Go:
189 case Qt::Key_iTouch:
190 case Qt::Key_LogOff:
191 case Qt::Key_Market:
192 case Qt::Key_Meeting:
193 case Qt::Key_MenuKB:
194 case Qt::Key_MenuPB:
195 case Qt::Key_MySites:
196 case Qt::Key_News:
198 case Qt::Key_Option:
199 case Qt::Key_Paste:
200 case Qt::Key_Phone:
201 case Qt::Key_Reply:
202 case Qt::Key_Reload:
206 case Qt::Key_Save:
207 case Qt::Key_Send:
208 case Qt::Key_Spell:
210 case Qt::Key_Support:
211 case Qt::Key_TaskPane:
212 case Qt::Key_Terminal:
213 case Qt::Key_Tools:
214 case Qt::Key_Travel:
215 case Qt::Key_Video:
216 case Qt::Key_Word:
217 case Qt::Key_Xfer:
218 case Qt::Key_ZoomIn:
219 case Qt::Key_ZoomOut:
220 case Qt::Key_Away:
222 case Qt::Key_WebCam:
224 case Qt::Key_Pictures:
225 case Qt::Key_Music:
226 case Qt::Key_Battery:
228 case Qt::Key_WLAN:
229 case Qt::Key_UWB:
233 case Qt::Key_Subtitle:
235 case Qt::Key_Time:
236 case Qt::Key_Select:
237 case Qt::Key_View:
238 case Qt::Key_TopMenu:
239 case Qt::Key_Suspend:
241 case Qt::Key_Launch0:
242 case Qt::Key_Launch1:
243 case Qt::Key_Launch2:
244 case Qt::Key_Launch3:
245 case Qt::Key_Launch4:
246 case Qt::Key_Launch5:
247 case Qt::Key_Launch6:
248 case Qt::Key_Launch7:
249 case Qt::Key_Launch8:
250 case Qt::Key_Launch9:
251 case Qt::Key_LaunchA:
252 case Qt::Key_LaunchB:
253 case Qt::Key_LaunchC:
254 case Qt::Key_LaunchD:
255 case Qt::Key_LaunchE:
256 case Qt::Key_LaunchF:
257 case Qt::Key_Shift:
258 case Qt::Key_Control:
259 case Qt::Key_Meta:
260 case Qt::Key_Alt:
261 case Qt::Key_Super_L:
262 case Qt::Key_Super_R:
263 return true;
264
265 default:
266 return false;
267 }
268}
269
270static bool isOkWhenModifierless(int key)
271{
272 // this whole function is a hack, but especially the first line of code
273 if (QKeySequence(key).toString().length() == 1) {
274 return false;
275 }
276
277 switch (key) {
278 case Qt::Key_Return:
279 case Qt::Key_Space:
280 case Qt::Key_Tab:
281 case Qt::Key_Backtab: // does this ever happen?
283 case Qt::Key_Delete:
284 return false;
285 default:
286 return true;
287 }
288}
289
290static QKeySequence appendToSequence(const QKeySequence &sequence, int key)
291{
292 if (sequence.count() >= KKeySequenceRecorderPrivate::MaxKeyCount) {
293 qCWarning(KGUIADDONS_LOG) << "Cannot append to a key to a sequence which is already of length" << sequence.count();
294 return sequence;
295 }
296
297 std::array<int, KKeySequenceRecorderPrivate::MaxKeyCount> keys{sequence[0].toCombined(),
298 sequence[1].toCombined(),
299 sequence[2].toCombined(),
300 sequence[3].toCombined()};
301 // When the user presses Mod(s)+Alt+Print, the SysReq event is fired only
302 // when the Alt key is released. Before we get the Mod(s)+SysReq event, we
303 // first get a Mod(s)+Alt event, which we have to ignore.
304 // Known limitation: only works when the Alt key is released before the Mod(s) key(s).
307 if (sequence.count() > 0 && (sequence[sequence.count() - 1].toCombined() & ~Qt::KeyboardModifierMask) == Qt::Key_Alt) {
308 keys[sequence.count() - 1] = key;
309 return QKeySequence(keys[0], keys[1], keys[2], keys[3]);
310 }
311 }
312 keys[sequence.count()] = key;
313 return QKeySequence(keys[0], keys[1], keys[2], keys[3]);
314}
315
316KKeySequenceRecorderPrivate::KKeySequenceRecorderPrivate(KKeySequenceRecorder *qq)
317 : QObject(qq)
318 , q(qq)
319{
320}
321
322void KKeySequenceRecorderPrivate::controlModifierlessTimeout()
323{
324 if (m_currentKeySequence != 0 && !m_currentModifiers) {
325 // No modifier key pressed currently. Start the timeout
326 m_modifierlessTimer.start(600);
327 } else {
328 // A modifier is pressed. Stop the timeout
329 m_modifierlessTimer.stop();
330 }
331}
332
333bool KKeySequenceRecorderPrivate::eventFilter(QObject *watched, QEvent *event)
334{
335 if (!m_isRecording) {
336 return QObject::eventFilter(watched, event);
337 }
338
339 if (event->type() == QEvent::ShortcutOverride || event->type() == QEvent::ContextMenu) {
340 event->accept();
341 return true;
342 }
343 if (event->type() == QEvent::KeyRelease) {
344 handleKeyRelease(static_cast<QKeyEvent *>(event));
345 return true;
346 }
347 if (event->type() == QEvent::KeyPress) {
348 handleKeyPress(static_cast<QKeyEvent *>(event));
349 return true;
350 }
351 return QObject::eventFilter(watched, event);
352}
353
354static Qt::KeyboardModifiers keyToModifier(int key)
355{
356 switch (key) {
357 case Qt::Key_Meta:
358 case Qt::Key_Super_L:
359 case Qt::Key_Super_R:
360 // Qt doesn't properly recognize Super_L/Super_R as MetaModifier
361 return Qt::MetaModifier;
362 case Qt::Key_Shift:
363 return Qt::ShiftModifier;
364 case Qt::Key_Control:
365 return Qt::ControlModifier;
366 case Qt::Key_Alt:
367 return Qt::AltModifier;
368 default:
369 return Qt::NoModifier;
370 }
371}
372
373void KKeySequenceRecorderPrivate::handleKeyPress(QKeyEvent *event)
374{
375 m_isReleasingModifierOnly = false;
376 m_currentModifiers = event->modifiers() & modifierMask;
377 int key = event->key();
378 switch (key) {
379 case -1:
380 qCWarning(KGUIADDONS_LOG) << "Got unknown key";
381 // Old behavior was to stop recording here instead of continuing like this
382 return;
383 case 0:
384 break;
385 case Qt::Key_AltGr:
386 // or else we get unicode salad
387 break;
388 case Qt::Key_Super_L:
389 case Qt::Key_Super_R:
390 case Qt::Key_Shift:
391 case Qt::Key_Control:
392 case Qt::Key_Alt:
393 case Qt::Key_Meta:
394 m_currentModifiers |= keyToModifier(key);
395 m_lastPressedModifiers = m_currentModifiers;
396 controlModifierlessTimeout();
397 Q_EMIT q->currentKeySequenceChanged();
398 break;
399 default:
400 m_lastPressedModifiers = Qt::NoModifier;
401 if (m_currentKeySequence.count() == 0 && !(m_currentModifiers & ~Qt::ShiftModifier)) {
402 // It's the first key and no modifier pressed. Check if this is allowed
403 if (!(isOkWhenModifierless(key) || m_modifierlessAllowed)) {
404 // No it's not
405 return;
406 }
407 }
408
409 // We now have a valid key press.
410 if ((key == Qt::Key_Backtab) && (m_currentModifiers & Qt::ShiftModifier)) {
411 key = QKeyCombination(Qt::Key_Tab).toCombined() | m_currentModifiers;
412 } else if (isShiftAsModifierAllowed(key)) {
413 key |= m_currentModifiers;
414 } else {
415 key |= (m_currentModifiers & ~Qt::ShiftModifier);
416 }
417
418 m_currentKeySequence = appendToSequence(m_currentKeySequence, key);
419 Q_EMIT q->currentKeySequenceChanged();
420 // Now we are in a critical region (race), where recording is still
421 // ongoing, but key sequence has already changed (potentially) to the
422 // longest. But we still want currentKeySequenceChanged to trigger
423 // before gotKeySequence, so there's only so much we can do about it.
424 if ((!m_multiKeyShortcutsAllowed) || (m_currentKeySequence.count() == MaxKeyCount)) {
425 finishRecording();
426 break;
427 }
428 controlModifierlessTimeout();
429 }
430 event->accept();
431}
432
433// Turn a bunch of modifiers into mods + key
434// so that the ordering is always Meta + Ctrl + Alt + Shift
435static int prettifyModifierOnly(Qt::KeyboardModifiers modifier)
436{
437 if (modifier & Qt::ShiftModifier) {
438 return (Qt::Key_Shift | (modifier & ~Qt::ShiftModifier)).toCombined();
439 } else if (modifier & Qt::AltModifier) {
440 return (Qt::Key_Alt | (modifier & ~Qt::AltModifier)).toCombined();
441 } else if (modifier & Qt::ControlModifier) {
442 return (Qt::Key_Control | (modifier & ~Qt::ControlModifier)).toCombined();
443 } else if (modifier & Qt::MetaModifier) {
444 return (Qt::Key_Meta | (modifier & ~Qt::MetaModifier)).toCombined();
445 } else {
446 return Qt::Key(0);
447 }
448}
449
450void KKeySequenceRecorderPrivate::handleKeyRelease(QKeyEvent *event)
451{
452 Qt::KeyboardModifiers modifiers = event->modifiers() & modifierMask;
453
454 switch (event->key()) {
455 case -1:
456 return;
457 case Qt::Key_Super_L:
458 case Qt::Key_Super_R:
459 case Qt::Key_Meta:
460 case Qt::Key_Shift:
461 case Qt::Key_Control:
462 case Qt::Key_Alt:
463 modifiers &= ~keyToModifier(event->key());
464 }
465 if ((modifiers & m_currentModifiers) < m_currentModifiers) {
466 constexpr auto releaseTimeout = std::chrono::milliseconds(200);
467 const auto currentTime = std::chrono::steady_clock::now().time_since_epoch();
468 if (!m_isReleasingModifierOnly) {
469 m_isReleasingModifierOnly = true;
470 m_modifierFirstReleaseTime = currentTime;
471 }
472 if (m_modifierOnlyAllowed && !modifiers && (currentTime - m_modifierFirstReleaseTime) < releaseTimeout) {
473 m_currentKeySequence = appendToSequence(m_currentKeySequence, prettifyModifierOnly(m_lastPressedModifiers));
474 m_lastPressedModifiers = Qt::NoModifier;
475 }
476 m_currentModifiers = modifiers;
477 Q_EMIT q->currentKeySequenceChanged();
478 if (m_currentKeySequence.count() == (m_multiKeyShortcutsAllowed ? MaxKeyCount : 1)) {
479 finishRecording();
480 }
481 controlModifierlessTimeout();
482 };
483}
484
485void KKeySequenceRecorderPrivate::receivedRecording()
486{
487 m_modifierlessTimer.stop();
488 m_isRecording = false;
489 m_currentModifiers = Qt::NoModifier;
490 m_lastPressedModifiers = Qt::NoModifier;
491 m_isReleasingModifierOnly = false;
492 if (m_inhibition) {
493 m_inhibition->disableInhibition();
494 }
495 QObject::disconnect(KKeySequenceRecorderGlobal::self(), &KKeySequenceRecorderGlobal::sequenceRecordingStarted, q, &KKeySequenceRecorder::cancelRecording);
496 Q_EMIT q->recordingChanged();
497}
498
499void KKeySequenceRecorderPrivate::finishRecording()
500{
501 receivedRecording();
502 Q_EMIT q->gotKeySequence(m_currentKeySequence);
503}
504
506 : QObject(parent)
507 , d(new KKeySequenceRecorderPrivate(this))
508{
509 d->m_isRecording = false;
510 d->m_modifierlessAllowed = false;
511 d->m_multiKeyShortcutsAllowed = true;
512
513 setWindow(window);
514 connect(&d->m_modifierlessTimer, &QTimer::timeout, d.get(), &KKeySequenceRecorderPrivate::finishRecording);
515}
516
517KKeySequenceRecorder::~KKeySequenceRecorder() noexcept
518{
519 if (d->m_inhibition && d->m_inhibition->shortcutsAreInhibited()) {
520 d->m_inhibition->disableInhibition();
521 }
522}
523
525{
526 d->m_previousKeySequence = d->m_currentKeySequence;
527
528 KKeySequenceRecorderGlobal::self()->sequenceRecordingStarted();
529 connect(KKeySequenceRecorderGlobal::self(),
530 &KKeySequenceRecorderGlobal::sequenceRecordingStarted,
531 this,
534
535 if (!d->m_window) {
536 qCWarning(KGUIADDONS_LOG) << "Cannot record without a window";
537 return;
538 }
539 d->m_isRecording = true;
540 d->m_currentKeySequence = QKeySequence();
541 if (d->m_inhibition) {
542 d->m_inhibition->enableInhibition();
543 }
544 Q_EMIT recordingChanged();
545 Q_EMIT currentKeySequenceChanged();
546}
547
549{
550 setCurrentKeySequence(d->m_previousKeySequence);
551 d->receivedRecording();
552 Q_ASSERT(!isRecording());
553}
554
556{
557 return d->m_isRecording;
558}
559
561{
562 // We need a check for count() here because there's a race between the
563 // state of recording and a length of currentKeySequence.
564 if (d->m_isRecording && d->m_currentKeySequence.count() < KKeySequenceRecorderPrivate::MaxKeyCount) {
565 return appendToSequence(d->m_currentKeySequence, d->m_currentModifiers);
566 } else {
567 return d->m_currentKeySequence;
568 }
569}
570
571void KKeySequenceRecorder::setCurrentKeySequence(const QKeySequence &sequence)
572{
573 if (d->m_currentKeySequence == sequence) {
574 return;
575 }
576 d->m_currentKeySequence = sequence;
577 Q_EMIT currentKeySequenceChanged();
578}
579
581{
582 return d->m_window;
583}
584
585void KKeySequenceRecorder::setWindow(QWindow *window)
586{
587 if (window == d->m_window) {
588 return;
589 }
590
591 if (d->m_window) {
592 d->m_window->removeEventFilter(d.get());
593 }
594
595 if (window) {
596 window->installEventFilter(d.get());
597 qCDebug(KGUIADDONS_LOG) << "listening for events in" << window;
598 }
599
600 if (qGuiApp->platformName() == QLatin1String("wayland")) {
601#ifdef WITH_WAYLAND
602 d->m_inhibition.reset(new WaylandInhibition(window));
603#endif
604 } else {
605 d->m_inhibition.reset(new KeyboardGrabber(window));
606 }
607
608 d->m_window = window;
609
610 Q_EMIT windowChanged();
611}
612
614{
615 return d->m_multiKeyShortcutsAllowed;
616}
617
618void KKeySequenceRecorder::setMultiKeyShortcutsAllowed(bool allowed)
619{
620 if (allowed == d->m_multiKeyShortcutsAllowed) {
621 return;
622 }
623 d->m_multiKeyShortcutsAllowed = allowed;
624 Q_EMIT multiKeyShortcutsAllowedChanged();
625}
626
628{
629 return d->m_modifierlessAllowed;
630}
631
632void KKeySequenceRecorder::setModifierlessAllowed(bool allowed)
633{
634 if (allowed == d->m_modifierlessAllowed) {
635 return;
636 }
637 d->m_modifierlessAllowed = allowed;
638 Q_EMIT modifierlessAllowedChanged();
639}
640
642{
643 return d->m_modifierOnlyAllowed;
644}
645
646void KKeySequenceRecorder::setModifierOnlyAllowed(bool allowed)
647{
648 if (allowed == d->m_modifierOnlyAllowed) {
649 return;
650 }
651 d->m_modifierOnlyAllowed = allowed;
652 Q_EMIT modifierOnlyAllowedChanged();
653}
654
655#include "kkeysequencerecorder.moc"
656#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...
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(char32_t ucs4)
ShortcutOverride
int count() const const
QObject(QObject *parent)
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)
QObject * parent() const const
UniqueConnection
typedef KeyboardModifiers
void timeout()
This file is part of the KDE documentation.
Documentation copyright © 1996-2025 The KDE developers.
Generated on Fri Jan 24 2025 11:52:27 by doxygen 1.13.2 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.