on xenial help sign in
Prevent alt key presses from being dispatched to the app on alt+tab Fixes: #1532862

Approved by: Lukáš Tinkl

commit0882beac70993e835641919bfc5b3846cb0519ea authorMichael Zanetti <michael.zanetti@canonical.com> dateFri 29 Jan 2016, 11:52 (11 years ago) parent0b1066176a95be
9 files changed+249−65
plugins/Utils/CMakeLists.txt+1 plugins/Utils/inputeventgenerator.cpp+34 plugins/Utils/inputeventgenerator.h+40 plugins/Utils/plugin.cpp+2 qml/Components/PhysicalKeysMapper.qml+32−2 tests/mocks/Utils/CMakeLists.txt+1 tests/mocks/Utils/plugin.cpp+2 tests/qmltests/CMakeLists.txt+1−1 tests/qmltests/Components/tst_PhysicalKeysMapper.qml+136−62
plugins/Utils/CMakeLists.txt+1
@@ -24,6 +24,7 @@ set(QMLPLUGIN_SRC
2424 easingcurve.cpp
2525 windowstatestorage.cpp
2626 timezoneFormatter.cpp
27+ inputeventgenerator.cpp
2728 plugin.cpp
2829 )
2930
plugins/Utils/inputeventgenerator.cppnew file+34
@@ -0,0 +1,34 @@
1+/*
2+ * Copyright (C) 2016 Canonical, Ltd.
3+ *
4+ * This program is free software; you can redistribute it and/or modify
5+ * it under the terms of the GNU General Public License as published by
6+ * the Free Software Foundation; version 3.
7+ *
8+ * This program is distributed in the hope that it will be useful,
9+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
10+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+ * GNU General Public License for more details.
12+ *
13+ * You should have received a copy of the GNU General Public License
14+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
15+ */
16+
17+#include "inputeventgenerator.h"
18+
19+#include <QQuickWindow>
20+#include <QDebug>
21+#include <QDateTime>
22+
23+InputEventGenerator::InputEventGenerator(QQuickItem *parent)
24+ : QQuickItem(parent)
25+{
26+}
27+
28+void InputEventGenerator::generateKeyEvent(Qt::Key key, bool pressed, Qt::KeyboardModifiers modifiers, quint64 timestamp, quint32 nativeScanCode, const QString &text)
29+{
30+ QEvent::Type type = pressed ? QEvent::KeyPress : QEvent::KeyRelease;
31+ QKeyEvent ev(type, key, modifiers, nativeScanCode, 0, 0, text);
32+ ev.setTimestamp(timestamp);
33+ QCoreApplication::sendEvent(window(), &ev);
34+}
plugins/Utils/inputeventgenerator.hnew file+40
@@ -0,0 +1,40 @@
1+/*
2+ * Copyright (C) 2016 Canonical, Ltd.
3+ *
4+ * This program is free software; you can redistribute it and/or modify
5+ * it under the terms of the GNU General Public License as published by
6+ * the Free Software Foundation; version 3.
7+ *
8+ * This program is distributed in the hope that it will be useful,
9+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
10+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+ * GNU General Public License for more details.
12+ *
13+ * You should have received a copy of the GNU General Public License
14+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
15+ */
16+
17+#ifndef INPUTEVENTGENERATOR_H
18+#define INPUTEVENTGENERATOR_H
19+
20+#include <QQuickItem>
21+#include <QPointer>
22+#include <QDateTime>
23+
24+/**
25+ * \brief This class allows injecting Key events into the scene it lives in.
26+ */
27+class InputEventGenerator : public QQuickItem
28+{
29+ Q_OBJECT
30+public:
31+ InputEventGenerator(QQuickItem *parent = 0);
32+
33+ /**
34+ * Generate and post and event to the scene. The key event will be sent to the scene the Generator
35+ * lives in and it will be dispatched through the regular event/focus queue.
36+ */
37+ Q_INVOKABLE void generateKeyEvent(Qt::Key key, bool pressed, Qt::KeyboardModifiers modifiers = Qt::NoModifier, quint64 timestamp = QDateTime::currentMSecsSinceEpoch(), quint32 nativeScanCode = 0, const QString &text = QString());
38+};
39+
40+#endif // INPUTEVENTGENERATOR_H
plugins/Utils/plugin.cpp+2
@@ -36,6 +36,7 @@
3636#include "constants.h"
3737#include "timezoneFormatter.h"
3838#include "applicationsfiltermodel.h"
39+#include "inputeventgenerator.h"
3940
4041static QObject *createWindowStateStorage(QQmlEngine *engine, QJSEngine *scriptEngine)
4142{
@@ -68,6 +69,7 @@ void UtilsPlugin::registerTypes(const char *uri)
6869 [](QQmlEngine*, QJSEngine*) -> QObject* { return new TimezoneFormatter; });
6970 qmlRegisterType<ActiveFocusLogger>(uri, 0, 1, "ActiveFocusLogger");
7071 qmlRegisterType<ApplicationsFilterModel>(uri, 0, 1, "ApplicationsFilterModel");
72+ qmlRegisterType<InputEventGenerator>(uri, 0, 1, "InputEventGenerator");
7173}
7274
7375void UtilsPlugin::initializeEngine(QQmlEngine *engine, const char *uri)
qml/Components/PhysicalKeysMapper.qml+32−2
@@ -16,6 +16,7 @@
1616
1717import QtQuick 2.4
1818import Powerd 0.1
19+import Utils 0.1
1920
2021/*!
2122 \brief A mapper for the physical keys on the device
@@ -59,9 +60,24 @@ Item {
5960 property bool altTabPressed: false
6061
6162 property var powerButtonPressStart: 0
63+
64+ // We need to eat ALT presses until we know what they're for (Alt+Tab or going to the app?)
65+ // Once we know if an ALT keypress is for the app, we need to re-inject the pressed event for it
66+ // but we must only do that once.
67+ property bool altPressInjected: false
68+ }
69+
70+ InputEventGenerator {
71+ id: inputEventGenerator
6272 }
6373
6474 function onKeyPressed(event, currentEventTimestamp) {
75+ if (d.altPressed && !d.altTabPressed && event.key !== Qt.Key_Tab && event.key !== Qt.Key_Alt && !d.altPressInjected) {
76+ // ALT is pressed and another key that is not Tab has been received. Re-inject the alt pressed event
77+ d.altPressInjected = true;
78+ inputEventGenerator.generateKeyEvent(Qt.Key_Alt, true, Qt.NoModifier, currentEventTimestamp - 1, 56);
79+ }
80+
6581 if (event.key == Qt.Key_PowerDown || event.key == Qt.Key_PowerOff) {
6682 if (event.isAutoRepeat) {
6783 if (d.powerButtonPressStart > 0
@@ -97,7 +113,12 @@ Item {
97113 d.volumeUpKeyPressed = true;
98114 }
99115 } else if (event.key == Qt.Key_Alt || (root.controlInsteadOfAlt && event.key == Qt.Key_Control)) {
100− d.altPressed = true;
116+ if (!d.altPressed || event.isAutoRepeat) {
117+ // Only eat it if it's the first time we receive alt pressed (or if it's the autorepeat of the first press)
118+ d.altPressed = true;
119+ event.accepted = true;
120+ d.altPressInjected = false;
121+ }
101122 } else if (event.key == Qt.Key_Tab) {
102123 if (d.altPressed && !d.altTabPressed) {
103124 d.altTabPressed = true;
@@ -119,10 +140,19 @@ Item {
119140 d.volumeUpKeyPressed = false;
120141 if (!d.volumeDownKeyPressed) d.ignoreVolumeEvents = false;
121142 } else if (event.key == Qt.Key_Alt || (root.controlInsteadOfAlt && event.key == Qt.Key_Control)) {
122− d.altPressed = false;
123143 if (d.altTabPressed) {
124144 d.altTabPressed = false;
125145 event.accepted = true;
146+ } else if (d.altPressed && !d.altPressInjected) {
147+ // Alt was released but nothing else. Let's inject a pressed event and also forward the release.
148+ d.altPressInjected = true;
149+ inputEventGenerator.generateKeyEvent(Qt.Key_Alt, true, Qt.AltModifer, currentEventTimestamp, 56);
150+ d.altPressInjected = false;
151+ }
152+ d.altPressed = false;
153+ } else if (event.key == Qt.Key_Tab) {
154+ if (d.altTabPressed) {
155+ event.accepted = true;
126156 }
127157 }
128158 }
tests/mocks/Utils/CMakeLists.txt+1
@@ -19,6 +19,7 @@ set(QMLPLUGIN_SRC
1919 ${CMAKE_SOURCE_DIR}/plugins/Utils/easingcurve.cpp
2020 ${CMAKE_SOURCE_DIR}/plugins/Utils/inputwatcher.cpp
2121 ${CMAKE_SOURCE_DIR}/plugins/Utils/applicationsfiltermodel.cpp
22+ ${CMAKE_SOURCE_DIR}/plugins/Utils/inputeventgenerator.cpp
2223 ${APPLICATION_API_INCLUDEDIR}/unity/shell/application/ApplicationManagerInterface.h
2324 constants.cpp
2425 plugin.cpp
tests/mocks/Utils/plugin.cpp+2
@@ -36,6 +36,7 @@
3636#include <windowscreenshotprovider.h>
3737#include <easingcurve.h>
3838#include <applicationsfiltermodel.h>
39+#include <inputeventgenerator.h>
3940
4041static QObject *createWindowStateStorage(QQmlEngine *engine, QJSEngine *scriptEngine)
4142{
@@ -65,6 +66,7 @@ void FakeUtilsPlugin::registerTypes(const char *uri)
6566 qmlRegisterSingletonType<Constants>(uri, 0, 1, "Constants", createConstants);
6667 qmlRegisterType<ActiveFocusLogger>(uri, 0, 1, "ActiveFocusLogger");
6768 qmlRegisterType<ApplicationsFilterModel>(uri, 0, 1, "ApplicationsFilterModel");
69+ qmlRegisterType<InputEventGenerator>(uri, 0, 1, "InputEventGenerator");
6870}
6971
7072void FakeUtilsPlugin::initializeEngine(QQmlEngine *engine, const char *uri)
tests/qmltests/CMakeLists.txt+1−1
@@ -12,7 +12,7 @@ add_unity8_qmltest(Components DraggingArea)
1212add_unity8_qmltest(Components LazyImage)
1313add_unity8_qmltest(Components Lockscreen)
1414add_unity8_qmltest(Components ModeSwitchWarningDialog)
15−add_unity8_qmlunittest(Components PhysicalKeysMapper)
15+add_unity8_qmltest(Components PhysicalKeysMapper)
1616add_unity8_qmltest(Components Rating)
1717add_unity8_qmltest(Components ResponsiveGridView)
1818add_unity8_qmltest(Components ResponsiveVerticalJournal)
tests/qmltests/Components/tst_PhysicalKeysMapper.qml+136−62
@@ -18,12 +18,19 @@ import QtQuick 2.4
1818import QtTest 1.0
1919import "../../../qml/Components"
2020import Powerd 0.1
21+import Utils 0.1
2122
22−TestCase {
23− name: "PhysicalKeysMapper"
23+Item {
24+ width: 100
25+ height: 100
2426
2527 property var physicalKeysMapper: loader.item
2628
29+ WindowKeysFilter {
30+ Keys.onPressed: physicalKeysMapper.onKeyPressed(event, currentEventTimestamp);
31+ Keys.onReleased: physicalKeysMapper.onKeyReleased(event, currentEventTimestamp);
32+ }
33+
2734 Loader {
2835 // Using a Loader here to make sure mapper state is coherent
2936 // regardless of unmatched KeyPress and KeyRelease events
@@ -32,6 +39,21 @@ TestCase {
3239 sourceComponent: PhysicalKeysMapper { }
3340 }
3441
42+ Item {
43+ id: inputCatcher
44+ focus: true
45+
46+ property var pressedKeys: []
47+ property var releasedKeys: []
48+
49+ Keys.onPressed: {
50+ inputCatcher.pressedKeys.push(event.key);
51+ }
52+ Keys.onReleased: {
53+ inputCatcher.releasedKeys.push(event.key);
54+ }
55+ }
56+
3557 SignalSpy {
3658 id: powerSpy
3759 target: physicalKeysMapper
@@ -44,65 +66,117 @@ TestCase {
4466 signalName: "screenshotTriggered"
4567 }
4668
47− function init() {
48− Powerd.setStatus(Powerd.On, Powerd.Unknown);
49− loader.active = true;
50− tryCompare(loader.status == Loader.Ready);
51− }
52−
53− function cleanup() {
54− loader.active = false;
55− powerSpy.clear();
56− screenshotSpy.clear();
57− }
58−
59− function test_powerKeyLongPressed_data() {
60− return [
61− { tag: "screenOn", status: Powerd.On },
62− { tag: "screenOff", status: Powerd.Off },
63− ];
64− }
65−
66− function test_powerKeyLongPressed(data) {
67− Powerd.setStatus(data.status, Powerd.Unknown);
68− physicalKeysMapper.powerKeyLongPressTime = 500;
69−
70− physicalKeysMapper.onKeyPressed({ key: Qt.Key_PowerDown, isAutoRepeat: false }, 100);
71−
72− // After the first key press the screen is always on
73− // and the rest of keypresses are auto repeat
74− Powerd.setStatus(Powerd.On, Powerd.Unknown);
75−
76− physicalKeysMapper.onKeyPressed({ key: Qt.Key_PowerDown, isAutoRepeat: true}, 300);
77− physicalKeysMapper.onKeyPressed({ key: Qt.Key_PowerDown, isAutoRepeat: true}, 400);
78− physicalKeysMapper.onKeyPressed({ key: Qt.Key_PowerDown, isAutoRepeat: true}, 500);
79− physicalKeysMapper.onKeyPressed({ key: Qt.Key_PowerDown, isAutoRepeat: true}, 599);
80−
81− // powerKeyLongPressed should not have been emitted yet.
82− compare(powerSpy.count, 0);
83−
84− physicalKeysMapper.onKeyPressed({ key: Qt.Key_PowerDown, isAutoRepeat: true}, 600);
85−
86− compare(powerSpy.count, 1);
87−
88− // Confirm we only emit once
89− physicalKeysMapper.onKeyPressed({ key: Qt.Key_PowerDown, isAutoRepeat: true}, 601);
90− compare(powerSpy.count, 1);
91− }
92−
93− function test_screenshotButtons_data() {
94− return [
95− { tag: "UpFirst", first: Qt.Key_VolumeUp, second: Qt.Key_VolumeDown },
96− { tag: "DownFirst", first: Qt.Key_VolumeDown, second: Qt.Key_VolumeUp },
97− ];
98− }
99−
100− function test_screenshotButtons(data) {
101− physicalKeysMapper.onKeyPressed({ key: data.first });
102− physicalKeysMapper.onKeyPressed({ key: data.second });
103− screenshotSpy.wait();
104− physicalKeysMapper.onKeyReleased({ key: data.first });
105− physicalKeysMapper.onKeyReleased({ key: data.second });
106− compare(screenshotSpy.count, 1);
69+ TestCase {
70+ name: "PhysicalKeysMapper"
71+ when: windowShown
72+
73+ function init() {
74+ Powerd.setStatus(Powerd.On, Powerd.Unknown);
75+ loader.active = true;
76+ tryCompare(loader.status == Loader.Ready);
77+ inputCatcher.pressedKeys = [];
78+ inputCatcher.releasedKeys = [];
79+ }
80+
81+ function cleanup() {
82+ loader.active = false;
83+ powerSpy.clear();
84+ screenshotSpy.clear();
85+ }
86+
87+ function test_powerKeyLongPressed_data() {
88+ return [
89+ { tag: "screenOn", status: Powerd.On },
90+ { tag: "screenOff", status: Powerd.Off },
91+ ];
92+ }
93+
94+ function test_powerKeyLongPressed(data) {
95+ Powerd.setStatus(data.status, Powerd.Unknown);
96+ physicalKeysMapper.powerKeyLongPressTime = 500;
97+
98+ physicalKeysMapper.onKeyPressed({ key: Qt.Key_PowerDown, isAutoRepeat: false }, 100);
99+
100+ // After the first key press the screen is always on
101+ // and the rest of keypresses are auto repeat
102+ Powerd.setStatus(Powerd.On, Powerd.Unknown);
103+
104+ physicalKeysMapper.onKeyPressed({ key: Qt.Key_PowerDown, isAutoRepeat: true}, 300);
105+ physicalKeysMapper.onKeyPressed({ key: Qt.Key_PowerDown, isAutoRepeat: true}, 400);
106+ physicalKeysMapper.onKeyPressed({ key: Qt.Key_PowerDown, isAutoRepeat: true}, 500);
107+ physicalKeysMapper.onKeyPressed({ key: Qt.Key_PowerDown, isAutoRepeat: true}, 599);
108+
109+ // powerKeyLongPressed should not have been emitted yet.
110+ compare(powerSpy.count, 0);
111+
112+ physicalKeysMapper.onKeyPressed({ key: Qt.Key_PowerDown, isAutoRepeat: true}, 600);
113+
114+ compare(powerSpy.count, 1);
115+
116+ // Confirm we only emit once
117+ physicalKeysMapper.onKeyPressed({ key: Qt.Key_PowerDown, isAutoRepeat: true}, 601);
118+ compare(powerSpy.count, 1);
119+ }
120+
121+ function test_screenshotButtons_data() {
122+ return [
123+ { tag: "UpFirst", first: Qt.Key_VolumeUp, second: Qt.Key_VolumeDown },
124+ { tag: "DownFirst", first: Qt.Key_VolumeDown, second: Qt.Key_VolumeUp },
125+ ];
126+ }
127+
128+ function test_screenshotButtons(data) {
129+ physicalKeysMapper.onKeyPressed({ key: data.first });
130+ physicalKeysMapper.onKeyPressed({ key: data.second });
131+ screenshotSpy.wait();
132+ physicalKeysMapper.onKeyReleased({ key: data.first });
133+ physicalKeysMapper.onKeyReleased({ key: data.second });
134+ compare(screenshotSpy.count, 1);
135+ }
136+
137+ function test_altIsDispatchedOnRelease() {
138+ // Press alt, make sure it does *not* end up in inputCatcher (aka, the focused app)
139+ keyPress(Qt.Key_Alt, Qt.NoModifier)
140+ compare(inputCatcher.pressedKeys.length, 0);
141+ // Now release alt. It should cause the previous Alt press to be dispatched along with the release
142+ keyRelease(Qt.Key_Alt, Qt.NoModifier);
143+ compare(inputCatcher.pressedKeys.length, 1);
144+ compare(inputCatcher.pressedKeys[0], Qt.Key_Alt);
145+ compare(inputCatcher.releasedKeys.length, 1);
146+ compare(inputCatcher.releasedKeys[0], Qt.Key_Alt);
147+ }
148+
149+ function test_altIsNotDispatchedOnAltTab() {
150+ // Press alt, make sure it does *not* end up in inputCatcher (aka, the focused app)
151+ inputCatcher.pressedKeys = [];
152+ keyPress(Qt.Key_Alt, Qt.NoModifier)
153+ compare(inputCatcher.pressedKeys.length, 0);
154+ // Now also press tab. As this should trigger the spread, neither of them should end up in the app
155+ keyPress(Qt.Key_Tab, Qt.NoModifier)
156+ compare(inputCatcher.pressedKeys.length, 0);
157+
158+ // Also the release events should not be dispatched to the app
159+ keyRelease(Qt.Key_Tab, Qt.NoModifier);
160+ keyRelease(Qt.Key_Alt, Qt.NoModifier);
161+ compare(inputCatcher.pressedKeys.length, 0);
162+ compare(inputCatcher.releasedKeys.length, 0);
163+ }
164+
165+ function test_altComboIsDispatched() {
166+ inputCatcher.pressedKeys = [];
167+ // Press alt, make sure it does *not* yet end up in inputCatcher (aka, the focused app)
168+ keyPress(Qt.Key_Alt, Qt.NoModifier)
169+ compare(inputCatcher.pressedKeys.length, 0);
170+ // Now press F in order to opening the File menu (Alt+F), now the app should get the full combo
171+ keyPress(Qt.Key_F, Qt.NoModifier)
172+ compare(inputCatcher.pressedKeys.length, 2);
173+ compare(inputCatcher.pressedKeys[0], Qt.Key_Alt);
174+ compare(inputCatcher.pressedKeys[1], Qt.Key_F);
175+
176+ // release them both, both are supposed to end up in the app
177+ keyRelease(Qt.Key_F, Qt.NoModifier);
178+ keyRelease(Qt.Key_Alt, Qt.NoModifier);
179+ compare(inputCatcher.releasedKeys.length, 2);
180+ }
107181 }
108182}