on xenial help sign in
Implement Launcher's keyboard navigation and updated pip design

Approved by: Lukáš Tinkl

commit82e4b970b0e15eb8b36399786c8f74e88f9fcf9e authorMichael Zanetti <michael.zanetti@canonical.com> dateThu 10 Mar 2016, 22:37 (11 years ago) parentc259b704fa59ae
9 files changed+602−71
qml/Components/PhysicalKeysMapper.qml+24 qml/Launcher/Launcher.qml+112−1 qml/Launcher/LauncherDelegate.qml+46−8 qml/Launcher/LauncherPanel.qml+103−32 qml/Launcher/graphics/launcher-app-focus-ring.svg+12 qml/Shell.qml+33 tests/mocks/Unity/Launcher/MockLauncherModel.cpp+2 tests/qmltests/Launcher/tst_Launcher.qml+156−5 tests/qmltests/tst_Shell.qml+114−25
qml/Components/PhysicalKeysMapper.qml+24
@@ -42,12 +42,15 @@ Item {
4242 signal screenshotTriggered;
4343
4444 readonly property bool altTabPressed: d.altTabPressed
45+ readonly property bool superPressed: d.superPressed
46+ readonly property bool superTabPressed: d.superTabPressed
4547
4648 property int powerKeyLongPressTime: 2000
4749
4850 // For testing. If running windowed (e.g. tryShell), Alt+Tab is taken by the
4951 // running desktop, set this to true to use Ctrl+Tab instead.
5052 property bool controlInsteadOfAlt: false
53+ property bool controlInsteadOfSuper: false
5154
5255 QtObject {
5356 id: d
@@ -59,6 +62,9 @@ Item {
5962 property bool altPressed: false
6063 property bool altTabPressed: false
6164
65+ property bool superPressed: false
66+ property bool superTabPressed: false
67+
6268 property var powerButtonPressStart: 0
6369
6470 // We need to eat ALT presses until we know what they're for (Alt+Tab or going to the app?)
@@ -119,11 +125,23 @@ Item {
119125 event.accepted = true;
120126 d.altPressInjected = false;
121127 }
128+
129+ // Adding MetaModifier here because that's what keyboards do. Pressing Super_L actually gives
130+ // Super_L + MetaModifier. This helps to make sure we only invoke superPressed if no other
131+ // Modifier is pressed too.
132+ } else if (((event.key == Qt.Key_Super_L || event.key == Qt.Key_Super_R) && event.modifiers === Qt.MetaModifier)
133+ || (root.controlInsteadOfSuper && event.key == Qt.Key_Control)
134+ ) {
135+ d.superPressed = true;
122136 } else if (event.key == Qt.Key_Tab) {
123137 if (d.altPressed && !d.altTabPressed) {
124138 d.altTabPressed = true;
125139 event.accepted = true;
126140 }
141+ if (d.superPressed && !d.superTabPressed) {
142+ d.superTabPressed = true;
143+ event.accepted = true;
144+ }
127145 }
128146 }
129147
@@ -154,6 +172,12 @@ Item {
154172 if (d.altTabPressed) {
155173 event.accepted = true;
156174 }
175+ } else if (event.key == Qt.Key_Super_L || event.key == Qt.Key_Super_R || (root.controlInsteadOfSuper && event.key == Qt.Key_Control)) {
176+ d.superPressed = false;
177+ if (d.superTabPressed) {
178+ d.superTabPressed = false;
179+ event.accepted = true;
180+ }
157181 }
158182 }
159183}
qml/Launcher/Launcher.qml+112−1
@@ -19,8 +19,9 @@ import "../Components"
1919import Ubuntu.Components 1.3
2020import Ubuntu.Gestures 0.1
2121import Unity.Launcher 0.1
22+import GlobalShortcut 1.0
2223
23−Item {
24+FocusScope {
2425 id: root
2526
2627 property bool autohideEnabled: false
@@ -34,6 +35,9 @@ Item {
3435 property real progress: dragArea.dragging && dragArea.touchX > panelWidth ?
3536 (width * (dragArea.touchX-panelWidth) / (width - panelWidth)) : 0
3637
38+ property bool superPressed: false
39+ property bool superTabPressed: false
40+
3741 readonly property bool dragging: dragArea.dragging
3842 readonly property real dragDistance: dragArea.dragging ? dragArea.touchX : 0
3943 readonly property real visibleWidth: panel.width + panel.x
@@ -57,6 +61,37 @@ Item {
5761 }
5862 }
5963
64+ onSuperPressedChanged: {
65+ if (superPressed) {
66+ superPressTimer.start();
67+ superLongPressTimer.start();
68+ } else {
69+ superPressTimer.stop();
70+ superLongPressTimer.stop();
71+ launcher.switchToNextState("");
72+ panel.shortcutHintsShown = false;
73+ }
74+ }
75+
76+ onSuperTabPressedChanged: {
77+ if (superTabPressed) {
78+ switchToNextState("visible")
79+ panel.highlightIndex = -1;
80+ root.focus = true;
81+ superPressTimer.stop();
82+ superLongPressTimer.stop();
83+ } else {
84+ if (panel.highlightIndex == -1) {
85+ showDashHome();
86+ } else if (panel.highlightIndex >= 0){
87+ launcherApplicationSelected(LauncherModel.get(panel.highlightIndex).appId);
88+ }
89+ panel.highlightIndex = -2;
90+ switchToNextState("");
91+ root.focus = false;
92+ }
93+ }
94+
6095 function hide() {
6196 switchToNextState("")
6297 }
@@ -90,6 +125,77 @@ Item {
90125 }
91126 }
92127
128+ function openForKeyboardNavigation() {
129+ panel.highlightIndex = -1; // The BFB
130+ root.focus = true;
131+ switchToNextState("visible")
132+ }
133+
134+ Keys.onPressed: {
135+ switch (event.key) {
136+ case Qt.Key_Backtab:
137+ panel.highlightPrevious();
138+ event.accepted = true;
139+ break;
140+ case Qt.Key_Up:
141+ if (root.inverted) {
142+ panel.highlightNext()
143+ } else {
144+ panel.highlightPrevious();
145+ }
146+ event.accepted = true;
147+ break;
148+ case Qt.Key_Tab:
149+ panel.highlightNext();
150+ event.accepted = true;
151+ break;
152+ case Qt.Key_Down:
153+ if (root.inverted) {
154+ panel.highlightPrevious();
155+ } else {
156+ panel.highlightNext();
157+ }
158+ event.accepted = true;
159+ break;
160+ case Qt.Key_Right:
161+ panel.openQuicklist(panel.highlightIndex)
162+ event.accepted = true;
163+ break;
164+ case Qt.Key_Escape:
165+ panel.highlightIndex = -2;
166+ // Falling through intentionally
167+ case Qt.Key_Enter:
168+ case Qt.Key_Return:
169+ case Qt.Key_Space:
170+ if (panel.highlightIndex == -1) {
171+ showDashHome();
172+ } else if (panel.highlightIndex >= 0) {
173+ launcherApplicationSelected(LauncherModel.get(panel.highlightIndex).appId);
174+ }
175+ panel.highlightIndex = -2
176+ root.state = ""
177+ event.accepted = true;
178+ root.focus = false;
179+ }
180+ }
181+
182+ Timer {
183+ id: superPressTimer
184+ interval: 200
185+ onTriggered: {
186+ switchToNextState("visible")
187+ }
188+ }
189+
190+ Timer {
191+ id: superLongPressTimer
192+ interval: 1000
193+ onTriggered: {
194+ switchToNextState("visible")
195+ panel.shortcutHintsShown = true;
196+ }
197+ }
198+
93199 Timer {
94200 id: teaseTimer
95201 interval: mode == "teasing" ? 200 : 300
@@ -254,6 +360,11 @@ Item {
254360 }
255361 }
256362
363+ onKbdNavigationCancelled: {
364+ root.state = "";
365+ root.focus = false;
366+ }
367+
257368 Behavior on x {
258369 enabled: !dragArea.dragging && !launcherDragArea.drag.active && panel.animate;
259370 NumberAnimation {
qml/Launcher/LauncherDelegate.qml+46−8
@@ -20,6 +20,7 @@ import Ubuntu.Components 1.3
2020Item {
2121 id: root
2222
23+ property int itemIndex: 0
2324 property string iconName
2425 property int count: 0
2526 property bool countVisible: false
@@ -29,10 +30,12 @@ Item {
2930 property real maxAngle: 0
3031 property bool inverted: false
3132 property bool alerting: false
32− readonly property alias wiggling: wiggleAnim.running
33+ property bool highlighted: false
34+ property bool shortcutHintShown: false
3335
3436 readonly property int effectiveHeight: Math.cos(angle * Math.PI / 180) * itemHeight
3537 readonly property real foldedHeight: Math.cos(maxAngle * Math.PI / 180) * itemHeight
38+ readonly property alias wiggling: wiggleAnim.running
3639
3740 property int itemWidth
3841 property int itemHeight
@@ -125,6 +128,17 @@ Item {
125128 height: parent.itemHeight + units.gu(1)
126129 anchors.centerIn: parent
127130
131+ Image {
132+ objectName: "focusRing"
133+ anchors.centerIn: iconShape
134+ height: width * 15 / 16
135+ width: iconShape.width + units.gu(1)
136+ source: "graphics/launcher-app-focus-ring.svg"
137+ sourceSize.width: width
138+ sourceSize.height: height
139+ visible: root.highlighted
140+ }
141+
128142 ProportionalShape {
129143 id: iconShape
130144 anchors.centerIn: parent
@@ -206,25 +220,49 @@ Item {
206220 }
207221 }
208222
209− Image {
210− objectName: "runningHighlight"
223+ Column {
211224 anchors {
212225 left: parent.left
213226 verticalCenter: parent.verticalCenter
214227 }
215− visible: root.itemRunning
216− rotation: 180
217− source: "graphics/focused_app_arrow.png"
228+ spacing: units.gu(.5)
229+ Repeater {
230+ model: 1 // TODO: This should be "Math.min(3, app.surfaceCount)" once we have multiple surfaces
231+ Rectangle {
232+ objectName: "runningHighlight" + index
233+ width: units.gu(0.25)
234+ height: units.gu(.5)
235+ color: "white"
236+ visible: root.itemRunning
237+ }
238+ }
218239 }
219240
220− Image {
241+ Rectangle {
221242 objectName: "focusedHighlight"
222243 anchors {
223244 right: parent.right
224245 verticalCenter: parent.verticalCenter
225246 }
247+ width: units.gu(0.25)
248+ height: units.gu(.5)
249+ color: "white"
226250 visible: root.itemFocused
227− source: "graphics/focused_app_arrow.png"
251+ }
252+
253+ Rectangle {
254+ objectName: "shortcutHint"
255+ anchors.centerIn: parent
256+ width: units.gu(3)
257+ height: width
258+ color: "#E0292929"
259+ visible: root.shortcutHintShown
260+ Label {
261+ anchors.centerIn: parent
262+ text: (itemIndex + 1) % 10
263+ color: "white"
264+ font.weight: Font.DemiBold
265+ }
228266 }
229267 }
230268
qml/Launcher/LauncherPanel.qml+103−32
@@ -19,12 +19,13 @@ import Ubuntu.Components 1.3
1919import Ubuntu.Components.ListItems 1.3 as ListItems
2020import Unity.Launcher 0.1
2121import Ubuntu.Components.Popups 1.3
22+import GlobalShortcut 1.0
2223import "../Components/ListItems"
2324import "../Components/"
2425
2526Rectangle {
2627 id: root
27− color: "#B2000000"
28+ color: "#E0292929"
2829
2930 rotation: inverted ? 180 : 0
3031
@@ -33,11 +34,13 @@ Rectangle {
3334 property bool dragging: false
3435 property bool moving: launcherListView.moving || launcherListView.flicking
3536 property bool preventHiding: moving || dndArea.draggedIndex >= 0 || quickList.state === "open" || dndArea.pressed
36− || mouseEventEater.containsMouse || dashItem.hovered
37− property int highlightIndex: -1
37+ || mouseEventEater.containsMouse || dashItem.hovered
38+ property int highlightIndex: -2
39+ property bool shortcutHintsShown: false
3840
3941 signal applicationSelected(string appId)
4042 signal showDashHome()
43+ signal kbdNavigationCancelled()
4144
4245 onXChanged: {
4346 if (quickList.state == "open") {
@@ -45,6 +48,26 @@ Rectangle {
4548 }
4649 }
4750
51+ function highlightNext() {
52+ highlightIndex++;
53+ if (highlightIndex >= launcherListView.count) {
54+ highlightIndex = -1;
55+ }
56+ moveAnimation.moveToIndex(Math.max(highlightIndex, 0));
57+ }
58+ function highlightPrevious() {
59+ highlightIndex--;
60+ if (highlightIndex <= -2) {
61+ highlightIndex = launcherListView.count - 1;
62+ }
63+ moveAnimation.moveToIndex(Math.max(highlightIndex, 0));
64+ }
65+ function openQuicklist(index) {
66+ quickList.open(index);
67+ quickList.selectedIndex = 0;
68+ quickList.focus = true;
69+ }
70+
4871 MouseArea {
4972 id: mouseEventEater
5073 anchors.fill: parent
@@ -57,20 +80,12 @@ Rectangle {
5780 fill: parent
5881 }
5982
60− Item {
83+ Rectangle {
6184 objectName: "buttonShowDashHome"
6285 width: parent.width
6386 height: units.gu(7)
64− clip: true
65−
66− UbuntuShape {
67− anchors {
68− fill: parent
69− topMargin: -units.gu(2)
70− }
71− aspect: UbuntuShape.Flat
72− backgroundColor: UbuntuColors.orange
73− }
87+ color: UbuntuColors.orange
88+ readonly property bool highlighted: root.highlightIndex == -1;
7489
7590 Image {
7691 objectName: "dashItem"
@@ -85,6 +100,14 @@ Rectangle {
85100 anchors.fill: parent
86101 onClicked: root.showDashHome()
87102 }
103+ Rectangle {
104+ objectName: "bfbFocusHighlight"
105+ anchors.fill: parent
106+ border.color: "white"
107+ border.width: units.dp(1)
108+ color: "transparent"
109+ visible: parent.highlighted
110+ }
88111 }
89112
90113 Item {
@@ -179,6 +202,16 @@ Rectangle {
179202 to = contentY;
180203 start();
181204 }
205+ function moveToIndex(index) {
206+ var itemPosition = index * launcherListView.itemHeight;
207+ var height = launcherListView.height - launcherListView.topMargin - launcherListView.bottomMargin
208+ var distanceToEnd = index == 0 || index == launcherListView.count - 1 ? 0 : launcherListView.itemHeight
209+ if (itemPosition + launcherListView.itemHeight + distanceToEnd > launcherListView.contentY + launcherListView.topMargin + height) {
210+ moveAnimation.moveTo(itemPosition + launcherListView.itemHeight - launcherListView.topMargin - height + distanceToEnd);
211+ } else if (itemPosition - distanceToEnd < launcherListView.contentY + launcherListView.topMargin) {
212+ moveAnimation.moveTo(itemPosition - distanceToEnd - launcherListView.topMargin);
213+ }
214+ }
182215 }
183216
184217 displaced: Transition {
@@ -192,6 +225,7 @@ Rectangle {
192225 // the right app when running autopilot tests for
193226 // multiple apps.
194227 readonly property string appId: model.appId
228+ itemIndex: index
195229 itemHeight: launcherListView.itemHeight
196230 itemWidth: launcherListView.itemWidth
197231 width: itemWidth
@@ -204,6 +238,8 @@ Rectangle {
204238 itemFocused: model.focused
205239 inverted: root.inverted
206240 alerting: model.alerting
241+ highlighted: root.highlightIndex == index
242+ shortcutHintShown: root.shortcutHintsShown && index <= 9
207243 z: -Math.abs(offset)
208244 maxAngle: 55
209245 property bool dragging: false
@@ -241,14 +277,7 @@ Rectangle {
241277 onAlertingChanged: {
242278 if(alerting) {
243279 if (!dragging && (launcherListView.peekingIndex === -1 || launcher.visibleWidth > 0)) {
244− var itemPosition = index * launcherListView.itemHeight;
245− var height = launcherListView.height - launcherListView.topMargin - launcherListView.bottomMargin
246− var distanceToEnd = index == 0 || index == launcherListView.count - 1 ? 0 : launcherListView.itemHeight
247− if (itemPosition + launcherListView.itemHeight + distanceToEnd > launcherListView.contentY + launcherListView.topMargin + height) {
248− moveAnimation.moveTo(itemPosition + launcherListView.itemHeight - launcherListView.topMargin - height + distanceToEnd);
249− } else if (itemPosition - distanceToEnd < launcherListView.contentY + launcherListView.topMargin) {
250− moveAnimation.moveTo(itemPosition - distanceToEnd - launcherListView.topMargin);
251− }
280+ moveAnimation.moveToIndex(index)
252281 if (!dragging && launcher.state !== "visible") {
253282 peekingAnimation.start()
254283 }
@@ -402,10 +431,7 @@ Rectangle {
402431
403432 if (mouse.button & Qt.RightButton) { // context menu
404433 // Opening QuickList
405− quickList.item = clickedItem;
406− quickList.model = launcherListView.model.get(index).quickList;
407− quickList.appId = launcherListView.model.get(index).appId;
408− quickList.state = "open";
434+ quickList.open(index);
409435 return;
410436 }
411437
@@ -481,11 +507,7 @@ Rectangle {
481507
482508 draggedIndex = Math.floor((mouse.y + launcherListView.realContentY) / launcherListView.realItemHeight);
483509
484− // Opening QuickList
485− quickList.item = selectedItem;
486− quickList.model = launcherListView.model.get(draggedIndex).quickList;
487− quickList.appId = launcherListView.model.get(draggedIndex).appId;
488− quickList.state = "open";
510+ quickList.open(draggedIndex)
489511
490512 launcherListView.interactive = false
491513
@@ -644,7 +666,10 @@ Rectangle {
644666 enabled: quickList.state == "open" || pressed
645667
646668 onClicked: {
647− quickList.state = ""
669+ quickList.state = "";
670+ quickList.focus = false;
671+ root.highlightIndex = -2;
672+ root.kbdNavigationCancelled();
648673 }
649674
650675 // Forward for dragging to work when quickList is open
@@ -693,12 +718,55 @@ Rectangle {
693718 property var model
694719 property string appId
695720 property var item
721+ property int selectedIndex: -1
722+
723+ Keys.onPressed: {
724+ switch (event.key) {
725+ case Qt.Key_Down:
726+ selectedIndex++;
727+ if (selectedIndex >= popoverRepeater.count) {
728+ selectedIndex = 0;
729+ }
730+ event.accepted = true;
731+ break;
732+ case Qt.Key_Up:
733+ selectedIndex--;
734+ if (selectedIndex < 0) {
735+ selectedIndex = popoverRepeater.count - 1;
736+ }
737+ event.accepted = true;
738+ break;
739+ case Qt.Key_Left:
740+ case Qt.Key_Escape:
741+ quickList.selectedIndex = -1;
742+ // Falling through intentionally
743+ case Qt.Key_Enter:
744+ case Qt.Key_Return:
745+ case Qt.Key_Space:
746+ if (quickList.selectedIndex >= 0) {
747+ LauncherModel.quickListActionInvoked(quickList.appId, quickList.selectedIndex)
748+ }
749+ quickList.focus = false;
750+ quickList.state = ""
751+ // Don't consume the event. We want to close the Launcher too, not just the quicklist.
752+ break;
753+ }
754+ }
696755
697756 // internal
698757 property int itemCenter: item ? root.mapFromItem(quickList.item).y + (item.height / 2) + quickList.item.offset : units.gu(1)
699758 property int offset: itemCenter + (height/2) + units.gu(1) > parent.height ? -itemCenter - (height/2) - units.gu(1) + parent.height :
700759 itemCenter - (height/2) < units.gu(1) ? (height/2) - itemCenter + units.gu(1) : 0
701760
761+ function open(index) {
762+ var itemPosition = index * launcherListView.itemHeight;
763+ var height = launcherListView.height - launcherListView.topMargin - launcherListView.bottomMargin
764+ item = launcherListView.itemAt(launcherListView.width / 2, itemPosition + launcherListView.itemHeight / 2);
765+ quickList.model = launcherListView.model.get(index).quickList;
766+ quickList.appId = launcherListView.model.get(index).appId;
767+ quickList.state = "open";
768+ }
769+
702770 Column {
703771 id: quickListColumn
704772 width: parent.width
@@ -712,6 +780,7 @@ Rectangle {
712780 objectName: "quickListEntry" + index
713781 text: (model.clickable ? "" : "<b>") + model.label + (model.clickable ? "" : "</b>")
714782 highlightWhenPressed: model.clickable
783+ selected: index === quickList.selectedIndex
715784
716785 // FIXME: This is a workaround for the theme not being context sensitive. I.e. the
717786 // ListItems don't know that they are sitting in a themed Popover where the color
@@ -727,6 +796,8 @@ Rectangle {
727796 // Unsetting model to prevent showing changing entries during fading out
728797 // that may happen because of triggering an action.
729798 LauncherModel.quickListActionInvoked(quickList.appId, index);
799+ quickList.focus = false;
800+ root.kbdNavigationCancelled();
730801 quickList.model = undefined;
731802 }
732803 }
qml/Launcher/graphics/launcher-app-focus-ring.svgnew file+12
@@ -0,0 +1,12 @@
1+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
2+<svg width="172px" height="163px" viewBox="0 0 172 163" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:sketch="http://www.bohemiancoding.com/sketch/ns">
3+ <!-- Generator: Sketch 3.4.4 (17249) - http://www.bohemiancoding.com/sketch -->
4+ <title>Shape</title>
5+ <desc>Created with Sketch.</desc>
6+ <defs></defs>
7+ <g id="•-Launcher" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" sketch:type="MSPage">
8+ <g id="Artboard-9" sketch:type="MSArtboardGroup" transform="translate(-163.000000, -1436.000000)" fill="#E95420">
9+ <path d="M221.983432,1440 L221.983432,1440 C195.6127,1440 184.708233,1442.4723 177.107949,1450.10734 C169.476819,1457.77336 167,1468.79245 167,1495.3481 L167,1538.9019 C167,1565.45755 169.476819,1576.47664 177.107949,1584.14266 C184.708233,1591.7777 195.6127,1594.25 221.983432,1594.25 L276.016868,1594.25 C302.387595,1594.25 313.291998,1591.77771 320.892221,1584.14264 C328.523252,1576.47663 331,1565.45769 331,1538.9019 L331,1495.3481 C331,1468.79231 328.523252,1457.77337 320.892221,1450.10736 C313.291998,1442.47229 302.387595,1440 276.016868,1440 L221.983432,1440 Z M221.983432,1436 L276.016868,1436 C302.345315,1436 314.848953,1438.36655 323.727108,1447.2854 C332.633306,1456.23243 335,1468.85167 335,1495.3481 L335,1538.9019 C335,1565.39833 332.633306,1578.01757 323.727108,1586.9646 C314.848953,1595.88345 302.345315,1598.25 276.016868,1598.25 L221.983432,1598.25 C195.654985,1598.25 183.151291,1595.88345 174.273077,1586.96463 C165.366772,1578.0176 163,1565.39822 163,1538.9019 L163,1495.3481 C163,1468.85178 165.366772,1456.2324 174.273077,1447.28537 C183.151291,1438.36655 195.654985,1436 221.983432,1436 L221.983432,1436 Z" id="Shape" sketch:type="MSShapeGroup"></path>
10+ </g>
11+ </g>
12+</svg>
qml/Shell.qml+33
@@ -557,6 +557,8 @@ Item {
557557 && !greeter.hasLockedApp
558558 inverted: shell.usageScenario !== "desktop"
559559 shadeBackground: !tutorial.running
560+ superPressed: physicalKeysMapper.superPressed
561+ superTabPressed: physicalKeysMapper.superTabPressed
560562
561563 onShowDashHome: showHome()
562564 onDash: showDash()
@@ -576,6 +578,37 @@ Item {
576578 panel.indicators.hide()
577579 }
578580 }
581+ onFocusChanged: {
582+ if (!focus) {
583+ applicationsDisplayLoader.focus = true;
584+ }
585+ }
586+
587+ GlobalShortcut {
588+ shortcut: Qt.AltModifier | Qt.Key_F1
589+ onTriggered: {
590+ launcher.openForKeyboardNavigation();
591+ }
592+ }
593+ GlobalShortcut {
594+ shortcut: Qt.MetaModifier | Qt.Key_0
595+ onTriggered: {
596+ if (LauncherModel.get(9)) {
597+ activateApplication(LauncherModel.get(9).appId);
598+ }
599+ }
600+ }
601+ Repeater {
602+ model: 9
603+ GlobalShortcut {
604+ shortcut: Qt.MetaModifier | (Qt.Key_1 + index)
605+ onTriggered: {
606+ if (LauncherModel.get(index)) {
607+ activateApplication(LauncherModel.get(index).appId);
608+ }
609+ }
610+ }
611+ }
579612 }
580613
581614 Wizard {
tests/mocks/Unity/Launcher/MockLauncherModel.cpp+2
@@ -25,6 +25,7 @@ MockLauncherModel::MockLauncherModel(QObject* parent): LauncherModelInterface(pa
2525 MockLauncherItem *item = new MockLauncherItem("dialer-app", "/usr/share/applications/dialer-app.desktop", "Dialer", "dialer-app", this);
2626 item->setProgress(0);
2727 item->setPinned(true);
28+ item->setRunning(true);
2829 item->setFocused(true);
2930 m_list.append(item);
3031 item = new MockLauncherItem("camera-app", "/usr/share/applications/camera-app.desktop", "Camera", "camera", this);
@@ -34,6 +35,7 @@ MockLauncherModel::MockLauncherModel(QObject* parent): LauncherModelInterface(pa
3435 item = new MockLauncherItem("gallery-app", "/usr/share/applications/gallery-app.desktop", "Gallery", "gallery", this);
3536 item->setProgress(50);
3637 item->setCountVisible(true);
38+ item->setRunning(true);
3739 item->setAlerting(false);
3840 m_list.append(item);
3941 item = new MockLauncherItem("music-app", "/usr/share/applications/music-app.desktop", "Music", "soundcloud", this);
tests/qmltests/Launcher/tst_Launcher.qml+156−5
@@ -34,6 +34,7 @@ Item {
3434 Loader {
3535 id: launcherLoader
3636 anchors.fill: parent
37+ focus: true
3738 property bool itemDestroyed: false
3839 sourceComponent: Component {
3940 Launcher {
@@ -68,6 +69,7 @@ Item {
6869
6970 Component.onCompleted: {
7071 launcherLoader.itemDestroyed = false;
72+ launcherLoader.focus = true
7173 edgeBarrierControls.target = testCase.findChild(this, "edgeBarrierController");
7274 }
7375 Component.onDestruction: {
@@ -102,6 +104,15 @@ Item {
102104 Layout.fillWidth: true
103105 }
104106
107+ Button {
108+ text: "open for kbd navigation"
109+ onClicked: {
110+ launcherLoader.item.openForKeyboardNavigation()
111+ launcherLoader.item.forceActiveFocus();// = true
112+ }
113+ Layout.fillWidth: true
114+ }
115+
105116 Row {
106117 spacing: units.gu(1)
107118
@@ -276,6 +287,24 @@ Item {
276287 }
277288 }
278289
290+ function assertFocusOnIndex(index) {
291+ var launcherPanel = findChild(launcherLoader.item, "launcherPanel");
292+ var launcherListView = findChild(launcherLoader.item, "launcherListView");
293+ var bfbFocusHighlight = findChild(launcherLoader.item, "bfbFocusHighlight");
294+ waitForRendering(launcherLoader.item);
295+ tryCompare(launcherPanel, "highlightIndex", index);
296+ tryCompare(bfbFocusHighlight, "visible", index === -1);
297+ for (var i = 0; i < launcherListView.count; i++) {
298+ var item = findChild(launcher, "launcherDelegate" + i);
299+ // Delegates might be destroyed when not visible. We can't check if they paint a focus highlight.
300+ // Make sure the requested index does have focus. for the others, try best effort to check if they don't
301+ if (index === i || item) {
302+ var focusRing = findChild(item, "focusRing")
303+ tryCompare(focusRing, "visible", index === i);
304+ }
305+ }
306+ }
307+
279308 // Drag from the left edge of the screen rightwards and check that the launcher
280309 // appears (as if being dragged by the finger/pointer)
281310 function test_dragLeftEdgeToRevealLauncherAndTapCenterToDismiss() {
@@ -290,10 +319,10 @@ Item {
290319 dragLauncherIntoView()
291320
292321 // tapping on the center of the screen should dismiss the launcher
293− mouseClick(launcher)
322+ mouseClick(launcher, panel.width + units.gu(5), launcher.height / 2)
294323
295324 // should eventually get fully retracted (hidden)
296− tryCompare(panel, "x", -launcher.panelWidth, 1000)
325+ tryCompare(panel, "x", -launcher.panelWidth, 2000)
297326 }
298327
299328 /* If I click on the icon of an application on the launcher
@@ -421,7 +450,7 @@ Item {
421450 var launcherListView = findChild(launcher, "launcherListView");
422451 for (var i = 0; i < launcherListView.count; ++i) {
423452 var delegate = findChild(launcherListView, "launcherDelegate" + i)
424− compare(findChild(delegate, "runningHighlight").visible, LauncherModel.get(i).running)
453+ compare(findChild(delegate, "runningHighlight0").visible, LauncherModel.get(i).running)
425454 }
426455 }
427456
@@ -764,14 +793,15 @@ Item {
764793 function test_launcher_dismiss() {
765794 dragLauncherIntoView();
766795 verify(launcher.state == "visible");
767− mouseClick(root);
796+
797+ mouseClick(root, root.width / 2, units.gu(1));
768798 waitUntilLauncherDisappears();
769799 verify(launcher.state == "");
770800
771801 // and repeat, as a test for regression in lpbug#1531339
772802 dragLauncherIntoView();
773803 verify(launcher.state == "visible");
774− mouseClick(root);
804+ mouseClick(root, root.width / 2, units.gu(1));
775805 waitUntilLauncherDisappears();
776806 verify(launcher.state == "");
777807 }
@@ -1044,5 +1074,126 @@ Item {
10441074 LauncherModel.setCountVisible(LauncherModel.get(1).appId, 0)
10451075 LauncherModel.setCount(LauncherModel.get(1).appId, oldCount)
10461076 }
1077+
1078+ function test_longpressSuperKeyShowsHints() {
1079+ var shortCutHint0 = findChild(findChild(launcher, "launcherDelegate0"), "shortcutHint");
1080+
1081+ tryCompare(shortCutHint0, "visible", false);
1082+
1083+ launcher.superPressed = true;
1084+ tryCompare(launcher, "state", "visible");
1085+ tryCompare(shortCutHint0, "visible", true);
1086+
1087+ launcher.superPressed = false;
1088+ tryCompare(launcher, "state", "");
1089+ tryCompare(shortCutHint0, "visible", false);
1090+ }
1091+
1092+ function test_keyboardNavigation() {
1093+ var bfbFocusHighlight = findChild(launcher, "bfbFocusHighlight");
1094+ var quickList = findChild(launcher, "quickList");
1095+ var launcherListView = findChild(launcher, "launcherListView");
1096+ var last = launcherListView.count - 1;
1097+
1098+ compare(bfbFocusHighlight.visible, false);
1099+ launcher.openForKeyboardNavigation();
1100+
1101+ assertFocusOnIndex(-1);
1102+
1103+ // Down should go down
1104+ keyClick(Qt.Key_Down);
1105+ assertFocusOnIndex(0);
1106+
1107+ // Tab should go down
1108+ keyClick(Qt.Key_Tab);
1109+ assertFocusOnIndex(1);
1110+
1111+ // Up should go up
1112+ keyClick(Qt.Key_Up);
1113+ assertFocusOnIndex(0);
1114+
1115+ // Backtab should go up
1116+ keyClick(Qt.Key_Backtab);
1117+ assertFocusOnIndex(-1); // BFB
1118+
1119+ // The list should wrap around
1120+ keyClick(Qt.Key_Up);
1121+ waitForRendering(launcher);
1122+ assertFocusOnIndex(last);
1123+
1124+ keyClick(Qt.Key_Down);
1125+ waitForRendering(launcher);
1126+ keyClick(Qt.Key_Down);
1127+ assertFocusOnIndex(0); // Back to Top
1128+
1129+ // Right opens the quicklist
1130+ keyClick(Qt.Key_Right);
1131+ assertFocusOnIndex(0); // Navigating the quicklist... the launcher focus should not move
1132+ tryCompare(quickList, "visible", true);
1133+ tryCompare(quickList, "selectedIndex", 0)
1134+
1135+ // Down should move down the quicklist
1136+ keyClick(Qt.Key_Down);
1137+ tryCompare(quickList, "selectedIndex", 1)
1138+
1139+ // The quicklist should wrap around too
1140+ keyClick(Qt.Key_Down);
1141+ keyClick(Qt.Key_Down);
1142+ keyClick(Qt.Key_Down);
1143+ tryCompare(quickList, "selectedIndex", 0)
1144+
1145+ // Left gets us back to the launcher
1146+ keyClick(Qt.Key_Left);
1147+ assertFocusOnIndex(0);
1148+ tryCompare(quickList, "visible", false);
1149+
1150+ // Launcher navigation should still work
1151+ // Go bar to top by wrapping around
1152+ keyClick(Qt.Key_Down);
1153+ assertFocusOnIndex(1);
1154+
1155+ keyClick(Qt.Key_Enter);
1156+ assertFocusOnIndex(-2);
1157+ }
1158+
1159+ function test_selectQuicklistItemByKeyboard() {
1160+ launcher.openForKeyboardNavigation();
1161+ waitForRendering(launcher);
1162+
1163+ signalSpy.clear();
1164+ signalSpy.signalName = "quickListTriggered"
1165+
1166+ keyClick(Qt.Key_Down); // Down to launcher item 0
1167+ keyClick(Qt.Key_Down); // Down to launcher item 1
1168+ keyClick(Qt.Key_Right); // Into quicklist
1169+ keyClick(Qt.Key_Down); // Down to quicklist item 1
1170+ keyClick(Qt.Key_Down); // Down to quicklist item 2
1171+ keyClick(Qt.Key_Enter); // Trigger it
1172+
1173+ compare(signalSpy.count, 1, "Quicklist signal wasn't triggered")
1174+ compare(signalSpy.signalArguments[0][0], LauncherModel.get(1).appId)
1175+ compare(signalSpy.signalArguments[0][1], 2)
1176+ }
1177+
1178+ function test_cancelKbdNavigationWitMouse() {
1179+ launcher.openForKeyboardNavigation();
1180+ waitForRendering(launcher);
1181+ var launcherPanel = findChild(launcher, "launcherPanel");
1182+ tryCompare(launcherPanel, "x", 0);
1183+
1184+ var quickList = findChild(launcher, "quickList");
1185+
1186+ keyClick(Qt.Key_Down); // Down to launcher item 0
1187+ keyClick(Qt.Key_Down); // Down to launcher item 1
1188+ keyClick(Qt.Key_Right); // Into quicklist
1189+
1190+ waitForRendering(launcher)
1191+ tryCompare(quickList, "visible", true)
1192+
1193+ mouseClick(root, root.width / 2, units.gu(2));
1194+
1195+ tryCompare(launcher, "state", "");
1196+ tryCompare(launcherPanel, "highlightIndex", -2);
1197+ }
10471198 }
10481199}
tests/qmltests/tst_Shell.qml+114−25
@@ -26,6 +26,7 @@ import Unity.Application 0.1
2626import Unity.Connectivity 0.1
2727import Unity.Indicators 0.1
2828import Unity.Notifications 1.0
29+import Unity.Launcher 0.1
2930import Unity.Test 0.1
3031import Powerd 0.1
3132import Wizard 0.1 as Wizard
@@ -121,10 +122,6 @@ Rectangle {
121122 Component.onDestruction: {
122123 shellLoader.itemDestroyed = true;
123124 }
124− Component.onCompleted: {
125− var keyMapper = testCase.findChild(__shell, "physicalKeysMapper");
126− keyMapper.controlInsteadOfAlt = true;
127− }
128125 }
129126 }
130127 }
@@ -213,6 +210,18 @@ Rectangle {
213210 checked: true
214211 color: "white"
215212 }
213+ ListItem.ItemSelector {
214+ id: ctrlModifier
215+ anchors { left: parent.left; right: parent.right }
216+ activeFocusOnPress: false
217+ text: "Ctrl key as"
218+ model: ["Ctrl", "Alt", "Super"]
219+ onSelectedIndexChanged: {
220+ var keyMapper = testCase.findChild(shellContainer, "physicalKeysMapper");
221+ keyMapper.controlInsteadOfAlt = selectedIndex == 1;
222+ keyMapper.controlInsteadOfSuper = selectedIndex == 2;
223+ }
224+ }
216225
217226 Label { text: "Applications"; font.bold: true }
218227
@@ -1441,7 +1450,7 @@ Rectangle {
14411450
14421451 // Do a quick alt-tab and see if focus changes
14431452 tryCompare(app3.session.lastSurface, "activeFocus", true)
1444− keyClick(Qt.Key_Tab, Qt.ControlModifier)
1453+ keyClick(Qt.Key_Tab, Qt.AltModifier)
14451454 tryCompare(app2.session.lastSurface, "activeFocus", true)
14461455
14471456 var desktopSpread = findChild(shell, "spread")
@@ -1449,12 +1458,12 @@ Rectangle {
14491458 tryCompare(desktopSpread, "state", "")
14501459
14511460 // Just press Alt, make sure the spread comes up
1452− keyPress(Qt.Key_Control);
1461+ keyPress(Qt.Key_Alt);
14531462 keyClick(Qt.Key_Tab);
14541463 tryCompare(desktopSpread, "state", "altTab")
14551464
14561465 // Release control, check if spread disappears
1457− keyRelease(Qt.Key_Control)
1466+ keyRelease(Qt.Key_Alt)
14581467 tryCompare(desktopSpread, "state", "")
14591468
14601469 // Focus should have switched back now
@@ -1482,7 +1491,7 @@ Rectangle {
14821491 tryCompare(desktopSpread, "state", "")
14831492
14841493 // Just press Alt, make sure the spread comes up
1485− keyPress(Qt.Key_Control);
1494+ keyPress(Qt.Key_Alt);
14861495 keyClick(Qt.Key_Tab);
14871496 tryCompare(desktopSpread, "state", "altTab")
14881497 tryCompare(spreadRepeater, "highlightedIndex", 1)
@@ -1503,7 +1512,7 @@ Rectangle {
15031512 tryCompare(spreadRepeater, "highlightedIndex", 0)
15041513
15051514 // Release control, check if spread disappears
1506− keyRelease(Qt.Key_Control)
1515+ keyRelease(Qt.Key_Alt)
15071516 tryCompare(desktopSpread, "state", "")
15081517
15091518 // Make sure that after wrapping around once, we have the same one focused as at the beginning
@@ -1516,7 +1525,7 @@ Rectangle {
15161525 var spreadRepeater = findInvisibleChild(shell, "spreadRepeater");
15171526 verify(spreadRepeater !== null);
15181527
1519− keyPress(Qt.Key_Control)
1528+ keyPress(Qt.Key_Alt)
15201529 keyClick(Qt.Key_Tab);
15211530 tryCompare(spreadRepeater, "highlightedIndex", 1);
15221531
@@ -1538,7 +1547,7 @@ Rectangle {
15381547 keyClick(Qt.Key_Backtab);
15391548 tryCompare(spreadRepeater, "highlightedIndex", 1);
15401549
1541− keyRelease(Qt.Key_Control);
1550+ keyRelease(Qt.Key_Alt);
15421551 }
15431552
15441553 function test_highlightFollowsMouse() {
@@ -1547,7 +1556,7 @@ Rectangle {
15471556 var spreadRepeater = findInvisibleChild(shell, "spreadRepeater");
15481557 verify(spreadRepeater !== null);
15491558
1550− keyPress(Qt.Key_Control)
1559+ keyPress(Qt.Key_Alt)
15511560 keyClick(Qt.Key_Tab);
15521561
15531562 tryCompare(spreadRepeater, "highlightedIndex", 1);
@@ -1566,7 +1575,7 @@ Rectangle {
15661575
15671576 verify(y < 4000);
15681577
1569− keyRelease(Qt.Key_Control);
1578+ keyRelease(Qt.Key_Alt);
15701579 }
15711580
15721581 function test_closeFromSpread() {
@@ -1575,7 +1584,7 @@ Rectangle {
15751584 var spreadRepeater = findInvisibleChild(shell, "spreadRepeater");
15761585 verify(spreadRepeater !== null);
15771586
1578− keyPress(Qt.Key_Control)
1587+ keyPress(Qt.Key_Alt)
15791588 keyClick(Qt.Key_Tab);
15801589
15811590 appRemovedSpy.clear();
@@ -1602,7 +1611,7 @@ Rectangle {
16021611 tryCompare(appRemovedSpy, "count", 1)
16031612 compare(appRemovedSpy.signalArguments[0][0], closedAppId);
16041613
1605− keyRelease(Qt.Key_Control);
1614+ keyRelease(Qt.Key_Alt);
16061615 }
16071616
16081617 function test_selectFromSpreadWithMouse_data() {
@@ -1622,7 +1631,7 @@ Rectangle {
16221631 var spreadRepeater = findInvisibleChild(shell, "spreadRepeater");
16231632 verify(spreadRepeater !== null);
16241633
1625− keyPress(Qt.Key_Control)
1634+ keyPress(Qt.Key_Alt)
16261635 keyClick(Qt.Key_Tab);
16271636
16281637 var focusAppId = ApplicationManager.get(2).appId;
@@ -1649,7 +1658,7 @@ Rectangle {
16491658 tryCompare(stage, "state", "");
16501659 tryCompare(ApplicationManager, "focusedApplicationId", focusAppId);
16511660
1652− keyRelease(Qt.Key_Control);
1661+ keyRelease(Qt.Key_Alt);
16531662 }
16541663
16551664 function test_progressiveAutoScrolling() {
@@ -1658,7 +1667,7 @@ Rectangle {
16581667 var appRepeater = findInvisibleChild(shell, "appRepeater");
16591668 verify(appRepeater !== null);
16601669
1661− keyPress(Qt.Key_Control)
1670+ keyPress(Qt.Key_Alt)
16621671 keyClick(Qt.Key_Tab);
16631672
16641673 var spreadFlickable = findChild(shell, "spreadFlickable")
@@ -1684,7 +1693,7 @@ Rectangle {
16841693 }
16851694 tryCompare(spreadFlickable, "contentX", 0);
16861695
1687− keyRelease(Qt.Key_Control);
1696+ keyRelease(Qt.Key_Alt);
16881697 }
16891698
16901699 // This makes sure the hoverMouseArea is set to invisible AND disabled
@@ -1696,13 +1705,13 @@ Rectangle {
16961705 tryCompare(hoverMouseArea, "enabled", false)
16971706 tryCompare(hoverMouseArea, "visible", false)
16981707
1699− keyPress(Qt.Key_Control)
1708+ keyPress(Qt.Key_Alt)
17001709 keyClick(Qt.Key_Tab);
17011710
17021711 tryCompare(hoverMouseArea, "enabled", true)
17031712 tryCompare(hoverMouseArea, "visible", true)
17041713
1705− keyRelease(Qt.Key_Control)
1714+ keyRelease(Qt.Key_Alt)
17061715
17071716 tryCompare(hoverMouseArea, "enabled", false)
17081717 tryCompare(hoverMouseArea, "visible", false)
@@ -1719,7 +1728,7 @@ Rectangle {
17191728 var appRepeater = findInvisibleChild(shell, "appRepeater");
17201729 verify(appRepeater !== null);
17211730
1722− keyPress(Qt.Key_Control)
1731+ keyPress(Qt.Key_Alt)
17231732 keyClick(Qt.Key_Tab);
17241733
17251734 tryCompare(spreadRepeater, "highlightedIndex", 1);
@@ -1740,7 +1749,7 @@ Rectangle {
17401749
17411750 verify(y < 4000);
17421751
1743− keyRelease(Qt.Key_Control);
1752+ keyRelease(Qt.Key_Alt);
17441753 }
17451754
17461755 function test_focusAppFromLauncherExitsSpread() {
@@ -1750,7 +1759,7 @@ Rectangle {
17501759 var launcher = findChild(shell, "launcher");
17511760 var bfb = findChild(launcher, "buttonShowDashHome");
17521761
1753− keyPress(Qt.Key_Control)
1762+ keyPress(Qt.Key_Alt)
17541763 keyClick(Qt.Key_Tab);
17551764
17561765 tryCompare(desktopSpread, "state", "altTab")
@@ -1766,7 +1775,7 @@ Rectangle {
17661775
17671776 tryCompare(ApplicationManager, "focusedApplicationId", "unity8-dash")
17681777
1769− keyRelease(Qt.Key_Control);
1778+ keyRelease(Qt.Key_Alt);
17701779 }
17711780
17721781 // regression test for http://pad.lv/1443319
@@ -1989,6 +1998,86 @@ Rectangle {
19891998 }
19901999 }
19912000
2001+ function test_superTabToCycleLauncher() {
2002+ loadShell("desktop");
2003+ shell.usageScenario = "desktop";
2004+ waitForRendering(shell);
2005+
2006+ var launcher = findChild(shell, "launcher");
2007+ var launcherPanel = findChild(launcher, "launcherPanel");
2008+ var firstAppInLauncher = LauncherModel.get(0).appId;
2009+
2010+ compare(launcher.state, "");
2011+ compare(launcherPanel.highlightIndex, -2);
2012+ compare(ApplicationManager.focusedApplicationId, "unity8-dash");
2013+
2014+ // Use Super + Tab Tab to cycle to the first entry in the launcher
2015+ keyPress(Qt.Key_Super_L, Qt.MetaModifier);
2016+ keyClick(Qt.Key_Tab);
2017+ tryCompare(launcher, "state", "visible");
2018+ tryCompare(launcherPanel, "highlightIndex", -1);
2019+ keyClick(Qt.Key_Tab);
2020+ tryCompare(launcherPanel, "highlightIndex", 0);
2021+ keyRelease(Qt.Key_Super_L, Qt.MetaModifier);
2022+ tryCompare(launcher, "state", "");
2023+ tryCompare(launcherPanel, "highlightIndex", -2);
2024+ tryCompare(ApplicationManager, "focusedApplicationId", firstAppInLauncher);
2025+
2026+ // Now go back to the dash
2027+ keyPress(Qt.Key_Super_L, Qt.MetaModifier);
2028+ keyClick(Qt.Key_Tab);
2029+ tryCompare(launcher, "state", "visible");
2030+ tryCompare(launcherPanel, "highlightIndex", -1);
2031+ keyRelease(Qt.Key_Super_L, Qt.MetaModifier);
2032+ tryCompare(launcher, "state", "");
2033+ tryCompare(launcherPanel, "highlightIndex", -2);
2034+ tryCompare(ApplicationManager, "focusedApplicationId", "unity8-dash");
2035+ }
2036+
2037+ function test_longpressSuperOpensLauncher() {
2038+ loadShell("desktop");
2039+ var launcher = findChild(shell, "launcher");
2040+ var shortcutHint = findChild(findChild(launcher, "launcherDelegate0"), "shortcutHint")
2041+
2042+ compare(launcher.state, "");
2043+ keyPress(Qt.Key_Super_L, Qt.MetaModifier);
2044+ tryCompare(launcher, "state", "visible");
2045+ tryCompare(shortcutHint, "visible", true);
2046+
2047+ keyRelease(Qt.Key_Super_L, Qt.MetaModifier);
2048+ tryCompare(launcher, "state", "");
2049+ tryCompare(shortcutHint, "visible", false);
2050+ }
2051+
2052+ function test_metaNumberLaunchesFromLauncher_data() {
2053+ return [
2054+ {tag: "Meta+1", key: Qt.Key_1, index: 0},
2055+ {tag: "Meta+2", key: Qt.Key_2, index: 1},
2056+ {tag: "Meta+4", key: Qt.Key_5, index: 4},
2057+ {tag: "Meta+0", key: Qt.Key_0, index: 9},
2058+ ]
2059+ }
2060+
2061+ function test_metaNumberLaunchesFromLauncher(data) {
2062+ loadShell("desktop");
2063+ var launcher = findChild(shell, "launcher");
2064+ var appId = LauncherModel.get(data.index).appId;
2065+ waitForRendering(shell);
2066+
2067+ keyClick(data.key, Qt.MetaModifier);
2068+ tryCompare(ApplicationManager, "focusedApplicationId", appId);
2069+ }
2070+
2071+ function test_altF1OpensLauncherForKeyboardNavigation() {
2072+ loadShell("desktop");
2073+ waitForRendering(shell);
2074+ var launcher = findChild(shell, "launcher");
2075+
2076+ keyClick(Qt.Key_F1, Qt.AltModifier);
2077+ tryCompare(launcher, "state", "visible");
2078+ tryCompare(launcher, "focus", true)
2079+ }
2080+
19922081 function test_inputEventsOnEdgesEndUpInAppSurface_data() {
19932082 return [
19942083 { tag: "phone", repeaterName: "spreadRepeater" },