on xenial help sign in
Implement Launcher's keyboard navigation and updated pip design
82e4b97Michael Zanetti
qml/Components/PhysicalKeysMapper.qml+24
@@ -42,12 +42,15 @@ Item {
42 signal screenshotTriggered;
42 signal screenshotTriggered;
43
43
44 readonly property bool altTabPressed: d.altTabPressed
44 readonly property bool altTabPressed: d.altTabPressed
45+ readonly property bool superPressed: d.superPressed
46+ readonly property bool superTabPressed: d.superTabPressed
45
47
46 property int powerKeyLongPressTime: 2000
48 property int powerKeyLongPressTime: 2000
47
49
48 // For testing. If running windowed (e.g. tryShell), Alt+Tab is taken by the
50 // For testing. If running windowed (e.g. tryShell), Alt+Tab is taken by the
49 // running desktop, set this to true to use Ctrl+Tab instead.
51 // running desktop, set this to true to use Ctrl+Tab instead.
50 property bool controlInsteadOfAlt: false
52 property bool controlInsteadOfAlt: false
53+ property bool controlInsteadOfSuper: false
51
54
52 QtObject {
55 QtObject {
53 id: d
56 id: d
@@ -59,6 +62,9 @@ Item {
59 property bool altPressed: false
62 property bool altPressed: false
60 property bool altTabPressed: false
63 property bool altTabPressed: false
61
64
65+ property bool superPressed: false
66+ property bool superTabPressed: false
67+
62 property var powerButtonPressStart: 0
68 property var powerButtonPressStart: 0
63
69
64 // We need to eat ALT presses until we know what they're for (Alt+Tab or going to the app?)
70 // 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 {
119 event.accepted = true;
125 event.accepted = true;
120 d.altPressInjected = false;
126 d.altPressInjected = false;
121 }
127 }
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;
122 } else if (event.key == Qt.Key_Tab) {
136 } else if (event.key == Qt.Key_Tab) {
123 if (d.altPressed && !d.altTabPressed) {
137 if (d.altPressed && !d.altTabPressed) {
124 d.altTabPressed = true;
138 d.altTabPressed = true;
125 event.accepted = true;
139 event.accepted = true;
126 }
140 }
141+ if (d.superPressed && !d.superTabPressed) {
142+ d.superTabPressed = true;
143+ event.accepted = true;
144+ }
127 }
145 }
128 }
146 }
129
147
@@ -154,6 +172,12 @@ Item {
154 if (d.altTabPressed) {
172 if (d.altTabPressed) {
155 event.accepted = true;
173 event.accepted = true;
156 }
174 }
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+ }
157 }
181 }
158 }
182 }
159}
183}
qml/Launcher/Launcher.qml+112−1
@@ -19,8 +19,9 @@ import "../Components"
19import Ubuntu.Components 1.3
19import Ubuntu.Components 1.3
20import Ubuntu.Gestures 0.1
20import Ubuntu.Gestures 0.1
21import Unity.Launcher 0.1
21import Unity.Launcher 0.1
22+import GlobalShortcut 1.0
22
23
23−Item {
24+FocusScope {
24 id: root
25 id: root
25
26
26 property bool autohideEnabled: false
27 property bool autohideEnabled: false
@@ -34,6 +35,9 @@ Item {
34 property real progress: dragArea.dragging && dragArea.touchX > panelWidth ?
35 property real progress: dragArea.dragging && dragArea.touchX > panelWidth ?
35 (width * (dragArea.touchX-panelWidth) / (width - panelWidth)) : 0
36 (width * (dragArea.touchX-panelWidth) / (width - panelWidth)) : 0
36
37
38+ property bool superPressed: false
39+ property bool superTabPressed: false
40+
37 readonly property bool dragging: dragArea.dragging
41 readonly property bool dragging: dragArea.dragging
38 readonly property real dragDistance: dragArea.dragging ? dragArea.touchX : 0
42 readonly property real dragDistance: dragArea.dragging ? dragArea.touchX : 0
39 readonly property real visibleWidth: panel.width + panel.x
43 readonly property real visibleWidth: panel.width + panel.x
@@ -57,6 +61,37 @@ Item {
57 }
61 }
58 }
62 }
59
63
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+
60 function hide() {
95 function hide() {
61 switchToNextState("")
96 switchToNextState("")
62 }
97 }
@@ -90,6 +125,77 @@ Item {
90 }
125 }
91 }
126 }
92
127
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+
93 Timer {
199 Timer {
94 id: teaseTimer
200 id: teaseTimer
95 interval: mode == "teasing" ? 200 : 300
201 interval: mode == "teasing" ? 200 : 300
@@ -254,6 +360,11 @@ Item {
254 }
360 }
255 }
361 }
256
362
363+ onKbdNavigationCancelled: {
364+ root.state = "";
365+ root.focus = false;
366+ }
367+
257 Behavior on x {
368 Behavior on x {
258 enabled: !dragArea.dragging && !launcherDragArea.drag.active && panel.animate;
369 enabled: !dragArea.dragging && !launcherDragArea.drag.active && panel.animate;
259 NumberAnimation {
370 NumberAnimation {
qml/Launcher/LauncherDelegate.qml+46−8
@@ -20,6 +20,7 @@ import Ubuntu.Components 1.3
20Item {
20Item {
21 id: root
21 id: root
22
22
23+ property int itemIndex: 0
23 property string iconName
24 property string iconName
24 property int count: 0
25 property int count: 0
25 property bool countVisible: false
26 property bool countVisible: false
@@ -29,10 +30,12 @@ Item {
29 property real maxAngle: 0
30 property real maxAngle: 0
30 property bool inverted: false
31 property bool inverted: false
31 property bool alerting: false
32 property bool alerting: false
32− readonly property alias wiggling: wiggleAnim.running
33+ property bool highlighted: false
34+ property bool shortcutHintShown: false
33
35
34 readonly property int effectiveHeight: Math.cos(angle * Math.PI / 180) * itemHeight
36 readonly property int effectiveHeight: Math.cos(angle * Math.PI / 180) * itemHeight
35 readonly property real foldedHeight: Math.cos(maxAngle * Math.PI / 180) * itemHeight
37 readonly property real foldedHeight: Math.cos(maxAngle * Math.PI / 180) * itemHeight
38+ readonly property alias wiggling: wiggleAnim.running
36
39
37 property int itemWidth
40 property int itemWidth
38 property int itemHeight
41 property int itemHeight
@@ -125,6 +128,17 @@ Item {
125 height: parent.itemHeight + units.gu(1)
128 height: parent.itemHeight + units.gu(1)
126 anchors.centerIn: parent
129 anchors.centerIn: parent
127
130
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+
128 ProportionalShape {
142 ProportionalShape {
129 id: iconShape
143 id: iconShape
130 anchors.centerIn: parent
144 anchors.centerIn: parent
@@ -206,25 +220,49 @@ Item {
206 }
220 }
207 }
221 }
208
222
209− Image {
223+ Column {
210− objectName: "runningHighlight"
211 anchors {
224 anchors {
212 left: parent.left
225 left: parent.left
213 verticalCenter: parent.verticalCenter
226 verticalCenter: parent.verticalCenter
214 }
227 }
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"
215− visible: root.itemRunning
236+ visible: root.itemRunning
216− rotation: 180
237+ }
217− source: "graphics/focused_app_arrow.png"
238+ }
218 }
239 }
219
240
220− Image {
241+ Rectangle {
221 objectName: "focusedHighlight"
242 objectName: "focusedHighlight"
222 anchors {
243 anchors {
223 right: parent.right
244 right: parent.right
224 verticalCenter: parent.verticalCenter
245 verticalCenter: parent.verticalCenter
225 }
246 }
247+ width: units.gu(0.25)
248+ height: units.gu(.5)
249+ color: "white"
226 visible: root.itemFocused
250 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+ }
228 }
266 }
229 }
267 }
230
268
qml/Launcher/LauncherPanel.qml+103−32
@@ -19,12 +19,13 @@ import Ubuntu.Components 1.3
19import Ubuntu.Components.ListItems 1.3 as ListItems
19import Ubuntu.Components.ListItems 1.3 as ListItems
20import Unity.Launcher 0.1
20import Unity.Launcher 0.1
21import Ubuntu.Components.Popups 1.3
21import Ubuntu.Components.Popups 1.3
22+import GlobalShortcut 1.0
22import "../Components/ListItems"
23import "../Components/ListItems"
23import "../Components/"
24import "../Components/"
24
25
25Rectangle {
26Rectangle {
26 id: root
27 id: root
27− color: "#B2000000"
28+ color: "#E0292929"
28
29
29 rotation: inverted ? 180 : 0
30 rotation: inverted ? 180 : 0
30
31
@@ -33,11 +34,13 @@ Rectangle {
33 property bool dragging: false
34 property bool dragging: false
34 property bool moving: launcherListView.moving || launcherListView.flicking
35 property bool moving: launcherListView.moving || launcherListView.flicking
35 property bool preventHiding: moving || dndArea.draggedIndex >= 0 || quickList.state === "open" || dndArea.pressed
36 property bool preventHiding: moving || dndArea.draggedIndex >= 0 || quickList.state === "open" || dndArea.pressed
36− || mouseEventEater.containsMouse || dashItem.hovered
37+ || mouseEventEater.containsMouse || dashItem.hovered
37− property int highlightIndex: -1
38+ property int highlightIndex: -2
39+ property bool shortcutHintsShown: false
38
40
39 signal applicationSelected(string appId)
41 signal applicationSelected(string appId)
40 signal showDashHome()
42 signal showDashHome()
43+ signal kbdNavigationCancelled()
41
44
42 onXChanged: {
45 onXChanged: {
43 if (quickList.state == "open") {
46 if (quickList.state == "open") {
@@ -45,6 +48,26 @@ Rectangle {
45 }
48 }
46 }
49 }
47
50
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+
48 MouseArea {
71 MouseArea {
49 id: mouseEventEater
72 id: mouseEventEater
50 anchors.fill: parent
73 anchors.fill: parent
@@ -57,20 +80,12 @@ Rectangle {
57 fill: parent
80 fill: parent
58 }
81 }
59
82
60− Item {
83+ Rectangle {
61 objectName: "buttonShowDashHome"
84 objectName: "buttonShowDashHome"
62 width: parent.width
85 width: parent.width
63 height: units.gu(7)
86 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
87+ color: UbuntuColors.orange
73− }
88+ readonly property bool highlighted: root.highlightIndex == -1;
74
89
75 Image {
90 Image {
76 objectName: "dashItem"
91 objectName: "dashItem"
@@ -85,6 +100,14 @@ Rectangle {
85 anchors.fill: parent
100 anchors.fill: parent
86 onClicked: root.showDashHome()
101 onClicked: root.showDashHome()
87 }
102 }
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+ }
88 }
111 }
89
112
90 Item {
113 Item {
@@ -179,6 +202,16 @@ Rectangle {
179 to = contentY;
202 to = contentY;
180 start();
203 start();
181 }
204 }
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+ }
182 }
215 }
183
216
184 displaced: Transition {
217 displaced: Transition {
@@ -192,6 +225,7 @@ Rectangle {
192 // the right app when running autopilot tests for
225 // the right app when running autopilot tests for
193 // multiple apps.
226 // multiple apps.
194 readonly property string appId: model.appId
227 readonly property string appId: model.appId
228+ itemIndex: index
195 itemHeight: launcherListView.itemHeight
229 itemHeight: launcherListView.itemHeight
196 itemWidth: launcherListView.itemWidth
230 itemWidth: launcherListView.itemWidth
197 width: itemWidth
231 width: itemWidth
@@ -204,6 +238,8 @@ Rectangle {
204 itemFocused: model.focused
238 itemFocused: model.focused
205 inverted: root.inverted
239 inverted: root.inverted
206 alerting: model.alerting
240 alerting: model.alerting
241+ highlighted: root.highlightIndex == index
242+ shortcutHintShown: root.shortcutHintsShown && index <= 9
207 z: -Math.abs(offset)
243 z: -Math.abs(offset)
208 maxAngle: 55
244 maxAngle: 55
209 property bool dragging: false
245 property bool dragging: false
@@ -241,14 +277,7 @@ Rectangle {
241 onAlertingChanged: {
277 onAlertingChanged: {
242 if(alerting) {
278 if(alerting) {
243 if (!dragging && (launcherListView.peekingIndex === -1 || launcher.visibleWidth > 0)) {
279 if (!dragging && (launcherListView.peekingIndex === -1 || launcher.visibleWidth > 0)) {
244− var itemPosition = index * launcherListView.itemHeight;
280+ moveAnimation.moveToIndex(index)
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− }
252 if (!dragging && launcher.state !== "visible") {
281 if (!dragging && launcher.state !== "visible") {
253 peekingAnimation.start()
282 peekingAnimation.start()
254 }
283 }
@@ -402,10 +431,7 @@ Rectangle {
402
431
403 if (mouse.button & Qt.RightButton) { // context menu
432 if (mouse.button & Qt.RightButton) { // context menu
404 // Opening QuickList
433 // Opening QuickList
405− quickList.item = clickedItem;
434+ quickList.open(index);
406− quickList.model = launcherListView.model.get(index).quickList;
407− quickList.appId = launcherListView.model.get(index).appId;
408− quickList.state = "open";
409 return;
435 return;
410 }
436 }
411
437
@@ -481,11 +507,7 @@ Rectangle {
481
507
482 draggedIndex = Math.floor((mouse.y + launcherListView.realContentY) / launcherListView.realItemHeight);
508 draggedIndex = Math.floor((mouse.y + launcherListView.realContentY) / launcherListView.realItemHeight);
483
509
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)
489
511
490 launcherListView.interactive = false
512 launcherListView.interactive = false
491
513
@@ -644,7 +666,10 @@ Rectangle {
644 enabled: quickList.state == "open" || pressed
666 enabled: quickList.state == "open" || pressed
645
667
646 onClicked: {
668 onClicked: {
647− quickList.state = ""
669+ quickList.state = "";
670+ quickList.focus = false;
671+ root.highlightIndex = -2;
672+ root.kbdNavigationCancelled();
648 }
673 }
649
674
650 // Forward for dragging to work when quickList is open
675 // Forward for dragging to work when quickList is open
@@ -693,12 +718,55 @@ Rectangle {
693 property var model
718 property var model
694 property string appId
719 property string appId
695 property var item
720 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+ }
696
755
697 // internal
756 // internal
698 property int itemCenter: item ? root.mapFromItem(quickList.item).y + (item.height / 2) + quickList.item.offset : units.gu(1)
757 property int itemCenter: item ? root.mapFromItem(quickList.item).y + (item.height / 2) + quickList.item.offset : units.gu(1)
699 property int offset: itemCenter + (height/2) + units.gu(1) > parent.height ? -itemCenter - (height/2) - units.gu(1) + parent.height :
758 property int offset: itemCenter + (height/2) + units.gu(1) > parent.height ? -itemCenter - (height/2) - units.gu(1) + parent.height :
700 itemCenter - (height/2) < units.gu(1) ? (height/2) - itemCenter + units.gu(1) : 0
759 itemCenter - (height/2) < units.gu(1) ? (height/2) - itemCenter + units.gu(1) : 0
701
760
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+
702 Column {
770 Column {
703 id: quickListColumn
771 id: quickListColumn
704 width: parent.width
772 width: parent.width
@@ -712,6 +780,7 @@ Rectangle {
712 objectName: "quickListEntry" + index
780 objectName: "quickListEntry" + index
713 text: (model.clickable ? "" : "<b>") + model.label + (model.clickable ? "" : "</b>")
781 text: (model.clickable ? "" : "<b>") + model.label + (model.clickable ? "" : "</b>")
714 highlightWhenPressed: model.clickable
782 highlightWhenPressed: model.clickable
783+ selected: index === quickList.selectedIndex
715
784
716 // FIXME: This is a workaround for the theme not being context sensitive. I.e. the
785 // FIXME: This is a workaround for the theme not being context sensitive. I.e. the
717 // ListItems don't know that they are sitting in a themed Popover where the color
786 // ListItems don't know that they are sitting in a themed Popover where the color
@@ -727,6 +796,8 @@ Rectangle {
727 // Unsetting model to prevent showing changing entries during fading out
796 // Unsetting model to prevent showing changing entries during fading out
728 // that may happen because of triggering an action.
797 // that may happen because of triggering an action.
729 LauncherModel.quickListActionInvoked(quickList.appId, index);
798 LauncherModel.quickListActionInvoked(quickList.appId, index);
799+ quickList.focus = false;
800+ root.kbdNavigationCancelled();
730 quickList.model = undefined;
801 quickList.model = undefined;
731 }
802 }
732 }
803 }
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 {
557 && !greeter.hasLockedApp
557 && !greeter.hasLockedApp
558 inverted: shell.usageScenario !== "desktop"
558 inverted: shell.usageScenario !== "desktop"
559 shadeBackground: !tutorial.running
559 shadeBackground: !tutorial.running
560+ superPressed: physicalKeysMapper.superPressed
561+ superTabPressed: physicalKeysMapper.superTabPressed
560
562
561 onShowDashHome: showHome()
563 onShowDashHome: showHome()
562 onDash: showDash()
564 onDash: showDash()
@@ -576,6 +578,37 @@ Item {
576 panel.indicators.hide()
578 panel.indicators.hide()
577 }
579 }
578 }
580 }
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+ }
579 }
612 }
580
613
581 Wizard {
614 Wizard {
tests/mocks/Unity/Launcher/MockLauncherModel.cpp+2
@@ -25,6 +25,7 @@ MockLauncherModel::MockLauncherModel(QObject* parent): LauncherModelInterface(pa
25 MockLauncherItem *item = new MockLauncherItem("dialer-app", "/usr/share/applications/dialer-app.desktop", "Dialer", "dialer-app", this);
25 MockLauncherItem *item = new MockLauncherItem("dialer-app", "/usr/share/applications/dialer-app.desktop", "Dialer", "dialer-app", this);
26 item->setProgress(0);
26 item->setProgress(0);
27 item->setPinned(true);
27 item->setPinned(true);
28+ item->setRunning(true);
28 item->setFocused(true);
29 item->setFocused(true);
29 m_list.append(item);
30 m_list.append(item);
30 item = new MockLauncherItem("camera-app", "/usr/share/applications/camera-app.desktop", "Camera", "camera", this);
31 item = new MockLauncherItem("camera-app", "/usr/share/applications/camera-app.desktop", "Camera", "camera", this);
@@ -34,6 +35,7 @@ MockLauncherModel::MockLauncherModel(QObject* parent): LauncherModelInterface(pa
34 item = new MockLauncherItem("gallery-app", "/usr/share/applications/gallery-app.desktop", "Gallery", "gallery", this);
35 item = new MockLauncherItem("gallery-app", "/usr/share/applications/gallery-app.desktop", "Gallery", "gallery", this);
35 item->setProgress(50);
36 item->setProgress(50);
36 item->setCountVisible(true);
37 item->setCountVisible(true);
38+ item->setRunning(true);
37 item->setAlerting(false);
39 item->setAlerting(false);
38 m_list.append(item);
40 m_list.append(item);
39 item = new MockLauncherItem("music-app", "/usr/share/applications/music-app.desktop", "Music", "soundcloud", this);
41 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 {
34 Loader {
34 Loader {
35 id: launcherLoader
35 id: launcherLoader
36 anchors.fill: parent
36 anchors.fill: parent
37+ focus: true
37 property bool itemDestroyed: false
38 property bool itemDestroyed: false
38 sourceComponent: Component {
39 sourceComponent: Component {
39 Launcher {
40 Launcher {
@@ -68,6 +69,7 @@ Item {
68
69
69 Component.onCompleted: {
70 Component.onCompleted: {
70 launcherLoader.itemDestroyed = false;
71 launcherLoader.itemDestroyed = false;
72+ launcherLoader.focus = true
71 edgeBarrierControls.target = testCase.findChild(this, "edgeBarrierController");
73 edgeBarrierControls.target = testCase.findChild(this, "edgeBarrierController");
72 }
74 }
73 Component.onDestruction: {
75 Component.onDestruction: {
@@ -102,6 +104,15 @@ Item {
102 Layout.fillWidth: true
104 Layout.fillWidth: true
103 }
105 }
104
106
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+
105 Row {
116 Row {
106 spacing: units.gu(1)
117 spacing: units.gu(1)
107
118
@@ -276,6 +287,24 @@ Item {
276 }
287 }
277 }
288 }
278
289
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+
279 // Drag from the left edge of the screen rightwards and check that the launcher
308 // Drag from the left edge of the screen rightwards and check that the launcher
280 // appears (as if being dragged by the finger/pointer)
309 // appears (as if being dragged by the finger/pointer)
281 function test_dragLeftEdgeToRevealLauncherAndTapCenterToDismiss() {
310 function test_dragLeftEdgeToRevealLauncherAndTapCenterToDismiss() {
@@ -290,10 +319,10 @@ Item {
290 dragLauncherIntoView()
319 dragLauncherIntoView()
291
320
292 // tapping on the center of the screen should dismiss the launcher
321 // 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)
294
323
295 // should eventually get fully retracted (hidden)
324 // should eventually get fully retracted (hidden)
296− tryCompare(panel, "x", -launcher.panelWidth, 1000)
325+ tryCompare(panel, "x", -launcher.panelWidth, 2000)
297 }
326 }
298
327
299 /* If I click on the icon of an application on the launcher
328 /* If I click on the icon of an application on the launcher
@@ -421,7 +450,7 @@ Item {
421 var launcherListView = findChild(launcher, "launcherListView");
450 var launcherListView = findChild(launcher, "launcherListView");
422 for (var i = 0; i < launcherListView.count; ++i) {
451 for (var i = 0; i < launcherListView.count; ++i) {
423 var delegate = findChild(launcherListView, "launcherDelegate" + i)
452 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)
425 }
454 }
426 }
455 }
427
456
@@ -764,14 +793,15 @@ Item {
764 function test_launcher_dismiss() {
793 function test_launcher_dismiss() {
765 dragLauncherIntoView();
794 dragLauncherIntoView();
766 verify(launcher.state == "visible");
795 verify(launcher.state == "visible");
796+
767− mouseClick(root);
797+ mouseClick(root, root.width / 2, units.gu(1));
768 waitUntilLauncherDisappears();
798 waitUntilLauncherDisappears();
769 verify(launcher.state == "");
799 verify(launcher.state == "");
770
800
771 // and repeat, as a test for regression in lpbug#1531339
801 // and repeat, as a test for regression in lpbug#1531339
772 dragLauncherIntoView();
802 dragLauncherIntoView();
773 verify(launcher.state == "visible");
803 verify(launcher.state == "visible");
774− mouseClick(root);
804+ mouseClick(root, root.width / 2, units.gu(1));
775 waitUntilLauncherDisappears();
805 waitUntilLauncherDisappears();
776 verify(launcher.state == "");
806 verify(launcher.state == "");
777 }
807 }
@@ -1044,5 +1074,126 @@ Item {
1044 LauncherModel.setCountVisible(LauncherModel.get(1).appId, 0)
1074 LauncherModel.setCountVisible(LauncherModel.get(1).appId, 0)
1045 LauncherModel.setCount(LauncherModel.get(1).appId, oldCount)
1075 LauncherModel.setCount(LauncherModel.get(1).appId, oldCount)
1046 }
1076 }
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+ }
1047 }
1198 }
1048}
1199}
tests/qmltests/tst_Shell.qml+114−25
@@ -26,6 +26,7 @@ import Unity.Application 0.1
26import Unity.Connectivity 0.1
26import Unity.Connectivity 0.1
27import Unity.Indicators 0.1
27import Unity.Indicators 0.1
28import Unity.Notifications 1.0
28import Unity.Notifications 1.0
29+import Unity.Launcher 0.1
29import Unity.Test 0.1
30import Unity.Test 0.1
30import Powerd 0.1
31import Powerd 0.1
31import Wizard 0.1 as Wizard
32import Wizard 0.1 as Wizard
@@ -121,10 +122,6 @@ Rectangle {
121 Component.onDestruction: {
122 Component.onDestruction: {
122 shellLoader.itemDestroyed = true;
123 shellLoader.itemDestroyed = true;
123 }
124 }
124− Component.onCompleted: {
125− var keyMapper = testCase.findChild(__shell, "physicalKeysMapper");
126− keyMapper.controlInsteadOfAlt = true;
127− }
128 }
125 }
129 }
126 }
130 }
127 }
@@ -213,6 +210,18 @@ Rectangle {
213 checked: true
210 checked: true
214 color: "white"
211 color: "white"
215 }
212 }
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+ }
216
225
217 Label { text: "Applications"; font.bold: true }
226 Label { text: "Applications"; font.bold: true }
218
227
@@ -1441,7 +1450,7 @@ Rectangle {
1441
1450
1442 // Do a quick alt-tab and see if focus changes
1451 // Do a quick alt-tab and see if focus changes
1443 tryCompare(app3.session.lastSurface, "activeFocus", true)
1452 tryCompare(app3.session.lastSurface, "activeFocus", true)
1444− keyClick(Qt.Key_Tab, Qt.ControlModifier)
1453+ keyClick(Qt.Key_Tab, Qt.AltModifier)
1445 tryCompare(app2.session.lastSurface, "activeFocus", true)
1454 tryCompare(app2.session.lastSurface, "activeFocus", true)
1446
1455
1447 var desktopSpread = findChild(shell, "spread")
1456 var desktopSpread = findChild(shell, "spread")
@@ -1449,12 +1458,12 @@ Rectangle {
1449 tryCompare(desktopSpread, "state", "")
1458 tryCompare(desktopSpread, "state", "")
1450
1459
1451 // Just press Alt, make sure the spread comes up
1460 // Just press Alt, make sure the spread comes up
1452− keyPress(Qt.Key_Control);
1461+ keyPress(Qt.Key_Alt);
1453 keyClick(Qt.Key_Tab);
1462 keyClick(Qt.Key_Tab);
1454 tryCompare(desktopSpread, "state", "altTab")
1463 tryCompare(desktopSpread, "state", "altTab")
1455
1464
1456 // Release control, check if spread disappears
1465 // Release control, check if spread disappears
1457− keyRelease(Qt.Key_Control)
1466+ keyRelease(Qt.Key_Alt)
1458 tryCompare(desktopSpread, "state", "")
1467 tryCompare(desktopSpread, "state", "")
1459
1468
1460 // Focus should have switched back now
1469 // Focus should have switched back now
@@ -1482,7 +1491,7 @@ Rectangle {
1482 tryCompare(desktopSpread, "state", "")
1491 tryCompare(desktopSpread, "state", "")
1483
1492
1484 // Just press Alt, make sure the spread comes up
1493 // Just press Alt, make sure the spread comes up
1485− keyPress(Qt.Key_Control);
1494+ keyPress(Qt.Key_Alt);
1486 keyClick(Qt.Key_Tab);
1495 keyClick(Qt.Key_Tab);
1487 tryCompare(desktopSpread, "state", "altTab")
1496 tryCompare(desktopSpread, "state", "altTab")
1488 tryCompare(spreadRepeater, "highlightedIndex", 1)
1497 tryCompare(spreadRepeater, "highlightedIndex", 1)
@@ -1503,7 +1512,7 @@ Rectangle {
1503 tryCompare(spreadRepeater, "highlightedIndex", 0)
1512 tryCompare(spreadRepeater, "highlightedIndex", 0)
1504
1513
1505 // Release control, check if spread disappears
1514 // Release control, check if spread disappears
1506− keyRelease(Qt.Key_Control)
1515+ keyRelease(Qt.Key_Alt)
1507 tryCompare(desktopSpread, "state", "")
1516 tryCompare(desktopSpread, "state", "")
1508
1517
1509 // Make sure that after wrapping around once, we have the same one focused as at the beginning
1518 // Make sure that after wrapping around once, we have the same one focused as at the beginning
@@ -1516,7 +1525,7 @@ Rectangle {
1516 var spreadRepeater = findInvisibleChild(shell, "spreadRepeater");
1525 var spreadRepeater = findInvisibleChild(shell, "spreadRepeater");
1517 verify(spreadRepeater !== null);
1526 verify(spreadRepeater !== null);
1518
1527
1519− keyPress(Qt.Key_Control)
1528+ keyPress(Qt.Key_Alt)
1520 keyClick(Qt.Key_Tab);
1529 keyClick(Qt.Key_Tab);
1521 tryCompare(spreadRepeater, "highlightedIndex", 1);
1530 tryCompare(spreadRepeater, "highlightedIndex", 1);
1522
1531
@@ -1538,7 +1547,7 @@ Rectangle {
1538 keyClick(Qt.Key_Backtab);
1547 keyClick(Qt.Key_Backtab);
1539 tryCompare(spreadRepeater, "highlightedIndex", 1);
1548 tryCompare(spreadRepeater, "highlightedIndex", 1);
1540
1549
1541− keyRelease(Qt.Key_Control);
1550+ keyRelease(Qt.Key_Alt);
1542 }
1551 }
1543
1552
1544 function test_highlightFollowsMouse() {
1553 function test_highlightFollowsMouse() {
@@ -1547,7 +1556,7 @@ Rectangle {
1547 var spreadRepeater = findInvisibleChild(shell, "spreadRepeater");
1556 var spreadRepeater = findInvisibleChild(shell, "spreadRepeater");
1548 verify(spreadRepeater !== null);
1557 verify(spreadRepeater !== null);
1549
1558
1550− keyPress(Qt.Key_Control)
1559+ keyPress(Qt.Key_Alt)
1551 keyClick(Qt.Key_Tab);
1560 keyClick(Qt.Key_Tab);
1552
1561
1553 tryCompare(spreadRepeater, "highlightedIndex", 1);
1562 tryCompare(spreadRepeater, "highlightedIndex", 1);
@@ -1566,7 +1575,7 @@ Rectangle {
1566
1575
1567 verify(y < 4000);
1576 verify(y < 4000);
1568
1577
1569− keyRelease(Qt.Key_Control);
1578+ keyRelease(Qt.Key_Alt);
1570 }
1579 }
1571
1580
1572 function test_closeFromSpread() {
1581 function test_closeFromSpread() {
@@ -1575,7 +1584,7 @@ Rectangle {
1575 var spreadRepeater = findInvisibleChild(shell, "spreadRepeater");
1584 var spreadRepeater = findInvisibleChild(shell, "spreadRepeater");
1576 verify(spreadRepeater !== null);
1585 verify(spreadRepeater !== null);
1577
1586
1578− keyPress(Qt.Key_Control)
1587+ keyPress(Qt.Key_Alt)
1579 keyClick(Qt.Key_Tab);
1588 keyClick(Qt.Key_Tab);
1580
1589
1581 appRemovedSpy.clear();
1590 appRemovedSpy.clear();
@@ -1602,7 +1611,7 @@ Rectangle {
1602 tryCompare(appRemovedSpy, "count", 1)
1611 tryCompare(appRemovedSpy, "count", 1)
1603 compare(appRemovedSpy.signalArguments[0][0], closedAppId);
1612 compare(appRemovedSpy.signalArguments[0][0], closedAppId);
1604
1613
1605− keyRelease(Qt.Key_Control);
1614+ keyRelease(Qt.Key_Alt);
1606 }
1615 }
1607
1616
1608 function test_selectFromSpreadWithMouse_data() {
1617 function test_selectFromSpreadWithMouse_data() {
@@ -1622,7 +1631,7 @@ Rectangle {
1622 var spreadRepeater = findInvisibleChild(shell, "spreadRepeater");
1631 var spreadRepeater = findInvisibleChild(shell, "spreadRepeater");
1623 verify(spreadRepeater !== null);
1632 verify(spreadRepeater !== null);
1624
1633
1625− keyPress(Qt.Key_Control)
1634+ keyPress(Qt.Key_Alt)
1626 keyClick(Qt.Key_Tab);
1635 keyClick(Qt.Key_Tab);
1627
1636
1628 var focusAppId = ApplicationManager.get(2).appId;
1637 var focusAppId = ApplicationManager.get(2).appId;
@@ -1649,7 +1658,7 @@ Rectangle {
1649 tryCompare(stage, "state", "");
1658 tryCompare(stage, "state", "");
1650 tryCompare(ApplicationManager, "focusedApplicationId", focusAppId);
1659 tryCompare(ApplicationManager, "focusedApplicationId", focusAppId);
1651
1660
1652− keyRelease(Qt.Key_Control);
1661+ keyRelease(Qt.Key_Alt);
1653 }
1662 }
1654
1663
1655 function test_progressiveAutoScrolling() {
1664 function test_progressiveAutoScrolling() {
@@ -1658,7 +1667,7 @@ Rectangle {
1658 var appRepeater = findInvisibleChild(shell, "appRepeater");
1667 var appRepeater = findInvisibleChild(shell, "appRepeater");
1659 verify(appRepeater !== null);
1668 verify(appRepeater !== null);
1660
1669
1661− keyPress(Qt.Key_Control)
1670+ keyPress(Qt.Key_Alt)
1662 keyClick(Qt.Key_Tab);
1671 keyClick(Qt.Key_Tab);
1663
1672
1664 var spreadFlickable = findChild(shell, "spreadFlickable")
1673 var spreadFlickable = findChild(shell, "spreadFlickable")
@@ -1684,7 +1693,7 @@ Rectangle {
1684 }
1693 }
1685 tryCompare(spreadFlickable, "contentX", 0);
1694 tryCompare(spreadFlickable, "contentX", 0);
1686
1695
1687− keyRelease(Qt.Key_Control);
1696+ keyRelease(Qt.Key_Alt);
1688 }
1697 }
1689
1698
1690 // This makes sure the hoverMouseArea is set to invisible AND disabled
1699 // This makes sure the hoverMouseArea is set to invisible AND disabled
@@ -1696,13 +1705,13 @@ Rectangle {
1696 tryCompare(hoverMouseArea, "enabled", false)
1705 tryCompare(hoverMouseArea, "enabled", false)
1697 tryCompare(hoverMouseArea, "visible", false)
1706 tryCompare(hoverMouseArea, "visible", false)
1698
1707
1699− keyPress(Qt.Key_Control)
1708+ keyPress(Qt.Key_Alt)
1700 keyClick(Qt.Key_Tab);
1709 keyClick(Qt.Key_Tab);
1701
1710
1702 tryCompare(hoverMouseArea, "enabled", true)
1711 tryCompare(hoverMouseArea, "enabled", true)
1703 tryCompare(hoverMouseArea, "visible", true)
1712 tryCompare(hoverMouseArea, "visible", true)
1704
1713
1705− keyRelease(Qt.Key_Control)
1714+ keyRelease(Qt.Key_Alt)
1706
1715
1707 tryCompare(hoverMouseArea, "enabled", false)
1716 tryCompare(hoverMouseArea, "enabled", false)
1708 tryCompare(hoverMouseArea, "visible", false)
1717 tryCompare(hoverMouseArea, "visible", false)
@@ -1719,7 +1728,7 @@ Rectangle {
1719 var appRepeater = findInvisibleChild(shell, "appRepeater");
1728 var appRepeater = findInvisibleChild(shell, "appRepeater");
1720 verify(appRepeater !== null);
1729 verify(appRepeater !== null);
1721
1730
1722− keyPress(Qt.Key_Control)
1731+ keyPress(Qt.Key_Alt)
1723 keyClick(Qt.Key_Tab);
1732 keyClick(Qt.Key_Tab);
1724
1733
1725 tryCompare(spreadRepeater, "highlightedIndex", 1);
1734 tryCompare(spreadRepeater, "highlightedIndex", 1);
@@ -1740,7 +1749,7 @@ Rectangle {
1740
1749
1741 verify(y < 4000);
1750 verify(y < 4000);
1742
1751
1743− keyRelease(Qt.Key_Control);
1752+ keyRelease(Qt.Key_Alt);
1744 }
1753 }
1745
1754
1746 function test_focusAppFromLauncherExitsSpread() {
1755 function test_focusAppFromLauncherExitsSpread() {
@@ -1750,7 +1759,7 @@ Rectangle {
1750 var launcher = findChild(shell, "launcher");
1759 var launcher = findChild(shell, "launcher");
1751 var bfb = findChild(launcher, "buttonShowDashHome");
1760 var bfb = findChild(launcher, "buttonShowDashHome");
1752
1761
1753− keyPress(Qt.Key_Control)
1762+ keyPress(Qt.Key_Alt)
1754 keyClick(Qt.Key_Tab);
1763 keyClick(Qt.Key_Tab);
1755
1764
1756 tryCompare(desktopSpread, "state", "altTab")
1765 tryCompare(desktopSpread, "state", "altTab")
@@ -1766,7 +1775,7 @@ Rectangle {
1766
1775
1767 tryCompare(ApplicationManager, "focusedApplicationId", "unity8-dash")
1776 tryCompare(ApplicationManager, "focusedApplicationId", "unity8-dash")
1768
1777
1769− keyRelease(Qt.Key_Control);
1778+ keyRelease(Qt.Key_Alt);
1770 }
1779 }
1771
1780
1772 // regression test for http://pad.lv/1443319
1781 // regression test for http://pad.lv/1443319
@@ -1989,6 +1998,86 @@ Rectangle {
1989 }
1998 }
1990 }
1999 }
1991
2000
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+
1992 function test_inputEventsOnEdgesEndUpInAppSurface_data() {
2081 function test_inputEventsOnEdgesEndUpInAppSurface_data() {
1993 return [
2082 return [
1994 { tag: "phone", repeaterName: "spreadRepeater" },
2083 { tag: "phone", repeaterName: "spreadRepeater" },