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
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...
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
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 Sat Apr 27 2024 22:08:23 by doxygen 1.10.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.