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{
28 Q_OBJECT
29public:
30 static KKeySequenceRecorderGlobal *self()
31 {
32 static KKeySequenceRecorderGlobal s_self;
33 return &s_self;
34 }
35
36Q_SIGNALS:
37 void sequenceRecordingStarted();
38};
39
40class KKeySequenceRecorderPrivate : public QObject
41{
42 Q_OBJECT
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
56 KKeySequenceRecorder *q;
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
70constexpr Qt::KeyboardModifiers modifierMask = Qt::ShiftModifier | Qt::ControlModifier | Qt::AltModifier | Qt::MetaModifier | Qt::KeypadModifier;
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:
93 case Qt::Key_Backspace:
94 case Qt::Key_Tab:
95 case Qt::Key_Backtab:
96 case Qt::Key_Escape:
97 case Qt::Key_Print:
98 case Qt::Key_ScrollLock:
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:
119 case Qt::Key_Favorites:
120 case Qt::Key_LaunchMedia:
121 case Qt::Key_OpenUrl:
122 case Qt::Key_HomePage:
123 case Qt::Key_Search:
124 case Qt::Key_VolumeDown:
125 case Qt::Key_VolumeMute:
126 case Qt::Key_VolumeUp:
127 case Qt::Key_BassBoost:
128 case Qt::Key_BassUp:
129 case Qt::Key_BassDown:
130 case Qt::Key_TrebleUp:
131 case Qt::Key_TrebleDown:
132 case Qt::Key_MediaPlay:
133 case Qt::Key_MediaStop:
134 case Qt::Key_MediaPrevious:
135 case Qt::Key_MediaNext:
136 case Qt::Key_MediaRecord:
137 case Qt::Key_MediaPause:
138 case Qt::Key_MediaTogglePlayPause:
139 case Qt::Key_LaunchMail:
140 case Qt::Key_Calculator:
141 case Qt::Key_Memo:
142 case Qt::Key_ToDoList:
143 case Qt::Key_Calendar:
144 case Qt::Key_PowerDown:
145 case Qt::Key_ContrastAdjust:
146 case Qt::Key_Standby:
147 case Qt::Key_MonBrightnessUp:
148 case Qt::Key_MonBrightnessDown:
149 case Qt::Key_KeyboardLightOnOff:
150 case Qt::Key_KeyboardBrightnessUp:
151 case Qt::Key_KeyboardBrightnessDown:
152 case Qt::Key_PowerOff:
153 case Qt::Key_WakeUp:
154 case Qt::Key_Eject:
155 case Qt::Key_ScreenSaver:
156 case Qt::Key_WWW:
157 case Qt::Key_Sleep:
158 case Qt::Key_LightBulb:
159 case Qt::Key_Shop:
160 case Qt::Key_History:
161 case Qt::Key_AddFavorite:
162 case Qt::Key_HotLinks:
163 case Qt::Key_BrightnessAdjust:
164 case Qt::Key_Finance:
165 case Qt::Key_Community:
166 case Qt::Key_AudioRewind:
167 case Qt::Key_BackForward:
168 case Qt::Key_ApplicationLeft:
169 case Qt::Key_ApplicationRight:
170 case Qt::Key_Book:
171 case Qt::Key_CD:
172 case Qt::Key_Clear:
173 case Qt::Key_ClearGrab:
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:
179 case Qt::Key_Documents:
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:
192 case Qt::Key_OfficeHome:
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:
198 case Qt::Key_RotateWindows:
199 case Qt::Key_RotationPB:
200 case Qt::Key_RotationKB:
201 case Qt::Key_Save:
202 case Qt::Key_Send:
203 case Qt::Key_Spell:
204 case Qt::Key_SplitScreen:
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:
216 case Qt::Key_Messenger:
217 case Qt::Key_WebCam:
218 case Qt::Key_MailForward:
219 case Qt::Key_Pictures:
220 case Qt::Key_Music:
221 case Qt::Key_Battery:
222 case Qt::Key_Bluetooth:
223 case Qt::Key_WLAN:
224 case Qt::Key_UWB:
225 case Qt::Key_AudioForward:
226 case Qt::Key_AudioRepeat:
227 case Qt::Key_AudioRandomPlay:
228 case Qt::Key_Subtitle:
229 case Qt::Key_AudioCycleTrack:
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:
235 case Qt::Key_Hibernate:
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?
271 case Qt::Key_Backspace:
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 keys[sequence.count()] = key;
291 return QKeySequence(keys[0], keys[1], keys[2], keys[3]);
292}
293
294KKeySequenceRecorderPrivate::KKeySequenceRecorderPrivate(KKeySequenceRecorder *qq)
295 : QObject(qq)
296 , q(qq)
297{
298}
299
300void KKeySequenceRecorderPrivate::controlModifierlessTimeout()
301{
302 if (m_currentKeySequence != 0 && !m_currentModifiers) {
303 // No modifier key pressed currently. Start the timeout
304 m_modifierlessTimer.start(600);
305 } else {
306 // A modifier is pressed. Stop the timeout
307 m_modifierlessTimer.stop();
308 }
309}
310
311bool KKeySequenceRecorderPrivate::eventFilter(QObject *watched, QEvent *event)
312{
313 if (!m_isRecording) {
314 return QObject::eventFilter(watched, event);
315 }
316
317 if (event->type() == QEvent::ShortcutOverride || event->type() == QEvent::ContextMenu) {
318 event->accept();
319 return true;
320 }
321 if (event->type() == QEvent::KeyRelease) {
322 handleKeyRelease(static_cast<QKeyEvent *>(event));
323 return true;
324 }
325 if (event->type() == QEvent::KeyPress) {
326 handleKeyPress(static_cast<QKeyEvent *>(event));
327 return true;
328 }
329 return QObject::eventFilter(watched, event);
330}
331
332void KKeySequenceRecorderPrivate::handleKeyPress(QKeyEvent *event)
333{
334 m_currentModifiers = event->modifiers() & modifierMask;
335 int key = event->key();
336 switch (key) {
337 case -1:
338 qCWarning(KGUIADDONS_LOG) << "Got unknown key";
339 // Old behavior was to stop recording here instead of continuing like this
340 return;
341 case 0:
342 break;
343 case Qt::Key_AltGr:
344 // or else we get unicode salad
345 break;
346 case Qt::Key_Super_L:
347 case Qt::Key_Super_R:
348 // Qt doesn't properly recognize Super_L/Super_R as MetaModifier
349 m_currentModifiers |= Qt::MetaModifier;
350 Q_FALLTHROUGH();
351 case Qt::Key_Shift:
352 case Qt::Key_Control:
353 case Qt::Key_Alt:
354 case Qt::Key_Meta:
355 controlModifierlessTimeout();
356 Q_EMIT q->currentKeySequenceChanged();
357 break;
358 default:
359 if (m_currentKeySequence.count() == 0 && !(m_currentModifiers & ~Qt::ShiftModifier)) {
360 // It's the first key and no modifier pressed. Check if this is allowed
361 if (!(isOkWhenModifierless(key) || m_modifierlessAllowed)) {
362 // No it's not
363 return;
364 }
365 }
366
367 // We now have a valid key press.
368 if ((key == Qt::Key_Backtab) && (m_currentModifiers & Qt::ShiftModifier)) {
369 key = QKeyCombination(Qt::Key_Tab).toCombined() | m_currentModifiers;
370 } else if (isShiftAsModifierAllowed(keyQt: key)) {
371 key |= m_currentModifiers;
372 } else {
373 key |= (m_currentModifiers & ~Qt::ShiftModifier);
374 }
375
376 m_currentKeySequence = appendToSequence(m_currentKeySequence, key);
377 Q_EMIT q->currentKeySequenceChanged();
378 // Now we are in a critical region (race), where recording is still
379 // ongoing, but key sequence has already changed (potentially) to the
380 // longest. But we still want currentKeySequenceChanged to trigger
381 // before gotKeySequence, so there's only so much we can do about it.
382 if ((!m_multiKeyShortcutsAllowed) || (m_currentKeySequence.count() == MaxKeyCount)) {
383 finishRecording();
384 break;
385 }
386 controlModifierlessTimeout();
387 }
388 event->accept();
389}
390
391void KKeySequenceRecorderPrivate::handleKeyRelease(QKeyEvent *event)
392{
393 Qt::KeyboardModifiers modifiers = event->modifiers() & modifierMask;
394
395 /* The modifier release event (e.g. Qt::Key_Shift) also has the modifier
396 flag set so we were interpreting the "Shift" press as "Shift + Shift".
397 This function makes it so we just take the key part but not the modifier
398 if we are doing this one alone. */
399 const auto justKey = [&](Qt::KeyboardModifiers modifier) {
400 modifiers &= ~modifier;
401 if (m_currentKeySequence.isEmpty() && m_modifierOnlyAllowed) {
402 m_currentKeySequence = appendToSequence(m_currentKeySequence, event->key());
403 }
404 };
405 switch (event->key()) {
406 case -1:
407 return;
408 case Qt::Key_Super_L:
409 case Qt::Key_Super_R:
410 case Qt::Key_Meta:
411 justKey(Qt::MetaModifier);
412 break;
413 case Qt::Key_Shift:
414 justKey(Qt::ShiftModifier);
415 break;
416 case Qt::Key_Control:
417 justKey(Qt::ControlModifier);
418 break;
419 case Qt::Key_Alt:
420 justKey(Qt::AltModifier);
421 break;
422 }
423
424 if ((modifiers & m_currentModifiers) < m_currentModifiers) {
425 m_currentModifiers = modifiers;
426 controlModifierlessTimeout();
427 Q_EMIT q->currentKeySequenceChanged();
428 }
429}
430
431void KKeySequenceRecorderPrivate::receivedRecording()
432{
433 m_modifierlessTimer.stop();
434 m_isRecording = false;
435 m_currentModifiers = Qt::NoModifier;
436 if (m_inhibition) {
437 m_inhibition->disableInhibition();
438 }
439 QObject::disconnect(KKeySequenceRecorderGlobal::self(), &KKeySequenceRecorderGlobal::sequenceRecordingStarted, q, &KKeySequenceRecorder::cancelRecording);
440 Q_EMIT q->recordingChanged();
441}
442
443void KKeySequenceRecorderPrivate::finishRecording()
444{
445 receivedRecording();
446 Q_EMIT q->gotKeySequence(m_currentKeySequence);
447}
448
449KKeySequenceRecorder::KKeySequenceRecorder(QWindow *window, QObject *parent)
450 : QObject(parent)
451 , d(new KKeySequenceRecorderPrivate(this))
452{
453 d->m_isRecording = false;
454 d->m_modifierlessAllowed = false;
455 d->m_multiKeyShortcutsAllowed = true;
456
457 setWindow(window);
458 connect(&d->m_modifierlessTimer, &QTimer::timeout, d.get(), &KKeySequenceRecorderPrivate::finishRecording);
459}
460
461KKeySequenceRecorder::~KKeySequenceRecorder() noexcept
462{
463 if (d->m_inhibition && d->m_inhibition->shortcutsAreInhibited()) {
464 d->m_inhibition->disableInhibition();
465 }
466}
467
468void KKeySequenceRecorder::startRecording()
469{
470 d->m_previousKeySequence = d->m_currentKeySequence;
471
472 KKeySequenceRecorderGlobal::self()->sequenceRecordingStarted();
473 connect(KKeySequenceRecorderGlobal::self(),
474 &KKeySequenceRecorderGlobal::sequenceRecordingStarted,
475 this,
476 &KKeySequenceRecorder::cancelRecording,
477 Qt::UniqueConnection);
478
479 if (!d->m_window) {
480 qCWarning(KGUIADDONS_LOG) << "Cannot record without a window";
481 return;
482 }
483 d->m_isRecording = true;
484 d->m_currentKeySequence = QKeySequence();
485 if (d->m_inhibition) {
486 d->m_inhibition->enableInhibition();
487 }
488 Q_EMIT recordingChanged();
489 Q_EMIT currentKeySequenceChanged();
490}
491
492void KKeySequenceRecorder::cancelRecording()
493{
494 setCurrentKeySequence(d->m_previousKeySequence);
495 d->receivedRecording();
496 Q_ASSERT(!isRecording());
497}
498
499bool KKeySequenceRecorder::isRecording() const
500{
501 return d->m_isRecording;
502}
503
504QKeySequence KKeySequenceRecorder::currentKeySequence() const
505{
506 // We need a check for count() here because there's a race between the
507 // state of recording and a length of currentKeySequence.
508 if (d->m_isRecording && d->m_currentKeySequence.count() < KKeySequenceRecorderPrivate::MaxKeyCount) {
509 return appendToSequence(d->m_currentKeySequence, d->m_currentModifiers);
510 } else {
511 return d->m_currentKeySequence;
512 }
513}
514
515void KKeySequenceRecorder::setCurrentKeySequence(const QKeySequence &sequence)
516{
517 if (d->m_currentKeySequence == sequence) {
518 return;
519 }
520 d->m_currentKeySequence = sequence;
521 Q_EMIT currentKeySequenceChanged();
522}
523
524QWindow *KKeySequenceRecorder::window() const
525{
526 return d->m_window;
527}
528
529void KKeySequenceRecorder::setWindow(QWindow *window)
530{
531 if (window == d->m_window) {
532 return;
533 }
534
535 if (d->m_window) {
536 d->m_window->removeEventFilter(d.get());
537 }
538
539 if (window) {
540 window->installEventFilter(d.get());
541 qCDebug(KGUIADDONS_LOG) << "listening for events in" << window;
542 }
543
544 if (qGuiApp->platformName() == QLatin1String("wayland")) {
545#ifdef WITH_WAYLAND
546 d->m_inhibition.reset(p: new WaylandInhibition(window));
547#endif
548 } else {
549 d->m_inhibition.reset(p: new KeyboardGrabber(window));
550 }
551
552 d->m_window = window;
553
554 Q_EMIT windowChanged();
555}
556
557bool KKeySequenceRecorder::multiKeyShortcutsAllowed() const
558{
559 return d->m_multiKeyShortcutsAllowed;
560}
561
562void KKeySequenceRecorder::setMultiKeyShortcutsAllowed(bool allowed)
563{
564 if (allowed == d->m_multiKeyShortcutsAllowed) {
565 return;
566 }
567 d->m_multiKeyShortcutsAllowed = allowed;
568 Q_EMIT multiKeyShortcutsAllowedChanged();
569}
570
571bool KKeySequenceRecorder::modifierlessAllowed() const
572{
573 return d->m_modifierlessAllowed;
574}
575
576void KKeySequenceRecorder::setModifierlessAllowed(bool allowed)
577{
578 if (allowed == d->m_modifierlessAllowed) {
579 return;
580 }
581 d->m_modifierlessAllowed = allowed;
582 Q_EMIT modifierlessAllowedChanged();
583}
584
585bool KKeySequenceRecorder::modifierOnlyAllowed() const
586{
587 return d->m_modifierOnlyAllowed;
588}
589
590void KKeySequenceRecorder::setModifierOnlyAllowed(bool allowed)
591{
592 if (allowed == d->m_modifierOnlyAllowed) {
593 return;
594 }
595 d->m_modifierOnlyAllowed = allowed;
596 Q_EMIT modifierOnlyAllowedChanged();
597}
598
599#include "kkeysequencerecorder.moc"
600#include "moc_kkeysequencerecorder.cpp"
601

source code of kguiaddons/src/recorder/kkeysequencerecorder.cpp