on xenial help sign in
prevent alt key presses to be dispatched to the app on alt+tab
059d671Michael Zanetti
plugins/Utils/CMakeLists.txt+1
@@ -24,6 +24,7 @@ set(QMLPLUGIN_SRC
24 easingcurve.cpp
24 easingcurve.cpp
25 windowstatestorage.cpp
25 windowstatestorage.cpp
26 timezoneFormatter.cpp
26 timezoneFormatter.cpp
27+ inputeventgenerator.cpp
27 plugin.cpp
28 plugin.cpp
28 )
29 )
29
30
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::generateEvent(QQuickItem *receiver, Qt::Key key, bool pressed, Qt::KeyboardModifiers modifier, quint64 timestamp, quint32 nativeScanCode, const QString &text)
29+{
30+ QEvent::Type type = pressed ? QEvent::KeyPress : QEvent::KeyRelease;
31+ QKeyEvent ev(type, key, modifier, nativeScanCode, 0, 0, text);
32+ ev.setTimestamp(timestamp);
33+ QCoreApplication::sendEvent(receiver->window(), &ev);
34+}
plugins/Utils/inputeventgenerator.hnew file+33
@@ -0,0 +1,33 @@
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+class InputEventGenerator : public QQuickItem
25+{
26+ Q_OBJECT
27+public:
28+ InputEventGenerator(QQuickItem *parent = 0);
29+
30+ Q_INVOKABLE void generateEvent(QQuickItem *receiver, Qt::Key key, bool pressed, Qt::KeyboardModifiers modifier = Qt::NoModifier, quint64 timestamp = QDateTime::currentMSecsSinceEpoch(), quint32 nativeScanCode = 0, const QString &text = QString());
31+};
32+
33+#endif // INPUTEVENTGENERATOR_H
plugins/Utils/plugin.cpp+2
@@ -36,6 +36,7 @@
36#include "constants.h"
36#include "constants.h"
37#include "timezoneFormatter.h"
37#include "timezoneFormatter.h"
38#include "applicationsfiltermodel.h"
38#include "applicationsfiltermodel.h"
39+#include "inputeventgenerator.h"
39
40
40static QObject *createWindowStateStorage(QQmlEngine *engine, QJSEngine *scriptEngine)
41static QObject *createWindowStateStorage(QQmlEngine *engine, QJSEngine *scriptEngine)
41{
42{
@@ -68,6 +69,7 @@ void UtilsPlugin::registerTypes(const char *uri)
68 [](QQmlEngine*, QJSEngine*) -> QObject* { return new TimezoneFormatter; });
69 [](QQmlEngine*, QJSEngine*) -> QObject* { return new TimezoneFormatter; });
69 qmlRegisterType<ActiveFocusLogger>(uri, 0, 1, "ActiveFocusLogger");
70 qmlRegisterType<ActiveFocusLogger>(uri, 0, 1, "ActiveFocusLogger");
70 qmlRegisterType<ApplicationsFilterModel>(uri, 0, 1, "ApplicationsFilterModel");
71 qmlRegisterType<ApplicationsFilterModel>(uri, 0, 1, "ApplicationsFilterModel");
72+ qmlRegisterType<InputEventGenerator>(uri, 0, 1, "InputEventGenerator");
71}
73}
72
74
73void UtilsPlugin::initializeEngine(QQmlEngine *engine, const char *uri)
75void UtilsPlugin::initializeEngine(QQmlEngine *engine, const char *uri)
qml/Components/PhysicalKeysMapper.qml+32−2
@@ -16,6 +16,7 @@
16
16
17import QtQuick 2.4
17import QtQuick 2.4
18import Powerd 0.1
18import Powerd 0.1
19+import Utils 0.1
19
20
20/*!
21/*!
21 \brief A mapper for the physical keys on the device
22 \brief A mapper for the physical keys on the device
@@ -59,9 +60,24 @@ Item {
59 property bool altTabPressed: false
60 property bool altTabPressed: false
60
61
61 property var powerButtonPressStart: 0
62 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
62 }
72 }
63
73
64 function onKeyPressed(event, currentEventTimestamp) {
74 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.generateEvent(root, Qt.Key_Alt, true, Qt.NoModifier, currentEventTimestamp - 1, 56);
79+ }
80+
65 if (event.key == Qt.Key_PowerDown || event.key == Qt.Key_PowerOff) {
81 if (event.key == Qt.Key_PowerDown || event.key == Qt.Key_PowerOff) {
66 if (event.isAutoRepeat) {
82 if (event.isAutoRepeat) {
67 if (d.powerButtonPressStart > 0
83 if (d.powerButtonPressStart > 0
@@ -97,7 +113,12 @@ Item {
97 d.volumeUpKeyPressed = true;
113 d.volumeUpKeyPressed = true;
98 }
114 }
99 } else if (event.key == Qt.Key_Alt || (root.controlInsteadOfAlt && event.key == Qt.Key_Control)) {
115 } else if (event.key == Qt.Key_Alt || (root.controlInsteadOfAlt && event.key == Qt.Key_Control)) {
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)
100− d.altPressed = true;
118+ d.altPressed = true;
119+ event.accepted = true;
120+ d.altPressInjected = false;
121+ }
101 } else if (event.key == Qt.Key_Tab) {
122 } else if (event.key == Qt.Key_Tab) {
102 if (d.altPressed && !d.altTabPressed) {
123 if (d.altPressed && !d.altTabPressed) {
103 d.altTabPressed = true;
124 d.altTabPressed = true;
@@ -119,10 +140,19 @@ Item {
119 d.volumeUpKeyPressed = false;
140 d.volumeUpKeyPressed = false;
120 if (!d.volumeDownKeyPressed) d.ignoreVolumeEvents = false;
141 if (!d.volumeDownKeyPressed) d.ignoreVolumeEvents = false;
121 } else if (event.key == Qt.Key_Alt || (root.controlInsteadOfAlt && event.key == Qt.Key_Control)) {
142 } else if (event.key == Qt.Key_Alt || (root.controlInsteadOfAlt && event.key == Qt.Key_Control)) {
122− d.altPressed = false;
123 if (d.altTabPressed) {
143 if (d.altTabPressed) {
124 d.altTabPressed = false;
144 d.altTabPressed = false;
125 event.accepted = true;
145 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.generateEvent(root, 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;
126 }
156 }
127 }
157 }
128 }
158 }
tests/mocks/Utils/CMakeLists.txt+1
@@ -19,6 +19,7 @@ set(QMLPLUGIN_SRC
19 ${CMAKE_SOURCE_DIR}/plugins/Utils/easingcurve.cpp
19 ${CMAKE_SOURCE_DIR}/plugins/Utils/easingcurve.cpp
20 ${CMAKE_SOURCE_DIR}/plugins/Utils/inputwatcher.cpp
20 ${CMAKE_SOURCE_DIR}/plugins/Utils/inputwatcher.cpp
21 ${CMAKE_SOURCE_DIR}/plugins/Utils/applicationsfiltermodel.cpp
21 ${CMAKE_SOURCE_DIR}/plugins/Utils/applicationsfiltermodel.cpp
22+ ${CMAKE_SOURCE_DIR}/plugins/Utils/inputeventgenerator.cpp
22 ${APPLICATION_API_INCLUDEDIR}/unity/shell/application/ApplicationManagerInterface.h
23 ${APPLICATION_API_INCLUDEDIR}/unity/shell/application/ApplicationManagerInterface.h
23 constants.cpp
24 constants.cpp
24 plugin.cpp
25 plugin.cpp
tests/mocks/Utils/plugin.cpp+2
@@ -36,6 +36,7 @@
36#include <windowscreenshotprovider.h>
36#include <windowscreenshotprovider.h>
37#include <easingcurve.h>
37#include <easingcurve.h>
38#include <applicationsfiltermodel.h>
38#include <applicationsfiltermodel.h>
39+#include <inputeventgenerator.h>
39
40
40static QObject *createWindowStateStorage(QQmlEngine *engine, QJSEngine *scriptEngine)
41static QObject *createWindowStateStorage(QQmlEngine *engine, QJSEngine *scriptEngine)
41{
42{
@@ -65,6 +66,7 @@ void FakeUtilsPlugin::registerTypes(const char *uri)
65 qmlRegisterSingletonType<Constants>(uri, 0, 1, "Constants", createConstants);
66 qmlRegisterSingletonType<Constants>(uri, 0, 1, "Constants", createConstants);
66 qmlRegisterType<ActiveFocusLogger>(uri, 0, 1, "ActiveFocusLogger");
67 qmlRegisterType<ActiveFocusLogger>(uri, 0, 1, "ActiveFocusLogger");
67 qmlRegisterType<ApplicationsFilterModel>(uri, 0, 1, "ApplicationsFilterModel");
68 qmlRegisterType<ApplicationsFilterModel>(uri, 0, 1, "ApplicationsFilterModel");
69+ qmlRegisterType<InputEventGenerator>(uri, 0, 1, "InputEventGenerator");
68}
70}
69
71
70void FakeUtilsPlugin::initializeEngine(QQmlEngine *engine, const char *uri)
72void FakeUtilsPlugin::initializeEngine(QQmlEngine *engine, const char *uri)
tests/qmltests/CMakeLists.txt+1−1
@@ -12,7 +12,7 @@ add_unity8_qmltest(Components DraggingArea)
12add_unity8_qmltest(Components LazyImage)
12add_unity8_qmltest(Components LazyImage)
13add_unity8_qmltest(Components Lockscreen)
13add_unity8_qmltest(Components Lockscreen)
14add_unity8_qmltest(Components ModeSwitchWarningDialog)
14add_unity8_qmltest(Components ModeSwitchWarningDialog)
15−add_unity8_qmlunittest(Components PhysicalKeysMapper)
15+add_unity8_qmltest(Components PhysicalKeysMapper)
16add_unity8_qmltest(Components Rating)
16add_unity8_qmltest(Components Rating)
17add_unity8_qmltest(Components ResponsiveGridView)
17add_unity8_qmltest(Components ResponsiveGridView)
18add_unity8_qmltest(Components ResponsiveVerticalJournal)
18add_unity8_qmltest(Components ResponsiveVerticalJournal)
tests/qmltests/Components/tst_PhysicalKeysMapper.qml+136−62
@@ -18,12 +18,19 @@ import QtQuick 2.4
18import QtTest 1.0
18import QtTest 1.0
19import "../../../qml/Components"
19import "../../../qml/Components"
20import Powerd 0.1
20import Powerd 0.1
21+import Utils 0.1
21
22
22−TestCase {
23+Item {
23− name: "PhysicalKeysMapper"
24+ width: 100
25+ height: 100
24
26
25 property var physicalKeysMapper: loader.item
27 property var physicalKeysMapper: loader.item
26
28
29+ WindowKeysFilter {
30+ Keys.onPressed: physicalKeysMapper.onKeyPressed(event, currentEventTimestamp);
31+ Keys.onReleased: physicalKeysMapper.onKeyReleased(event, currentEventTimestamp);
32+ }
33+
27 Loader {
34 Loader {
28 // Using a Loader here to make sure mapper state is coherent
35 // Using a Loader here to make sure mapper state is coherent
29 // regardless of unmatched KeyPress and KeyRelease events
36 // regardless of unmatched KeyPress and KeyRelease events
@@ -32,6 +39,21 @@ TestCase {
32 sourceComponent: PhysicalKeysMapper { }
39 sourceComponent: PhysicalKeysMapper { }
33 }
40 }
34
41
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+
35 SignalSpy {
57 SignalSpy {
36 id: powerSpy
58 id: powerSpy
37 target: physicalKeysMapper
59 target: physicalKeysMapper
@@ -44,65 +66,117 @@ TestCase {
44 signalName: "screenshotTriggered"
66 signalName: "screenshotTriggered"
45 }
67 }
46
68
69+ TestCase {
70+ name: "PhysicalKeysMapper"
71+ when: windowShown
72+
47− function init() {
73+ function init() {
48− Powerd.setStatus(Powerd.On, Powerd.Unknown);
74+ Powerd.setStatus(Powerd.On, Powerd.Unknown);
49− loader.active = true;
75+ loader.active = true;
50− tryCompare(loader.status == Loader.Ready);
76+ tryCompare(loader.status == Loader.Ready);
77+ inputCatcher.pressedKeys = [];
78+ inputCatcher.releasedKeys = [];
51− }
79+ }
52−
80+
53− function cleanup() {
81+ function cleanup() {
54− loader.active = false;
82+ loader.active = false;
55− powerSpy.clear();
83+ powerSpy.clear();
56− screenshotSpy.clear();
84+ screenshotSpy.clear();
57− }
85+ }
58−
86+
59− function test_powerKeyLongPressed_data() {
87+ function test_powerKeyLongPressed_data() {
60− return [
88+ return [
61− { tag: "screenOn", status: Powerd.On },
89+ { tag: "screenOn", status: Powerd.On },
62− { tag: "screenOff", status: Powerd.Off },
90+ { tag: "screenOff", status: Powerd.Off },
63− ];
91+ ];
64− }
92+ }
65−
93+
66− function test_powerKeyLongPressed(data) {
94+ function test_powerKeyLongPressed(data) {
67− Powerd.setStatus(data.status, Powerd.Unknown);
95+ Powerd.setStatus(data.status, Powerd.Unknown);
68− physicalKeysMapper.powerKeyLongPressTime = 500;
96+ physicalKeysMapper.powerKeyLongPressTime = 500;
69−
97+
70− physicalKeysMapper.onKeyPressed({ key: Qt.Key_PowerDown, isAutoRepeat: false }, 100);
98+ physicalKeysMapper.onKeyPressed({ key: Qt.Key_PowerDown, isAutoRepeat: false }, 100);
71−
99+
72− // After the first key press the screen is always on
100+ // After the first key press the screen is always on
73− // and the rest of keypresses are auto repeat
101+ // and the rest of keypresses are auto repeat
74− Powerd.setStatus(Powerd.On, Powerd.Unknown);
102+ Powerd.setStatus(Powerd.On, Powerd.Unknown);
75−
103+
76− physicalKeysMapper.onKeyPressed({ key: Qt.Key_PowerDown, isAutoRepeat: true}, 300);
104+ physicalKeysMapper.onKeyPressed({ key: Qt.Key_PowerDown, isAutoRepeat: true}, 300);
77− physicalKeysMapper.onKeyPressed({ key: Qt.Key_PowerDown, isAutoRepeat: true}, 400);
105+ physicalKeysMapper.onKeyPressed({ key: Qt.Key_PowerDown, isAutoRepeat: true}, 400);
78− physicalKeysMapper.onKeyPressed({ key: Qt.Key_PowerDown, isAutoRepeat: true}, 500);
106+ physicalKeysMapper.onKeyPressed({ key: Qt.Key_PowerDown, isAutoRepeat: true}, 500);
79− physicalKeysMapper.onKeyPressed({ key: Qt.Key_PowerDown, isAutoRepeat: true}, 599);
107+ physicalKeysMapper.onKeyPressed({ key: Qt.Key_PowerDown, isAutoRepeat: true}, 599);
80−
108+
81− // powerKeyLongPressed should not have been emitted yet.
109+ // powerKeyLongPressed should not have been emitted yet.
82− compare(powerSpy.count, 0);
110+ compare(powerSpy.count, 0);
83−
111+
84− physicalKeysMapper.onKeyPressed({ key: Qt.Key_PowerDown, isAutoRepeat: true}, 600);
112+ physicalKeysMapper.onKeyPressed({ key: Qt.Key_PowerDown, isAutoRepeat: true}, 600);
85−
113+
86− compare(powerSpy.count, 1);
114+ compare(powerSpy.count, 1);
87−
115+
88− // Confirm we only emit once
116+ // Confirm we only emit once
89− physicalKeysMapper.onKeyPressed({ key: Qt.Key_PowerDown, isAutoRepeat: true}, 601);
117+ physicalKeysMapper.onKeyPressed({ key: Qt.Key_PowerDown, isAutoRepeat: true}, 601);
90− compare(powerSpy.count, 1);
118+ compare(powerSpy.count, 1);
91− }
119+ }
92−
120+
93− function test_screenshotButtons_data() {
121+ function test_screenshotButtons_data() {
94− return [
122+ return [
95− { tag: "UpFirst", first: Qt.Key_VolumeUp, second: Qt.Key_VolumeDown },
123+ { tag: "UpFirst", first: Qt.Key_VolumeUp, second: Qt.Key_VolumeDown },
96− { tag: "DownFirst", first: Qt.Key_VolumeDown, second: Qt.Key_VolumeUp },
124+ { tag: "DownFirst", first: Qt.Key_VolumeDown, second: Qt.Key_VolumeUp },
97− ];
125+ ];
98− }
126+ }
99−
127+
100− function test_screenshotButtons(data) {
128+ function test_screenshotButtons(data) {
101− physicalKeysMapper.onKeyPressed({ key: data.first });
129+ physicalKeysMapper.onKeyPressed({ key: data.first });
102− physicalKeysMapper.onKeyPressed({ key: data.second });
130+ physicalKeysMapper.onKeyPressed({ key: data.second });
103− screenshotSpy.wait();
131+ screenshotSpy.wait();
104− physicalKeysMapper.onKeyReleased({ key: data.first });
132+ physicalKeysMapper.onKeyReleased({ key: data.first });
105− physicalKeysMapper.onKeyReleased({ key: data.second });
133+ physicalKeysMapper.onKeyReleased({ key: data.second });
106− compare(screenshotSpy.count, 1);
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.pressedKeys.length, 2);
180+ }
107 }
181 }
108}
182}