on xenial help sign in
Create a PhysicalKeyMapper to handle all physical/hardware keys Fixes: #1390393
22b61ccJosh Arenson
qml/Components/Dialogs.qml+3−23
@@ -36,21 +36,12 @@ Item {
36 }
36 }
37 }
37 }
38
38
39− function onPowerKeyPressed() {
39+ signal powerOffClicked();
40− // FIXME: event.isAutoRepeat is always false on Nexus 4.
41− // So we use powerKeyTimer.running to avoid the PowerOff key repeat
42− // https://launchpad.net/bugs/1349416
43− if (!powerKeyTimer.running) {
44− powerKeyTimer.restart();
45− }
46− }
47
40
48− function onPowerKeyReleased() {
41+ function showPowerDialog() {
49− powerKeyTimer.stop();
42+ d.showPowerDialog();
50 }
43 }
51
44
52− signal powerOffClicked();
53−
54 QtObject {
45 QtObject {
55 id: d // private stuff
46 id: d // private stuff
56 objectName: "dialogsPrivate"
47 objectName: "dialogsPrivate"
@@ -69,17 +60,6 @@ Item {
69 active: false
60 active: false
70 }
61 }
71
62
72− Timer {
73− id: powerKeyTimer
74− interval: 2000
75− repeat: false
76− triggeredOnStart: false
77−
78− onTriggered: {
79− d.showPowerDialog();
80− }
81− }
82−
83 Component {
63 Component {
84 id: logoutDialogComponent
64 id: logoutDialogComponent
85 ShellDialog {
65 ShellDialog {
qml/Components/PhysicalKeysMapper.qmlnew file+113
@@ -0,0 +1,113 @@
1+/*
2+ * Copyright (C) 2014-2015 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+import QtQuick 2.0
18+import Powerd 0.1
19+
20+/*!
21+ \brief A mapper for the physical keys on the device
22+
23+ A mapper to handle events triggered by pressing physical keys on a device.
24+ Keys included are
25+ * Volume Decrease
26+ * Volume Increase
27+ * Power
28+
29+ This allows for handling the following events
30+ * Power dialog
31+ * Volume Decreases/Increases
32+ * Screenshots
33+
34+*/
35+
36+Item {
37+ id: root
38+
39+ signal powerKeyLongPressed;
40+ signal volumeDownTriggered;
41+ signal volumeUpTriggered;
42+ signal screenshotTriggered;
43+
44+ QtObject {
45+ id: d
46+
47+ property bool volumeDownKeyPressed: false
48+ property bool volumeUpKeyPressed: false
49+ property bool ignoreVolumeEvents: false
50+ }
51+
52+ Timer {
53+ id: powerKeyLongPressTimer
54+
55+ interval: 2000
56+ onTriggered: root.powerKeyLongPressed();
57+ }
58+
59+
60+ function onKeyPressed(event) {
61+ if ((event.key == Qt.Key_PowerDown || event.key == Qt.Key_PowerOff)
62+ && !event.isAutoRepeat) {
63+
64+ // FIXME: We only consider power key presses if the screen is
65+ // on because of bugs 1410830/1409003. The theory is that when
66+ // those bugs are encountered, there is a >2s delay between the
67+ // power press event and the power release event, which causes
68+ // the shutdown dialog to appear on resume. So to avoid that
69+ // symptom while we investigate the root cause, we simply won't
70+ // initiate any dialogs when the screen is off.
71+ if (Powerd.status === Powerd.On) {
72+ powerKeyLongPressTimer.restart();
73+ }
74+ event.accepted = true;
75+ } else if ((event.key == Qt.Key_MediaTogglePlayPause || event.key == Qt.Key_MediaPlay)
76+ && !event.isAutoRepeat) {
77+ event.accepted = callManager.handleMediaKey(false);
78+ } else if (event.key == Qt.Key_VolumeDown) {
79+ if (event.isAutoRepeat && !d.ignoreVolumeEvents) root.volumeDownTriggered();
80+ else if (!event.isAutoRepeat) {
81+ if (d.volumeUpKeyPressed) {
82+ if (Powerd.status === Powerd.On) root.screenshotTriggered();
83+ d.ignoreVolumeEvents = true;
84+ }
85+ d.volumeDownKeyPressed = true;
86+ }
87+ } else if (event.key == Qt.Key_VolumeUp) {
88+ if (event.isAutoRepeat && !d.ignoreVolumeEvents) root.volumeUpTriggered();
89+ else if (!event.isAutoRepeat) {
90+ if (d.volumeDownKeyPressed) {
91+ if (Powerd.status === Powerd.On) root.screenshotTriggered();
92+ d.ignoreVolumeEvents = true;
93+ }
94+ d.volumeUpKeyPressed = true;
95+ }
96+ }
97+ }
98+
99+ function onKeyReleased(event) {
100+ if (event.key == Qt.Key_PowerDown || event.key == Qt.Key_PowerOff) {
101+ powerKeyLongPressTimer.stop();
102+ event.accepted = true;
103+ } else if (event.key == Qt.Key_VolumeDown) {
104+ if (!d.ignoreVolumeEvents) root.volumeDownTriggered();
105+ d.volumeDownKeyPressed = false;
106+ if (!d.volumeUpKeyPressed) d.ignoreVolumeEvents = false;
107+ } else if (event.key == Qt.Key_VolumeUp) {
108+ if (!d.ignoreVolumeEvents) root.volumeUpTriggered();
109+ d.volumeUpKeyPressed = false;
110+ if (!d.volumeDownKeyPressed) d.ignoreVolumeEvents = false;
111+ }
112+ }
113+}
qml/Components/VolumeKeyFilter.qmldeleted−63
@@ -1,63 +0,0 @@
1−/*
2− * Copyright (C) 2014 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−import QtQuick 2.0
18−/*!
19− \brief A filter for volume keys
20−
21−A filter which treats volume keys as single tri-state key with the states:
22−VolumeUp Pressed, VolumeDown Pressed or Volume Up+Down pressed
23−*/
24−QtObject {
25− id: root
26−
27− signal volumeUpPressed()
28− signal volumeDownPressed()
29− signal bothVolumeKeysPressed()
30−
31− property bool volumeUpKeyPressed: false
32− property bool volumeDownKeyPressed: false
33− property bool aVolumeKeyWasReleased: true
34−
35− function onKeyPressed(key) {
36− if (key == Qt.Key_VolumeUp)
37− volumeUpKeyPressed = true;
38− else if (key == Qt.Key_VolumeDown)
39− volumeDownKeyPressed = true;
40−
41− if (volumeDownKeyPressed && volumeUpKeyPressed) {
42− //avoids sending a signal repeatedly if both keys are held
43− //instead one of the keys must have been previously released
44− if (aVolumeKeyWasReleased)
45− bothVolumeKeysPressed();
46− aVolumeKeyWasReleased = false;
47− } else if (volumeDownKeyPressed) {
48− volumeDownPressed();
49− } else if (volumeUpKeyPressed) {
50− volumeUpPressed();
51− }
52− }
53−
54− function onKeyReleased(key) {
55− if (key == Qt.Key_VolumeUp) {
56− volumeUpKeyPressed = false;
57− aVolumeKeyWasReleased = true;
58− } else if (key == Qt.Key_VolumeDown) {
59− volumeDownKeyPressed = false;
60− aVolumeKeyWasReleased = true;
61− }
62− }
63−}
qml/Shell.qml+11−38
@@ -150,6 +150,15 @@ Item {
150 objectName: "dashCommunicator"
150 objectName: "dashCommunicator"
151 }
151 }
152
152
153+ PhysicalKeysMapper {
154+ id: physicalKeysMapper
155+
156+ onPowerKeyLongPressed: dialogs.showPowerDialog()
157+ onVolumeDownTriggered: volumeControl.volumeDown();
158+ onVolumeUpTriggered: volumeControl.volumeUp();
159+ onScreenshotTriggered: screenGrabber.capture();
160+ }
161+
153 ScreenGrabber {
162 ScreenGrabber {
154 id: screenGrabber
163 id: screenGrabber
155 z: dialogs.z + 10
164 z: dialogs.z + 10
@@ -162,45 +171,9 @@ Item {
162 value: launcher.shown || launcher.dashSwipe
171 value: launcher.shown || launcher.dashSwipe
163 }
172 }
164
173
165− VolumeKeyFilter {
166− id: volumeKeyFilter
167− onVolumeDownPressed: volumeControl.volumeDown()
168− onVolumeUpPressed: volumeControl.volumeUp()
169− onBothVolumeKeysPressed: screenGrabber.capture()
170− }
171−
172 WindowKeysFilter {
174 WindowKeysFilter {
173− Keys.onPressed: {
175+ Keys.onPressed: physicalKeysMapper.onKeyPressed(event);
174− // Nokia earpieces give TogglePlayPause, while the iPhone's earpiece gives Play
175− if (event.key == Qt.Key_MediaTogglePlayPause || event.key == Qt.Key_MediaPlay) {
176− event.accepted = callManager.handleMediaKey(false);
177− } else if (event.key == Qt.Key_PowerOff || event.key == Qt.Key_PowerDown) {
178− // FIXME: We only consider power key presses if the screen is
179− // on because of bugs 1410830/1409003. The theory is that when
180− // those bugs are encountered, there is a >2s delay between the
181− // power press event and the power release event, which causes
182− // the shutdown dialog to appear on resume. So to avoid that
183− // symptom while we investigate the root cause, we simply won't
184− // initiate any dialogs when the screen is off.
185− if (Powerd.status === Powerd.On) {
186− dialogs.onPowerKeyPressed();
187− }
188− event.accepted = true;
189− } else {
190− volumeKeyFilter.onKeyPressed(event.key);
191− event.accepted = false;
192− }
193− }
194−
195− Keys.onReleased: {
176+ Keys.onReleased: physicalKeysMapper.onKeyReleased(event);
196− if (event.key == Qt.Key_PowerOff || event.key == Qt.Key_PowerDown) {
197− dialogs.onPowerKeyReleased();
198− event.accepted = true;
199− } else {
200− volumeKeyFilter.onKeyReleased(event.key);
201− event.accepted = false;
202− }
203− }
204 }
177 }
205
178
206 Item {
179 Item {
tests/qmltests/CMakeLists.txt+1
@@ -13,6 +13,7 @@ set(qmltest_DEFAULT_TARGETS qmlunittests)
13set(qmltest_DEFAULT_NO_ADD_TEST FALSE)
13set(qmltest_DEFAULT_NO_ADD_TEST FALSE)
14set(qmltest_DEFAULT_PROPERTIES ENVIRONMENT "QT_QPA_PLATFORM=minimal")
14set(qmltest_DEFAULT_PROPERTIES ENVIRONMENT "QT_QPA_PLATFORM=minimal")
15add_qml_test(utils/Unity/Test UnityTest)
15add_qml_test(utils/Unity/Test UnityTest)
16+add_qml_test(Components PhysicalKeysMapper)
16
17
17set(qmltest_DEFAULT_TARGETS qmluitests)
18set(qmltest_DEFAULT_TARGETS qmluitests)
18set(qmltest_DEFAULT_NO_ADD_TEST TRUE)
19set(qmltest_DEFAULT_NO_ADD_TEST TRUE)
tests/qmltests/Components/tst_PhysicalKeysMapper.qmlnew file+119
@@ -0,0 +1,119 @@
1+/*
2+ * Copyright (C) 2014-2015 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+import QtQuick 2.0
18+import QtTest 1.0
19+import "../../../qml/Components"
20+
21+TestCase {
22+ name: "PhysicalKeysMapper"
23+
24+ Loader {
25+ // Using a Loader here to make sure mapper state is coherent
26+ // regardless of unmatched KeyPress and KeyRelease events
27+ id: loader
28+ active: false
29+ sourceComponent: PhysicalKeysMapper { }
30+ }
31+
32+ SignalSpy {
33+ id: powerSpy
34+ target: loader.item
35+ signalName: "powerKeyLongPressed"
36+ }
37+
38+ SignalSpy {
39+ id: volumeDownSpy
40+ target: loader.item
41+ signalName: "volumeDownTriggered"
42+ }
43+
44+ SignalSpy {
45+ id: volumeUpSpy
46+ target: loader.item
47+ signalName: "volumeUpTriggered"
48+ }
49+
50+ SignalSpy {
51+ id: screenshotSpy
52+ target: loader.item
53+ signalName: "screenshotTriggered"
54+ }
55+
56+ function init() {
57+ loader.active = true;
58+ tryCompare(loader.status == Loader.Ready);
59+ }
60+
61+ function cleanup() {
62+ loader.active = false;
63+ powerSpy.clear();
64+ volumeDownSpy.clear();
65+ volumeUpSpy.clear();
66+ screenshotSpy.clear();
67+ }
68+
69+ function test_LongPressPowerButton() {
70+ loader.item.onKeyPressed({ key: Qt.Key_PowerDown });
71+
72+ expectFailContinue("", "Power signal should not be emitted within a second");
73+ powerSpy.wait(1000);
74+ powerSpy.clear();
75+
76+ loader.item.onKeyPressed({ key: Qt.Key_PowerDown, isAutoRepeat: true });
77+ powerSpy.wait(1500);
78+ }
79+
80+ function test_ScreenshotButtons_data() {
81+ return [
82+ { tag: "UpFirst", first: Qt.Key_VolumeUp, second: Qt.Key_VolumeDown },
83+ { tag: "DownFirst", first: Qt.Key_VolumeDown, second: Qt.Key_VolumeUp },
84+ ];
85+ }
86+
87+ function test_ScreenshotButtons(data) {
88+ loader.item.onKeyPressed({ key: data.first });
89+ loader.item.onKeyPressed({ key: data.second });
90+ screenshotSpy.wait();
91+ loader.item.onKeyReleased({ key: data.first });
92+ loader.item.onKeyReleased({ key: data.second });
93+ expectFailContinue("", "VolumeUp signal should not fire");
94+ volumeUpSpy.wait(100);
95+ expectFailContinue("", "VolumeDown signal should not fire");
96+ volumeDownSpy.wait(100);
97+ }
98+
99+ function test_VolumeButton_data() {
100+ return [
101+ { tag: "Down", key: Qt.Key_VolumeDown, spy: volumeDownSpy },
102+ { tag: "Up", key: Qt.Key_VolumeUp, spy: volumeUpSpy },
103+ ];
104+ }
105+
106+ function test_VolumeButton(data) {
107+ loader.item.onKeyPressed({ key: data.key });
108+ expectFailContinue("", "Signal should not fire on press");
109+ data.spy.wait(100);
110+ data.spy.clear();
111+
112+ loader.item.onKeyReleased({ key: data.key });
113+ data.spy.wait();
114+
115+ loader.item.onKeyPressed({ key: data.key });
116+ loader.item.onKeyPressed({ key: data.key, isAutoRepeat: true });
117+ data.spy.wait();
118+ }
119+}