on xenial help sign in
Implement first edition for a desktop Alt+Tab spread

Approved by: Lukáš Tinkl

commit65d282d7ff38122df3619866ff39fe539a326333 authorMichael Zanetti <michael.zanetti@canonical.com> dateWed 29 Jul 2015, 12:38 (11 years ago) parent682a5f9f2eb292
12 files changed+1159−79
plugins/Utils/CMakeLists.txt+1 qml/Components/PhysicalKeysMapper.qml+22 qml/Panel/IndicatorsMenu.qml+1 qml/Shell.qml+6 qml/Stages/ApplicationWindow.qml+3 qml/Stages/DecoratedWindow.qml+72−8 qml/Stages/DesktopStage.qml+498−57 qml/Stages/SpreadMaths.qml+164 qml/Stages/graphics/window-close.svg+64 tests/mocks/Utils/CMakeLists.txt+3−1 tests/mocks/Utils/plugin.cpp+9−1 tests/qmltests/tst_Shell.qml+316−12
plugins/Utils/CMakeLists.txt+1
@@ -4,6 +4,7 @@ include_directories(
44 ${CMAKE_CURRENT_SOURCE_DIR}
55 ${CMAKE_CURRENT_BINARY_DIR}
66 ${Qt5Gui_PRIVATE_INCLUDE_DIRS}
7+ ${Qt5Quick_PRIVATE_INCLUDE_DIRS}
78 ${GIO_INCLUDE_DIRS}
89)
910
qml/Components/PhysicalKeysMapper.qml+22
@@ -41,14 +41,23 @@ Item {
4141 signal volumeUpTriggered;
4242 signal screenshotTriggered;
4343
44+ readonly property bool altTabPressed: d.altTabPressed
45+
4446 property int powerKeyLongPressTime: 2000
4547
48+ // 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.
50+ property bool controlInsteadOfAlt: false
51+
4652 QtObject {
4753 id: d
4854
4955 property bool volumeDownKeyPressed: false
5056 property bool volumeUpKeyPressed: false
5157 property bool ignoreVolumeEvents: false
58+
59+ property bool altPressed: false
60+ property bool altTabPressed: false
5261 }
5362
5463 Timer {
@@ -93,6 +102,13 @@ Item {
93102 }
94103 d.volumeUpKeyPressed = true;
95104 }
105+ } else if (event.key == Qt.Key_Alt || (root.controlInsteadOfAlt && event.key == Qt.Key_Control)) {
106+ d.altPressed = true;
107+ } else if (event.key == Qt.Key_Tab) {
108+ if (d.altPressed && !d.altTabPressed) {
109+ d.altTabPressed = true;
110+ event.accepted = true;
111+ }
96112 } else if (event.key == Qt.Key_Print) {
97113 root.screenshotTriggered();
98114 }
@@ -110,6 +126,12 @@ Item {
110126 if (!d.ignoreVolumeEvents) root.volumeUpTriggered();
111127 d.volumeUpKeyPressed = false;
112128 if (!d.volumeDownKeyPressed) d.ignoreVolumeEvents = false;
129+ } else if (event.key == Qt.Key_Alt || (root.controlInsteadOfAlt && event.key == Qt.Key_Control)) {
130+ d.altPressed = false;
131+ if (d.altTabPressed) {
132+ d.altTabPressed = false;
133+ event.accepted = true;
134+ }
113135 }
114136 }
115137}
qml/Panel/IndicatorsMenu.qml+1
@@ -69,6 +69,7 @@ Showable {
6969 // eater
7070 MouseArea {
7171 anchors.fill: parent
72+ hoverEnabled: true
7273 }
7374
7475 MenuContent {
qml/Shell.qml+6
@@ -178,6 +178,7 @@ Item {
178178
179179 PhysicalKeysMapper {
180180 id: physicalKeysMapper
181+ objectName: "physicalKeysMapper"
181182
182183 onPowerKeyLongPressed: dialogs.showPowerDialog()
183184 onVolumeDownTriggered: volumeControl.volumeDown();
@@ -351,6 +352,11 @@ Item {
351352 property: "beingResized"
352353 value: shell.beingResized
353354 }
355+ Binding {
356+ target: applicationsDisplayLoader.item
357+ property: "altTabPressed"
358+ value: physicalKeysMapper.altTabPressed
359+ }
354360 }
355361
356362 Tutorial {
qml/Stages/ApplicationWindow.qml+3
@@ -135,6 +135,9 @@ FocusScope {
135135 onSurfaceChanged: {
136136 if (sessionContainer.surface) {
137137 surfaceInitTimer.start();
138+ if (ApplicationManager.focusedApplicationId == application.appId) {
139+ root.forceActiveFocus();
140+ }
138141 } else {
139142 d.surfaceInitialized = false;
140143 }
qml/Stages/DecoratedWindow.qml+72−8
@@ -23,21 +23,67 @@ import Unity.Application 0.1
2323FocusScope {
2424 id: root
2525
26+ property alias window: applicationWindow
2627 property alias application: applicationWindow.application
2728 property alias active: decoration.active
2829
30+ property bool decorationShown: true
31+ property bool highlightShown: false
32+ property real shadowOpacity: 1
33+
34+ property int windowWidth: width
35+ property int windowHeight: height
36+
2937 signal close();
3038 signal maximize();
3139 signal minimize();
3240
41+ state: "normal"
42+ states: [
43+ State {
44+ name: "normal"
45+ PropertyChanges {
46+ target: root
47+ width: windowWidth
48+ height: windowHeight
49+ }
50+ },
51+ State {
52+ name: "transformed"
53+ PropertyChanges {
54+ target: applicationWindow
55+ itemScale: Math.max(root.width / root.windowWidth, root.height / root.windowHeight)
56+ interactive: false
57+ }
58+ PropertyChanges {
59+ target: clipper
60+ clip: true
61+ }
62+ }
63+ ]
64+
3365 BorderImage {
3466 anchors {
3567 fill: root
3668 margins: -units.gu(2)
3769 }
3870 source: "graphics/dropshadow2gu.sci"
39− opacity: .3
40− Behavior on opacity { UbuntuNumberAnimation {} }
71+ opacity: root.shadowOpacity * .3
72+ }
73+
74+ Rectangle {
75+ id: selectionHighlight
76+ anchors.fill: parent
77+ anchors.margins: -units.gu(1)
78+ color: "white"
79+ opacity: highlightShown ? 0.15 : 0
80+ }
81+
82+ Rectangle {
83+ anchors { left: selectionHighlight.left; right: selectionHighlight.right; bottom: selectionHighlight.bottom; }
84+ height: units.dp(2)
85+ color: UbuntuColors.orange
86+ visible: root.highlightShown
4187 }
4288
4389 WindowDecoration {
@@ -49,14 +95,32 @@ FocusScope {
4995 onClose: root.close();
5096 onMaximize: root.maximize();
5197 onMinimize: root.minimize();
98+ visible: decorationShown
5299 }
53100
54− ApplicationWindow {
55− id: applicationWindow
56− objectName: application ? "appWindow_" + application.appId : "appWindow_null"
101+ Item {
102+ id: clipper
57103 anchors.fill: parent
58− anchors.topMargin: units.gu(3)
59− interactive: true
60− focus: true
104+
105+ ApplicationWindow {
106+ id: applicationWindow
107+ objectName: application ? "appWindow_" + application.appId : "appWindow_null"
108+ anchors.top: parent.top
109+ anchors.topMargin: root.decorationShown ? decoration.height : 0
110+ anchors.left: parent.left
111+ width: root.windowWidth
112+ height: root.windowHeight - (root.decorationShown ? decoration.height : 0)
113+ interactive: true
114+ focus: true
115+
116+ property real itemScale: 1
117+ transform: [
118+ Scale {
119+ origin.x: 0; origin.y: 0
120+ xScale: applicationWindow.itemScale
121+ yScale: applicationWindow.itemScale
122+ }
123+ ]
124+ }
61125 }
62126}
qml/Stages/DesktopStage.qml+498−57
@@ -17,13 +17,15 @@
1717 */
1818
1919import QtQuick 2.3
20+import QtQuick.Layouts 1.1
2021import Ubuntu.Components 1.1
2122import Unity.Application 0.1
2223import "../Components/PanelState"
24+import Utils 0.1
25+import Ubuntu.Gestures 0.1
2326
2427Rectangle {
2528 id: root
26−
2729 anchors.fill: parent
2830
2931 // Controls to be set from outside
@@ -50,6 +52,7 @@ Rectangle {
5052 readonly property bool orientationChangesEnabled: false
5153
5254 property alias background: wallpaper.source
55+ property bool altTabPressed: false
5356
5457 CrossFadeImage {
5558 id: wallpaper
@@ -61,15 +64,18 @@ Rectangle {
6164 Connections {
6265 target: ApplicationManager
6366 onApplicationAdded: {
64− ApplicationManager.requestFocusApplication(ApplicationManager.get(ApplicationManager.count-1).appId)
67+ if (root.state == "altTab") {
68+ root.state = "";
69+ }
70+
71+ ApplicationManager.requestFocusApplication(appId)
6572 }
6673
6774 onFocusRequested: {
6875 var appIndex = priv.indexOf(appId);
6976 var appDelegate = appRepeater.itemAt(appIndex);
70− if (appDelegate.state === "minimized") {
71− appDelegate.state = "normal"
72− }
77+ appDelegate.minimized = false;
78+ appDelegate.focus = true;
7379 ApplicationManager.focusApplication(appId);
7480 }
7581 }
@@ -104,8 +110,8 @@ Rectangle {
104110 onClose: {
105111 ApplicationManager.stopApplication(ApplicationManager.focusedApplicationId)
106112 }
107− onMinimize: appRepeater.itemAt(0).state = "minimized"
108− onMaximize: appRepeater.itemAt(0).state = "normal"
113+ onMinimize: appRepeater.itemAt(0).minimize();
114+ onMaximize: appRepeater.itemAt(0).unmaximize();
109115 }
110116
111117 Binding {
@@ -114,67 +120,502 @@ Rectangle {
114120 value: priv.focusedAppDelegate !== null && priv.focusedAppDelegate.state === "maximized"
115121 }
116122
117− Repeater {
118− id: appRepeater
119− model: ApplicationManager
123+ Rectangle {
124+ id: spreadBackground
125+ anchors.fill: parent
126+ color: "#55000000"
127+ visible: false
128+ }
120129
121− delegate: FocusScope {
122− id: appDelegate
123− z: ApplicationManager.count - index
124− y: units.gu(3)
125− width: units.gu(60)
126− height: units.gu(50)
130+ FocusScope {
131+ id: appContainer
132+ objectName: "appContainer"
133+ anchors.fill: parent
127134
128− onFocusChanged: {
129− if (focus) {
130− ApplicationManager.requestFocusApplication(model.appId);
131− }
135+ Keys.onPressed: {
136+ switch (event.key) {
137+ case Qt.Key_Left:
138+ case Qt.Key_Backtab:
139+ selectPrevious(event.isAutoRepeat)
140+ break;
141+ case Qt.Key_Right:
142+ case Qt.Key_Tab:
143+ selectNext(event.isAutoRepeat)
144+ break;
145+ case Qt.Key_Escape:
146+ appRepeater.highlightedIndex = -1
147+ case Qt.Key_Enter:
148+ case Qt.Key_Return:
149+ case Qt.Key_Space:
150+ root.state = ""
151+ }
152+ }
153+
154+ function selectNext(isAutoRepeat) {
155+ if (isAutoRepeat && appRepeater.highlightedIndex >= ApplicationManager.count -1) {
156+ return; // AutoRepeat is not allowed to wrap around
157+ }
158+
159+ appRepeater.highlightedIndex = (appRepeater.highlightedIndex + 1) % ApplicationManager.count;
160+ var newContentX = ((spreadFlickable.contentWidth) / (ApplicationManager.count + 1)) * Math.max(0, Math.min(ApplicationManager.count - 5, appRepeater.highlightedIndex - 3));
161+ if (spreadFlickable.contentX < newContentX || appRepeater.highlightedIndex == 0) {
162+ spreadFlickable.snapTo(newContentX)
163+ }
164+ }
165+
166+ function selectPrevious(isAutoRepeat) {
167+ if (isAutoRepeat && appRepeater.highlightedIndex == 0) {
168+ return; // AutoRepeat is not allowed to wrap around
169+ }
170+
171+ var newIndex = appRepeater.highlightedIndex - 1 >= 0 ? appRepeater.highlightedIndex - 1 : ApplicationManager.count - 1;
172+ appRepeater.highlightedIndex = newIndex;
173+ var newContentX = ((spreadFlickable.contentWidth) / (ApplicationManager.count + 1)) * Math.max(0, Math.min(ApplicationManager.count - 5, appRepeater.highlightedIndex - 1));
174+ if (spreadFlickable.contentX > newContentX || newIndex == ApplicationManager.count -1) {
175+ spreadFlickable.snapTo(newContentX)
176+ }
177+ }
178+
179+ function focusSelected() {
180+ if (appRepeater.highlightedIndex != -1) {
181+ appRepeater.itemAt(appRepeater.highlightedIndex).focus = true;
132182 }
183+ }
184+
185+ Repeater {
186+ id: appRepeater
187+ model: ApplicationManager
188+ objectName: "appRepeater"
189+
190+ property int highlightedIndex: -1
191+ property int closingIndex: -1
192+
193+ delegate: FocusScope {
194+ id: appDelegate
195+ z: ApplicationManager.count - index
196+ y: units.gu(3)
197+ width: units.gu(60)
198+ height: units.gu(50)
199+
200+ readonly property int minWidth: units.gu(10)
201+ readonly property int minHeight: units.gu(10)
202+
203+ property bool maximized: false
204+ property bool minimized: false
205+
206+ onFocusChanged: {
207+ if (focus && ApplicationManager.focusedApplicationId !== model.appId) {
208+ ApplicationManager.requestFocusApplication(model.appId);
209+ decoratedWindow.forceActiveFocus();
210+ }
211+ }
212+ Component.onCompleted: {
213+ if (ApplicationManager.focusedApplicationId == model.appId) {
214+ decoratedWindow.forceActiveFocus();
215+ }
216+ }
217+
218+ function maximize() {
219+ minimized = false;
220+ maximized = true;
221+ }
222+ function minimize() {
223+ maximized = false;
224+ minimized = true;
225+ }
226+ function unmaximize() {
227+ minimized = false;
228+ maximized = false;
229+ }
230+
231+ Behavior on x {
232+ id: closeBehavior
233+ enabled: appRepeater.closingIndex >= 0
234+ UbuntuNumberAnimation {
235+ onRunningChanged: if (!running) appRepeater.closingIndex = -1
236+ }
237+ }
238+
239+ states: [
240+ State {
241+ name: "normal"; when: !appDelegate.maximized && !appDelegate.minimized && root.state !== "altTab"
242+ },
243+ State {
244+ name: "maximized"; when: appDelegate.maximized && (root.state !== "altTab" || (root.state == "altTab" && !root.workspacesUpdated))
245+ PropertyChanges { target: appDelegate; x: 0; y: 0; width: root.width; height: root.height }
246+ },
247+ State {
248+ name: "minimized"; when: appDelegate.minimized && (root.state !== "altTab" || (root.state == "altTab" && !root.workspacesUpdated))
249+ PropertyChanges { target: appDelegate; x: -appDelegate.width / 2; scale: units.gu(5) / appDelegate.width; opacity: 0 }
250+ },
251+ State {
252+ name: "altTab"; when: root.state == "altTab" && root.workspacesUpdated
253+ PropertyChanges {
254+ target: appDelegate
255+ x: spreadMaths.animatedX
256+ y: spreadMaths.animatedY + (appDelegate.height - decoratedWindow.height)
257+ angle: spreadMaths.animatedAngle
258+ itemScale: spreadMaths.scale
259+ itemScaleOriginY: decoratedWindow.height / 2;
260+ z: index
261+ visible: spreadMaths.itemVisible
262+ }
263+ PropertyChanges {
264+ target: decoratedWindow
265+ decorationShown: false
266+ highlightShown: index == appRepeater.highlightedIndex
267+ state: "transformed"
268+ width: spreadMaths.spreadHeight
269+ height: spreadMaths.spreadHeight
270+ shadowOpacity: spreadMaths.shadowOpacity
271+ }
272+ PropertyChanges {
273+ target: tileInfo
274+ visible: true
275+ opacity: spreadMaths.tileInfoOpacity
276+ }
277+ PropertyChanges {
278+ target: spreadSelectArea
279+ enabled: true
280+ }
281+ PropertyChanges {
282+ target: windowMoveResizeArea
283+ enabled: false
284+ }
285+ }
286+ ]
287+ transitions: [
288+ Transition {
289+ from: "maximized,minimized,normal,"
290+ to: "maximized,minimized,normal,"
291+ PropertyAnimation { target: appDelegate; properties: "x,y,opacity,width,height,scale" }
292+ }
293+ ]
294+ property real angle: 0
295+ property real itemScale: 1
296+ property int itemScaleOriginX: 0
297+ property int itemScaleOriginY: 0
298+
299+ SpreadMaths {
300+ id: spreadMaths
301+ flickable: spreadFlickable
302+ itemIndex: index
303+ totalItems: Math.max(6, ApplicationManager.count)
304+ sceneHeight: root.height
305+ itemHeight: appDelegate.height
306+ }
307+
308+ WindowMoveResizeArea {
309+ id: windowMoveResizeArea
310+ target: appDelegate
311+ minWidth: appDelegate.minWidth
312+ minHeight: appDelegate.minHeight
313+ resizeHandleWidth: units.gu(2)
314+ windowId: model.appId // FIXME: Change this to point to windowId once we have such a thing
315+
316+ onPressed: appDelegate.focus = true;
317+ }
318+
319+ DecoratedWindow {
320+ id: decoratedWindow
321+ objectName: "decoratedWindow"
322+ anchors.left: appDelegate.left
323+ anchors.top: appDelegate.top
324+ windowWidth: appDelegate.width
325+ windowHeight: appDelegate.height
326+ application: ApplicationManager.get(index)
327+ active: ApplicationManager.focusedApplicationId === model.appId
328+ focus: false
329+
330+ onClose: ApplicationManager.stopApplication(model.appId)
331+ onMaximize: appDelegate.maximize()
332+ onMinimize: appDelegate.minimize()
333+
334+ transform: [
335+ Scale {
336+ origin.x: itemScaleOriginX
337+ origin.y: itemScaleOriginY
338+ xScale: itemScale
339+ yScale: itemScale
340+ },
341+ Rotation {
342+ origin { x: 0; y: (decoratedWindow.height - (decoratedWindow.height * itemScale / 2)) }
343+ axis { x: 0; y: 1; z: 0 }
344+ angle: appDelegate.angle
345+ }
346+ ]
347+
348+ MouseArea {
349+ id: spreadSelectArea
350+ anchors.fill: parent
351+ anchors.margins: -units.gu(2)
352+ enabled: false
353+ hoverEnabled: enabled
354+
355+ // There is a bug in MouseArea where containsMouse doesn't
356+ // return to false if the MouseArea is disabled while
357+ // containing the mouse. Let's manage the property our own.
358+ property bool upperThirdContainsMouse: false
359+ onContainsMouseChanged: evaluateContainsMouse()
360+ onMouseYChanged: evaluateContainsMouse()
361+ function evaluateContainsMouse() {
362+ if (containsMouse) {
363+ appRepeater.highlightedIndex = index
364+ }
133365
134− readonly property int minWidth: units.gu(10)
135− readonly property int minHeight: units.gu(10)
136−
137− states: [
138− State {
139− name: "normal"
140− },
141− State {
142− name: "maximized"
143− PropertyChanges { target: appDelegate; x: 0; y: 0; width: root.width; height: root.height }
144− },
145− State {
146− name: "minimized"
147− PropertyChanges { target: appDelegate; x: -appDelegate.width / 2; scale: units.gu(5) / appDelegate.width; opacity: 0 }
366+ if (containsMouse && mouseY < height / 3) {
367+ spreadSelectArea.upperThirdContainsMouse = true
368+ } else {
369+ spreadSelectArea.upperThirdContainsMouse = false;
370+ }
371+ }
372+ onEnabledChanged: {
373+ if (!enabled) {
374+ spreadSelectArea.upperThirdContainsMouse = false
375+ }
376+ }
377+
378+ onClicked: {
379+ root.state = ""
380+ }
381+ }
148382 }
149− ]
150− transitions: [
151− Transition {
152− PropertyAnimation { target: appDelegate; properties: "x,y,opacity,width,height,scale" }
383+
384+ Image {
385+ id: closeImage
386+ anchors { left: parent.left; top: parent.top; leftMargin: -height / 2; topMargin: -height / 2 + spreadMaths.closeIconOffset }
387+ source: "graphics/window-close.svg"
388+ visible: spreadSelectArea.upperThirdContainsMouse
389+ height: units.gu(1.5)
390+ width: height
391+ sourceSize.width: width
392+ sourceSize.height: height
153393 }
154− ]
155394
156− WindowMoveResizeArea {
157− target: appDelegate
158− minWidth: appDelegate.minWidth
159− minHeight: appDelegate.minHeight
160− resizeHandleWidth: units.gu(0.5)
161− windowId: model.appId // FIXME: Change this to point to windowId once we have such a thing
395+ MouseArea {
396+ id: closeMouseArea
397+ objectName: "closeMouseArea"
398+ anchors.fill: closeImage
399+ anchors.margins: -units.gu(2)
400+ enabled: spreadSelectArea.upperThirdContainsMouse
401+ onClicked: {
402+ appRepeater.closingIndex = index;
403+ ApplicationManager.stopApplication(model.appId)
404+ }
405+ }
406+
407+ MouseArea {
408+ id: tileInfo
409+ objectName: "tileInfo"
410+ anchors { left: parent.left; top: decoratedWindow.bottom; topMargin: units.gu(5) }
411+ width: units.gu(30)
412+ height: titleInfoColumn.height
413+ visible: false
414+ hoverEnabled: true
415+
416+ onContainsMouseChanged: {
417+ if (containsMouse) {
418+ appRepeater.highlightedIndex = index
419+ }
420+ }
162421
163− onPressed: appDelegate.focus = true;
422+ onClicked: {
423+ root.state = ""
424+ }
425+
426+ ColumnLayout {
427+ id: titleInfoColumn
428+ anchors { left: parent.left; top: parent.top; right: parent.right }
429+ spacing: units.gu(1)
430+
431+ UbuntuShape {
432+ Layout.preferredHeight: Math.min(units.gu(6), root.height * .05)
433+ Layout.preferredWidth: height * 8 / 7.6
434+ image: Image {
435+ anchors.fill: parent
436+ source: model.icon
437+ }
438+ }
439+ Label {
440+ Layout.fillWidth: true
441+ Layout.preferredHeight: units.gu(6)
442+ text: model.name
443+ wrapMode: Text.WordWrap
444+ maximumLineCount: 2
445+ }
446+ }
447+ }
164448 }
449+ }
450+ }
451+
452+ FloatingFlickable {
453+ id: spreadFlickable
454+ anchors.fill: parent
455+ contentWidth: Math.max(6, ApplicationManager.count) * Math.min(height / 4, width / 5)
456+ enabled: false
457+
458+ function snapTo(contentX) {
459+ snapAnimation.stop();
460+ snapAnimation.to = contentX
461+ snapAnimation.start();
462+ }
463+
464+ UbuntuNumberAnimation {
465+ id: snapAnimation
466+ target: spreadFlickable
467+ property: "contentX"
468+ }
469+ }
470+
471+ Item {
472+ id: workspaceSelector
473+ anchors {
474+ left: parent.left
475+ top: parent.top
476+ right: parent.right
477+ topMargin: units.gu(3.5) // TODO: should be root.panelHeight
478+ }
479+ height: root.height * 0.25
480+ visible: false
481+
482+ RowLayout {
483+ anchors.fill: parent
484+ spacing: units.gu(1)
485+ Item { Layout.fillWidth: true }
486+ Repeater {
487+ model: 1 // TODO: will be a workspacemodel in the future
488+ Item {
489+ Layout.fillHeight: true
490+ Layout.preferredWidth: ((height - units.gu(6)) * root.width / root.height)
491+ Image {
492+ source: root.background
493+ anchors {
494+ left: parent.left
495+ right: parent.right
496+ verticalCenter: parent.verticalCenter
497+ }
498+ height: parent.height * 0.75
499+
500+ // FIXME: This is temporary until we can have multiple Items per surface
501+ ShaderEffect {
502+ anchors.fill: parent
503+
504+ property var source: ShaderEffectSource {
505+ id: shaderEffectSource
506+ live: false
507+ sourceItem: appContainer
508+ Connections { target: root; onUpdateWorkspaces: shaderEffectSource.scheduleUpdate() }
509+ }
510+
511+ fragmentShader: "
512+ varying highp vec2 qt_TexCoord0;
513+ uniform sampler2D source;
514+ void main(void)
515+ {
516+ highp vec4 sourceColor = texture2D(source, qt_TexCoord0);
517+ gl_FragColor = sourceColor;
518+ }"
519+ }
520+ }
521+
522+ // TODO: This is the bar for the currently selected workspace
523+ // Enable this once the workspace stuff is implemented
524+// Rectangle {
525+// anchors { left: parent.left; right: parent.right; bottom: parent.bottom }
526+// height: units.dp(2)
527+// color: UbuntuColors.orange
528+// visible: index == 0 // TODO: should be active workspace index
529+// }
530+ }
165531
166− DecoratedWindow {
167− id: decoratedWindow
168− objectName: "decoratedWindow_" + appId
169− anchors.fill: parent
170− application: ApplicationManager.get(index)
171− active: ApplicationManager.focusedApplicationId === model.appId
172− focus: true
173−
174− onClose: ApplicationManager.stopApplication(model.appId)
175− onMaximize: appDelegate.state = (appDelegate.state == "maximized" ? "normal" : "maximized")
176− onMinimize: appDelegate.state = "minimized"
177532 }
533+ // TODO: This is the "new workspace" button. Enable this once workspaces are implemented
534+// Item {
535+// Layout.fillHeight: true
536+// Layout.preferredWidth: ((height - units.gu(6)) * root.width / root.height)
537+// Rectangle {
538+// anchors {
539+// left: parent.left
540+// right: parent.right
541+// verticalCenter: parent.verticalCenter
542+// }
543+// height: parent.height * 0.75
544+// color: "#22ffffff"
545+
546+// Label {
547+// anchors.centerIn: parent
548+// font.pixelSize: parent.height / 2
549+// text: "+"
550+// }
551+// }
552+// }
553+ Item { Layout.fillWidth: true }
554+ }
555+ }
556+
557+ Label {
558+ id: currentSelectedLabel
559+ anchors { bottom: parent.bottom; bottomMargin: root.height * 0.625; horizontalCenter: parent.horizontalCenter }
560+ text: appRepeater.highlightedIndex >= 0 ? ApplicationManager.get(appRepeater.highlightedIndex).name : ""
561+ visible: false
562+ fontSize: "large"
563+ }
564+
565+ states: [
566+ State {
567+ name: "altTab"; when: root.altTabPressed
568+ PropertyChanges { target: workspaceSelector; visible: true }
569+ PropertyChanges { target: spreadFlickable; enabled: true }
570+ PropertyChanges { target: currentSelectedLabel; visible: true }
571+ PropertyChanges { target: spreadBackground; visible: true }
572+ PropertyChanges { target: appContainer; focus: true }
573+ }
574+ ]
575+ signal updateWorkspaces();
576+ property bool workspacesUpdated: false
577+ transitions: [
578+ Transition {
579+ from: "*"
580+ to: "altTab"
581+ SequentialAnimation {
582+ PropertyAction { target: appRepeater; property: "highlightedIndex"; value: Math.min(ApplicationManager.count - 1, 1) }
583+ PauseAnimation { duration: 50 }
584+ PropertyAction { target: workspaceSelector; property: "visible" }
585+ ScriptAction { script: root.updateWorkspaces() }
586+ // FIXME: Updating of shaderEffectSource take a bit of time. This is temporary until we can paint multiple items per surface
587+ PauseAnimation { duration: 10 }
588+ PropertyAction { target: root; property: "workspacesUpdated"; value: true }
589+ PropertyAction { target: spreadFlickable; property: "visible" }
590+ PropertyAction { targets: [currentSelectedLabel,spreadBackground]; property: "visible" }
591+ PropertyAction { target: spreadFlickable; property: "contentX"; value: 0 }
592+ }
593+ },
594+ Transition {
595+ from: "*"
596+ to: "*"
597+ PropertyAnimation { property: "opacity" }
598+ PropertyAction { target: root; property: "workspacesUpdated"; value: false }
599+ ScriptAction { script: { appContainer.focusSelected() } }
600+ PropertyAction { target: appRepeater; property: "highlightedIndex"; value: -1 }
178601 }
602+
603+ ]
604+
605+ MouseArea {
606+ anchors {
607+ top: parent.top
608+ right: parent.right
609+ bottom: parent.bottom
610+ }
611+ // TODO: Make this a push to edge thing like the launcher when we can,
612+ // for now, yes, we want 1 pixel, regardless of the scaling
613+ width: 1
614+ hoverEnabled: true
615+ onContainsMouseChanged: {
616+ if (containsMouse) {
617+ root.state = "altTab"
618+ }
619+ }
179620 }
180621}
qml/Stages/SpreadMaths.qmlnew file+164
@@ -0,0 +1,164 @@
1+/*
2+ * Copyright (C) 2015 Canonical, Ltd.
3+ *
4+ * This program is free software; you can redistribute it and/or modify
5+ * it under the terms of the GNU General Public License as published by
6+ * the Free Software Foundation; version 3.
7+ *
8+ * This program is distributed in the hope that it will be useful,
9+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
10+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+ * GNU General Public License for more details.
12+ *
13+ * You should have received a copy of the GNU General Public License
14+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
15+ */
16+
17+import QtQuick 2.2
18+import Ubuntu.Components 1.1
19+import Utils 0.1
20+
21+Item {
22+ id: root
23+ anchors { left: parent.left; top: parent.top; margins: units.gu(1) }
24+
25+ // Information about the transformed item
26+ property int itemIndex: 0
27+ property real itemHeight: units.gu(10)
28+
29+ // Information about the environment
30+ property int totalItems: 0
31+ property Item flickable: null
32+ property int sceneHeight: units.gu(20)
33+
34+ // Spread properties
35+ property real spreadHeight: sceneHeight * 0.4
36+ property int spreadBottomOffset: sceneHeight * 0.18
37+ property int foldingAreaWidth: flickableWidth * 0.2
38+ property int maxVisibleItems: 7
39+ property int margins: flickableWidth * 0.05
40+ property real stackScale: 0.1
41+ property int leftEndFoldedAngle: 70
42+ property int rightEndFoldedAngle: 65
43+ property int unfoldedAngle: 30
44+ property int stackWidth: flickableWidth * 0.01
45+
46+
47+ // Internal
48+ readonly property int flickableWidth: flickable ? flickable.width : 0
49+ readonly property int flickableContentWidth: flickable ? flickable.contentWidth: 0
50+ readonly property real flickableProgress: flickable ? flickable.contentX / (flickable.contentWidth - flickableWidth) : 0
51+
52+ readonly property int contentWidth: flickableWidth - root.margins * 2
53+
54+ readonly property int distance: (flickableContentWidth - (margins * 2) - (foldingAreaWidth * 2)) / (totalItems - 2)
55+ readonly property int startPos: margins + foldingAreaWidth + (itemIndex - 1) * distance
56+ readonly property int linearX: startPos - flickableProgress * (flickableContentWidth - flickableWidth)
57+
58+ readonly property int leftFoldingAreaX: margins + foldingAreaWidth
59+ readonly property int rightFoldingAreaX: flickableWidth - foldingAreaWidth - margins
60+
61+ readonly property real leftFoldingAreaProgress: linearAnimation(leftFoldingAreaX, margins, 0, 1, linearX)
62+ readonly property real rightFoldingAreaProgress: linearAnimation(rightFoldingAreaX, flickableWidth - margins, 0, 1, linearX)
63+
64+ readonly property real limitedLeftProgress: Math.min(2, leftFoldingAreaProgress)
65+ readonly property real limitedRightProgress: Math.min(2, rightFoldingAreaProgress)
66+
67+ readonly property real middleSectionProgress: (linearX - margins - foldingAreaWidth) / (flickableWidth - (margins + foldingAreaWidth) * 2)
68+
69+ // Output
70+ readonly property int animatedX: {
71+ if (leftFoldingAreaProgress > 4) { // Stop it at the edge
72+ return margins;
73+ }
74+ if (leftFoldingAreaProgress > 2) { // move it slowly through the stack
75+ return linearAnimation(2, 4, margins + stackWidth, margins, leftFoldingAreaProgress)
76+ }
77+ if (leftFoldingAreaProgress > 1 && itemIndex == 0) {
78+ // The leftmost runs faster... make it stop before the stack and wait for others
79+ return margins + stackWidth;
80+ }
81+
82+ if (leftFoldingAreaProgress > 0) { // slow it down in a curve
83+ if (itemIndex == 0) { // except if it's the leftmost. that one goes straigt
84+ return linearAnimation(0, 1, leftFoldingAreaX, margins + stackWidth, leftFoldingAreaProgress)
85+ }
86+ return linearAnimation(0, 1, leftFoldingAreaX, margins + stackWidth, leftEasing.value)
87+ }
88+ // same for the right side stack... mostly... don't need to treat the rightmost special...
89+ if (rightFoldingAreaProgress > 4) {
90+ return flickableWidth - margins
91+ }
92+ if (rightFoldingAreaProgress > 2) {
93+ return linearAnimation(2, 4, flickableWidth - margins - stackWidth, flickableWidth - margins, rightFoldingAreaProgress)
94+ }
95+
96+ if (rightFoldingAreaProgress > 0) {
97+ return linearAnimation(0, 1, rightFoldingAreaX, flickableWidth - margins - stackWidth, rightEasing.value);
98+ }
99+
100+ return linearX
101+ }
102+
103+ readonly property int animatedY: sceneHeight - itemHeight - spreadBottomOffset
104+
105+ readonly property real animatedAngle: {
106+ if (limitedLeftProgress > 0) {
107+ // Leftmost is special...
108+ if (index == 0) {
109+ if (limitedLeftProgress < 1) {
110+ return unfoldedAngle;
111+ } else {
112+ return linearAnimation(1, 2, unfoldedAngle, leftEndFoldedAngle, limitedLeftProgress)
113+ }
114+ }
115+ return linearAnimation(0, 2, unfoldedAngle, leftEndFoldedAngle, limitedLeftProgress)
116+ } else if (limitedRightProgress > 0) {
117+ return linearAnimation(0, 1, unfoldedAngle, rightEndFoldedAngle, rightEasing.value)
118+ } else {
119+ return unfoldedAngle
120+ }
121+ }
122+
123+ readonly property real scale: limitedLeftProgress > 0 ?
124+ linearAnimation(0, 1, 1, 1 + stackScale, leftEasing.value)
125+ : limitedRightProgress > 0 ?
126+ linearAnimation(0, 1, 1, 1 + stackScale, rightEasing.value)
127+ : 0.95 + Math.abs(middleSectionProgress - 0.5) * 0.1
128+
129+ readonly property real closeIconOffset: (scale - 1) * (-root.spreadHeight / 2)
130+
131+ readonly property real tileInfoOpacity: leftFoldingAreaProgress > 0 ?
132+ linearAnimation(1, 1.5, 1, 0, leftFoldingAreaProgress)
133+ : rightFoldingAreaProgress > 0 ?
134+ linearAnimation(1, 1.5, 1, 0, rightFoldingAreaProgress)
135+ : 1
136+
137+ readonly property bool itemVisible: itemIndex == totalItems - 1 ? true : leftFoldingAreaProgress < 5 && rightFoldingAreaProgress < 5
138+ readonly property real shadowOpacity: itemIndex == totalItems -1 ?
139+ 1
140+ : leftFoldingAreaProgress > 3 ?
141+ linearAnimation(3, 3.5, 1, 0, leftFoldingAreaProgress)
142+ : rightFoldingAreaProgress > 3 ?
143+ linearAnimation(3, 3.5, 1, 0, rightFoldingAreaProgress)
144+ : 1
145+
146+
147+ // Helpers
148+ function linearAnimation(startProgress, endProgress, startValue, endValue, progress) {
149+ // progress : progressDiff = value : valueDiff => value = progress * valueDiff / progressDiff
150+ return (progress - startProgress) * (endValue - startValue) / (endProgress - startProgress) + startValue;
151+ }
152+
153+ EasingCurve {
154+ id: leftEasing
155+ type: EasingCurve.OutSine
156+ progress: limitedLeftProgress / 2 // OutSine starts with twice the speed. slow it down.
157+ }
158+
159+ EasingCurve {
160+ id: rightEasing
161+ type: EasingCurve.OutSine
162+ progress: limitedRightProgress / 2 // OutSine starts with twice the speed. slow it down.
163+ }
164+}
qml/Stages/graphics/window-close.svgnew file+64
@@ -0,0 +1,64 @@
1+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
2+<!-- Created with Inkscape (http://www.inkscape.org/) -->
3+
4+<svg
5+ xmlns:dc="http://purl.org/dc/elements/1.1/"
6+ xmlns:cc="http://creativecommons.org/ns#"
7+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
8+ xmlns:svg="http://www.w3.org/2000/svg"
9+ xmlns="http://www.w3.org/2000/svg"
10+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
11+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
12+ width="14.81667mm"
13+ height="14.816668mm"
14+ viewBox="0 0 14.81667 14.816668"
15+ id="svg4171"
16+ version="1.1"
17+ inkscape:version="0.91+devel r"
18+ sodipodi:docname="window-close01.svg">
19+ <defs
20+ id="defs4173" />
21+ <sodipodi:namedview
22+ id="base"
23+ pagecolor="#ffffff"
24+ bordercolor="#666666"
25+ borderopacity="1.0"
26+ inkscape:pageopacity="0.0"
27+ inkscape:pageshadow="2"
28+ inkscape:zoom="11.957142"
29+ inkscape:cx="37.299889"
30+ inkscape:cy="25.219237"
31+ inkscape:document-units="mm"
32+ inkscape:current-layer="layer1"
33+ showgrid="false"
34+ fit-margin-top="0"
35+ fit-margin-left="0"
36+ fit-margin-right="0"
37+ fit-margin-bottom="0" />
38+ <metadata
39+ id="metadata4176">
40+ <rdf:RDF>
41+ <cc:Work
42+ rdf:about="">
43+ <dc:format>image/svg+xml</dc:format>
44+ <dc:type
45+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
46+ <dc:title></dc:title>
47+ </cc:Work>
48+ </rdf:RDF>
49+ </metadata>
50+ <g
51+ inkscape:label="Layer 1"
52+ inkscape:groupmode="layer"
53+ id="layer1"
54+ transform="translate(-134.71071,-3.8416665)">
55+ <path
56+ style="color:#000000;display:inline;overflow:visible;visibility:visible;opacity:1;fill:#dd4814;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:16.93333244;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;enable-background:accumulate"
57+ d="M 142.11905,3.8416665 A 7.4083333,7.4083333 0 0 0 134.71071,11.25 7.4083333,7.4083333 0 0 0 142.11905,18.658334 7.4083333,7.4083333 0 0 0 149.52738,11.25 7.4083333,7.4083333 0 0 0 142.11905,3.8416665 Z"
58+ id="path4141" />
59+ <path
60+ style="color:#000000;display:inline;overflow:visible;visibility:visible;opacity:1;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:16.93333244;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;enable-background:accumulate"
61+ d="m 139.31818,7.7008631 2.80087,2.8008629 2.80086,-2.8008629 0.74827,0.7482734 -2.80086,2.8008635 2.80086,2.800864 -0.74827,0.748273 -2.80086,-2.800864 -2.80087,2.800864 -0.74827,-0.748273 2.80086,-2.800864 -2.80086,-2.8008635 0.74827,-0.7482734 z"
62+ id="path4170" />
63+ </g>
64+</svg>
tests/mocks/Utils/CMakeLists.txt+3−1
@@ -4,12 +4,12 @@ include_directories(
44 ${CMAKE_CURRENT_SOURCE_DIR}
55 ${CMAKE_CURRENT_BINARY_DIR}
66 ${Qt5Gui_PRIVATE_INCLUDE_DIRS}
7+ ${Qt5Quick_PRIVATE_INCLUDE_DIRS}
78 ${GIO_INCLUDE_DIRS}
89 ${CMAKE_SOURCE_DIR}/plugins/Utils
910)
1011
1112set(QMLPLUGIN_SRC
12− ${CMAKE_SOURCE_DIR}/plugins/Utils/inputwatcher.cpp
1313 ${CMAKE_SOURCE_DIR}/plugins/Utils/qlimitproxymodelqml.cpp
1414 ${CMAKE_SOURCE_DIR}/plugins/Utils/unitysortfilterproxymodelqml.cpp
1515 ${CMAKE_SOURCE_DIR}/plugins/Utils/relativetimeformatter.cpp
@@ -18,9 +18,11 @@ set(QMLPLUGIN_SRC
1818 ${CMAKE_SOURCE_DIR}/plugins/Utils/windowkeysfilter.cpp
1919 ${CMAKE_SOURCE_DIR}/plugins/Utils/windowscreenshotprovider.cpp
2020 ${CMAKE_SOURCE_DIR}/plugins/Utils/easingcurve.cpp
21+ ${CMAKE_SOURCE_DIR}/plugins/Utils/inputwatcher.cpp
2122 constants.cpp
2223 plugin.cpp
2324 windowstatestorage.cpp
25+ plugin.cpp
2426 )
2527
2628add_library(FakeUtils-qml SHARED
tests/mocks/Utils/plugin.cpp+9−1
@@ -23,6 +23,14 @@
2323
2424// local
2525#include "plugin.h"
26+#include "inputwatcher.h"
27+#include "qlimitproxymodelqml.h"
28+#include "unitysortfilterproxymodelqml.h"
29+#include "relativetimeformatter.h"
30+#include "timeformatter.h"
31+#include "unitymenumodelpaths.h"
32+#include "windowkeysfilter.h"
33+#include "easingcurve.h"
2634#include "windowstatestorage.h"
2735#include "constants.h"
2836
@@ -53,7 +61,7 @@ static QObject *createConstants(QQmlEngine *engine, QJSEngine *scriptEngine)
5361
5462void FakeUtilsPlugin::registerTypes(const char *uri)
5563{
56− Q_ASSERT(uri == QLatin1String("Utils"));;
64+ Q_ASSERT(uri == QLatin1String("Utils"));
5765 qmlRegisterType<QAbstractItemModel>();
5866 qmlRegisterType<QLimitProxyModelQML>(uri, 0, 1, "LimitProxyModel");
5967 qmlRegisterType<UnitySortFilterProxyModelQML>(uri, 0, 1, "UnitySortFilterProxyModel");
tests/qmltests/tst_Shell.qml+316−12
@@ -47,6 +47,7 @@ Rectangle {
4747 }
4848
4949 Item {
50+ id: shellContainer
5051 anchors.left: root.left
5152 anchors.right: controls.left
5253 anchors.top: root.top
@@ -66,6 +67,11 @@ Rectangle {
6667 width: units.gu(40)
6768 height: units.gu(71)
6869 }
70+ StateChangeScript {
71+ script: {
72+ GSettingsController.setUsageMode("Staged")
73+ }
74+ }
6975 },
7076 State {
7177 name: "tablet"
@@ -74,6 +80,28 @@ Rectangle {
7480 width: units.gu(100)
7581 height: units.gu(71)
7682 }
83+ StateChangeScript {
84+ script: {
85+ GSettingsController.setUsageMode("Staged")
86+ }
87+ }
88+ },
89+ State {
90+ name: "desktop"
91+ PropertyChanges {
92+ target: shellLoader
93+ width: shellContainer.width
94+ height: shellContainer.height
95+ }
96+ PropertyChanges {
97+ target: mouseEmulation
98+ checked: false
99+ }
100+ StateChangeScript {
101+ script: {
102+ GSettingsController.setUsageMode("Windowed")
103+ }
104+ }
77105 }
78106 ]
79107
@@ -81,6 +109,7 @@ Rectangle {
81109 property bool itemDestroyed: false
82110 sourceComponent: Component {
83111 Shell {
112+ id: __shell
84113 usageScenario: usageScenarioSelector.model[usageScenarioSelector.selectedIndex]
85114 orientation: Qt.PortraitOrientation
86115 primaryOrientation: Qt.PortraitOrientation
@@ -88,6 +117,10 @@ Rectangle {
88117 Component.onDestruction: {
89118 shellLoader.itemDestroyed = true;
90119 }
120+ Component.onCompleted: {
121+ var keyMapper = testCase.findChild(__shell, "physicalKeysMapper");
122+ keyMapper.controlInsteadOfAlt = true;
123+ }
91124 }
92125 }
93126 }
@@ -112,16 +145,32 @@ Rectangle {
112145 anchors { left: parent.left; right: parent.right; top: parent.top; margins: units.gu(1) }
113146 spacing: units.gu(1)
114147
115− Button {
116− text: "Show Greeter"
117− activeFocusOnPress: false
118− onClicked: {
119− if (shellLoader.status !== Loader.Ready)
120− return;
121−
122− var greeter = testCase.findChild(shellLoader.item, "greeter");
123− if (!greeter.shown) {
124− LightDM.Greeter.showGreeter();
148+ Row {
149+ spacing: units.gu(1)
150+ Button {
151+ text: "Show Greeter"
152+ activeFocusOnPress: false
153+ onClicked: {
154+ if (shellLoader.status !== Loader.Ready)
155+ return;
156+
157+ var greeter = testCase.findChild(shellLoader.item, "greeter");
158+ if (!greeter.shown) {
159+ LightDM.Greeter.showGreeter();
160+ }
161+ }
162+ }
163+ Button {
164+ text: "Hide Greeter"
165+ activeFocusOnPress: false
166+ onClicked: {
167+ if (shellLoader.status !== Loader.Ready)
168+ return;
169+
170+ var greeter = testCase.findChild(shellLoader.item, "greeter");
171+ if (greeter.shown) {
172+ greeter.hide()
173+ }
125174 }
126175 }
127176 }
@@ -141,7 +190,7 @@ Rectangle {
141190 anchors { left: parent.left; right: parent.right }
142191 activeFocusOnPress: false
143192 text: "Size"
144− model: ["phone", "tablet"]
193+ model: ["phone", "tablet", "desktop"]
145194 onSelectedIndexChanged: {
146195 shellLoader.active = false;
147196 shellLoader.state = model[selectedIndex];
@@ -163,6 +212,17 @@ Rectangle {
163212
164213 Label { text: "Applications"; font.bold: true }
165214
215+ Button {
216+ text: "Start all apps"
217+ width: parent.width
218+ onClicked: {
219+ for (var i = 0; i < ApplicationManager.availableApplications.length; i++) {
220+ var appId = ApplicationManager.availableApplications[i];
221+ ApplicationManager.startApplication(appId)
222+ }
223+ }
224+ }
225+
166226 Repeater {
167227 id: appRepeater
168228 model: ApplicationManager.availableApplications
@@ -226,6 +286,12 @@ Rectangle {
226286 signalName: "actionInvoked"
227287 }
228288
289+ SignalSpy {
290+ id: appRemovedSpy
291+ target: ApplicationManager
292+ signalName: "applicationRemoved"
293+ }
294+
229295 Telephony.CallEntry {
230296 id: phoneCall
231297 phoneNumber: "+447812221111"
@@ -248,6 +314,7 @@ Rectangle {
248314 }
249315
250316 function cleanup() {
317+ mouseEmulation.checked = true;
251318 tryCompare(shell, "enabled", true); // make sure greeter didn't leave us in disabled state
252319 tearDown();
253320 GSettingsController.setUsageMode("Staged");
@@ -269,6 +336,20 @@ Rectangle {
269336 waitForGreeterToStabilize();
270337 }
271338
339+ function loadDesktopShellWithApps() {
340+ loadShell("desktop");
341+ shell.usageScenario = "desktop"
342+ waitForRendering(root)
343+ var app1 = ApplicationManager.startApplication("dialer-app")
344+ var app2 = ApplicationManager.startApplication("webbrowser-app")
345+ var app3 = ApplicationManager.startApplication("camera-app")
346+ var app4 = ApplicationManager.startApplication("facebook-webapp")
347+ var app5 = ApplicationManager.startApplication("calendar-app")
348+ var app6 = ApplicationManager.startApplication("gallery-app")
349+ var app7 = ApplicationManager.startApplication("camera-app")
350+ waitUntilAppWindowIsFullyLoaded(app7);
351+ }
352+
272353 function waitForGreeterToStabilize() {
273354 var greeter = findChild(shell, "greeter");
274355 verify(greeter);
@@ -759,7 +840,7 @@ Rectangle {
759840
760841 // check that the desktop stage and window have been loaded
761842 {
762− var desktopWindow = findChild(shell, "decoratedWindow_webbrowser-app");
843+ var desktopWindow = findChild(shell, "appWindow_webbrowser-app");
763844 verify(desktopWindow);
764845 }
765846
@@ -1265,5 +1346,228 @@ Rectangle {
12651346 var launcher = findChild(shell, "launcher");
12661347 compare(launcher.inverted, data.launcherInverted);
12671348 }
1349+
1350+ function test_altTabSwitchesFocus() {
1351+ loadShell("desktop");
1352+ shell.usageScenario = "desktop"
1353+ waitForRendering(root)
1354+
1355+ var desktopStage = findChild(shell, "stage");
1356+ verify(desktopStage != null)
1357+
1358+ var app1 = ApplicationManager.startApplication("dialer-app")
1359+ waitUntilAppWindowIsFullyLoaded(app1);
1360+ var app2 = ApplicationManager.startApplication("webbrowser-app")
1361+ waitUntilAppWindowIsFullyLoaded(app2);
1362+ var app3 = ApplicationManager.startApplication("camera-app")
1363+ waitUntilAppWindowIsFullyLoaded(app3);
1364+
1365+ // Do a quick alt-tab and see if focus changes
1366+ tryCompare(app3.session.surface, "activeFocus", true)
1367+ keyClick(Qt.Key_Tab, Qt.ControlModifier)
1368+ tryCompare(app2.session.surface, "activeFocus", true)
1369+
1370+
1371+ tryCompare(desktopStage, "state", "")
1372+
1373+ // Just press Alt, make sure the spread comes up
1374+ keyPress(Qt.Key_Control);
1375+ keyClick(Qt.Key_Tab);
1376+ tryCompare(desktopStage, "state", "altTab")
1377+
1378+ // Release control, check if spread disappears
1379+ keyRelease(Qt.Key_Control)
1380+ tryCompare(desktopStage, "state", "")
1381+
1382+ // Focus should have switched back now
1383+ tryCompare(app3.session.surface, "activeFocus", true)
1384+ }
1385+
1386+ function test_altTabWrapAround() {
1387+ loadDesktopShellWithApps();
1388+
1389+ var desktopStage = findChild(shell, "stage");
1390+ verify(desktopStage !== null)
1391+
1392+ var appContainer = findInvisibleChild(shell, "appContainer")
1393+ verify(appContainer !== null)
1394+
1395+ var appRepeater = findInvisibleChild(shell, "appRepeater")
1396+ verify(appRepeater !== null)
1397+
1398+ // remember the focused appId
1399+ var focused = ApplicationManager.get(ApplicationManager.findApplication(ApplicationManager.focusedApplicationId));
1400+
1401+ tryCompare(desktopStage, "state", "")
1402+
1403+ // Just press Alt, make sure the spread comes up
1404+ keyPress(Qt.Key_Control);
1405+ keyClick(Qt.Key_Tab);
1406+ tryCompare(desktopStage, "state", "altTab")
1407+ tryCompare(appRepeater, "highlightedIndex", 1)
1408+ waitForRendering(shell)
1409+
1410+ // Now press and hold Tab, make sure the highlight moves all the way but stops at the last one
1411+ // We can't simulate a pressed key with keyPress() currently, so let's inject the events
1412+ // at API level. Jump for 10 times, verify that it's still at the last one and didn't wrap around.
1413+ for (var i = 0; i < 10; i++) {
1414+ appContainer.selectNext(true); // true == isAutoRepeat
1415+ wait(0); // Trigger the event loop to make sure all the things happen
1416+ }
1417+ tryCompare(appRepeater, "highlightedIndex", 6)
1418+
1419+ // Now release it once, and verify that it does wrap around with an additional Tab press
1420+ keyRelease(Qt.Key_Tab);
1421+ keyClick(Qt.Key_Tab);
1422+ tryCompare(appRepeater, "highlightedIndex", 0)
1423+
1424+ // Release control, check if spread disappears
1425+ keyRelease(Qt.Key_Control)
1426+ tryCompare(desktopStage, "state", "")
1427+
1428+ // Make sure that after wrapping around once, we have the same one focused as at the beginning
1429+ tryCompare(focused.session.surface, "activeFocus", true)
1430+ }
1431+
1432+ function test_altBackTabNavigation() {
1433+ loadDesktopShellWithApps();
1434+
1435+ var appRepeater = findInvisibleChild(shell, "appRepeater");
1436+ verify(appRepeater !== null);
1437+
1438+ keyPress(Qt.Key_Control)
1439+ keyClick(Qt.Key_Tab);
1440+ tryCompare(appRepeater, "highlightedIndex", 1);
1441+
1442+ keyClick(Qt.Key_Tab);
1443+ tryCompare(appRepeater, "highlightedIndex", 2);
1444+
1445+ keyClick(Qt.Key_Tab);
1446+ tryCompare(appRepeater, "highlightedIndex", 3);
1447+
1448+ keyClick(Qt.Key_Tab);
1449+ tryCompare(appRepeater, "highlightedIndex", 4);
1450+
1451+ keyClick(Qt.Key_Backtab);
1452+ tryCompare(appRepeater, "highlightedIndex", 3);
1453+
1454+ keyClick(Qt.Key_Backtab);
1455+ tryCompare(appRepeater, "highlightedIndex", 2);
1456+
1457+ keyClick(Qt.Key_Backtab);
1458+ tryCompare(appRepeater, "highlightedIndex", 1);
1459+
1460+ keyRelease(Qt.Key_Control);
1461+ }
1462+
1463+ function test_highlightFollowsMouse() {
1464+ loadDesktopShellWithApps()
1465+
1466+ var appRepeater = findInvisibleChild(shell, "appRepeater");
1467+ verify(appRepeater !== null);
1468+
1469+ keyPress(Qt.Key_Control)
1470+ keyClick(Qt.Key_Tab);
1471+
1472+ tryCompare(appRepeater, "highlightedIndex", 1);
1473+
1474+ var x = 0;
1475+ var y = shell.height * .75;
1476+ mouseMove(shell, x, y)
1477+
1478+ for (var i = 0; i < 7; i++) {
1479+ while (appRepeater.highlightedIndex != i && x <= 4000) {
1480+ x+=10;
1481+ mouseMove(shell, x, y)
1482+ waitForRendering(shell)
1483+ }
1484+ }
1485+
1486+ verify(y < 4000);
1487+
1488+ keyRelease(Qt.Key_Control);
1489+ }
1490+
1491+ function test_closeFromSpread() {
1492+ loadDesktopShellWithApps()
1493+
1494+ var appRepeater = findInvisibleChild(shell, "appRepeater");
1495+ verify(appRepeater !== null);
1496+
1497+ keyPress(Qt.Key_Control)
1498+ keyClick(Qt.Key_Tab);
1499+
1500+ appRemovedSpy.clear();
1501+
1502+ var closedAppId = ApplicationManager.get(2).appId;
1503+ var appDelegate2 = appRepeater.itemAt(2);
1504+ var closeMouseArea = findChild(appDelegate2, "closeMouseArea");
1505+
1506+ // Move the mosue over tile 2 and verify the close button becomes visible
1507+ var x = 0;
1508+ var y = shell.height * .5;
1509+ mouseMove(shell, x, y)
1510+ while (appRepeater.highlightedIndex !== 2 && x <= 4000) {
1511+ x+=10;
1512+ mouseMove(shell, x, y)
1513+ waitForRendering(shell)
1514+ }
1515+ tryCompare(closeMouseArea, "enabled", true)
1516+
1517+ // Close the app using the close button
1518+ mouseClick(closeMouseArea, closeMouseArea.width / 2, closeMouseArea.height / 2)
1519+
1520+ // Verify applicationRemoved has been emitted correctly
1521+ tryCompare(appRemovedSpy, "count", 1)
1522+ compare(appRemovedSpy.signalArguments[0][0], closedAppId);
1523+
1524+ keyRelease(Qt.Key_Control);
1525+ }
1526+
1527+ function test_selectFromSpreadWithMouse_data() {
1528+ return [
1529+ {tag: "click on tileInfo", tileInfo: true },
1530+ {tag: "click on surface", tileInfo: false },
1531+ ]
1532+ }
1533+
1534+ function test_selectFromSpreadWithMouse(data) {
1535+ loadDesktopShellWithApps()
1536+
1537+ var appRepeater = findInvisibleChild(shell, "appRepeater");
1538+ verify(appRepeater !== null);
1539+
1540+ keyPress(Qt.Key_Control)
1541+ keyClick(Qt.Key_Tab);
1542+
1543+ appRemovedSpy.clear();
1544+
1545+ var focusAppId = ApplicationManager.get(2).appId;
1546+ var appDelegate2 = appRepeater.itemAt(2);
1547+ var decoratedWindow = findChild(appDelegate2, "decoratedWindow");
1548+ var stage = findChild(shell, "stage");
1549+
1550+ tryCompare(stage, "state", "altTab");
1551+
1552+ // Move the mouse over tile 2 and verify the highlight becomes visible
1553+ var x = 0;
1554+ var y = shell.height * (data.tileInfo ? .5 : 0.95)
1555+ mouseMove(shell, x, y)
1556+ while (appRepeater.highlightedIndex !== 2 && x <= 4000) {
1557+ x+=10;
1558+ mouseMove(shell, x, y)
1559+ waitForRendering(shell)
1560+ }
1561+ tryCompare(decoratedWindow, "highlightShown", true);
1562+
1563+ // Click the tile
1564+ mouseClick(decoratedWindow, decoratedWindow.width / 2, decoratedWindow.height / 2)
1565+
1566+ // Verify that we left the spread and app2 is the focused one now
1567+ tryCompare(stage, "state", "");
1568+ tryCompare(ApplicationManager, "focusedApplicationId", focusAppId);
1569+
1570+ keyRelease(Qt.Key_Control);
1571+ }
12681572 }
12691573}