2 * Copyright (C) 2014 Canonical, Ltd.
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.
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.
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/>.
18 import Ubuntu.Components 0.1
19 import Ubuntu.Gestures 0.1
20 import Unity.Application 0.1
22 import "../Components"
30 // Controls to be set from outside
31 property bool shown: false
32 property bool moving: false
33 property int dragAreaWidth
34 property real maximizedAppTopMargin
35 property bool interactive
36 property real inverseProgress: 0 // This is the progress for left edge drags, in pixels.
37 property int orientation: Qt.PortraitOrientation
39 onInverseProgressChanged: {
40 // This can't be a simple binding because that would be triggered after this handler
41 // while we need it active before doing the anition left/right
42 spreadView.animateX = (inverseProgress == 0)
43 if (inverseProgress == 0 && priv.oldInverseProgress > 0) {
44 // left edge drag released. Minimum distance is given by design.
45 if (priv.oldInverseProgress > units.gu(22)) {
46 ApplicationManager.requestFocusApplication("unity8-dash");
49 priv.oldInverseProgress = inverseProgress;
55 property string focusedAppId: ApplicationManager.focusedApplicationId
56 property string oldFocusedAppId: ""
58 property string mainStageAppId
59 property string sideStageAppId
61 // For convenience, keep properties of the first two apps in the model
62 property string appId0
63 property string appId1
65 property int oldInverseProgress: 0
67 onFocusedAppIdChanged: {
68 if (priv.focusedAppId.length > 0) {
69 var focusedApp = ApplicationManager.findApplication(focusedAppId);
70 if (focusedApp.stage == ApplicationInfoInterface.SideStage) {
71 priv.sideStageAppId = focusedAppId;
73 priv.mainStageAppId = focusedAppId;
77 appId0 = ApplicationManager.count >= 1 ? ApplicationManager.get(0).appId : "";
78 appId1 = ApplicationManager.count > 1 ? ApplicationManager.get(1).appId : "";
80 // Update the QML focus accordingly
81 updateSpreadDelegateFocus();
84 function updateSpreadDelegateFocus() {
85 if (priv.focusedAppId) {
86 var focusedAppIndex = priv.indexOf(priv.focusedAppId);
87 if (focusedAppIndex !== -1) {
88 spreadRepeater.itemAt(focusedAppIndex).focus = true;
90 console.warn("TabletStage: Failed to find the SpreadDelegate for appID=" + priv.focusedAppId);
95 function indexOf(appId) {
96 for (var i = 0; i < ApplicationManager.count; i++) {
97 if (ApplicationManager.get(i).appId == appId) {
104 function evaluateOneWayFlick(gesturePoints) {
105 // Need to have at least 3 points to recognize it as a flick
106 if (gesturePoints.length < 3) {
109 // Need to have a movement of at least 2 grid units to recognize it as a flick
110 if (Math.abs(gesturePoints[gesturePoints.length - 1] - gesturePoints[0]) < units.gu(2)) {
114 var oneWayFlick = true;
115 var smallestX = gesturePoints[0];
116 var leftWards = gesturePoints[1] < gesturePoints[0];
117 for (var i = 1; i < gesturePoints.length; i++) {
118 if ((leftWards && gesturePoints[i] >= smallestX)
119 || (!leftWards && gesturePoints[i] <= smallestX)) {
123 smallestX = gesturePoints[i];
130 target: ApplicationManager
132 if (spreadView.interactive) {
133 spreadView.snapTo(priv.indexOf(appId));
135 ApplicationManager.focusApplication(appId);
139 onApplicationAdded: {
140 if (spreadView.phase == 2) {
141 spreadView.snapTo(ApplicationManager.count - 1);
143 spreadView.phase = 0;
144 spreadView.contentX = -spreadView.shift;
145 ApplicationManager.focusApplication(appId);
149 onApplicationRemoved: {
150 if (priv.mainStageAppId == appId) {
151 ApplicationManager.focusApplication("unity8-dash")
153 if (priv.sideStageAppId == appId) {
154 priv.sideStageAppId = "";
157 if (ApplicationManager.count == 0) {
158 spreadView.phase = 0;
159 spreadView.contentX = -spreadView.shift;
160 } else if (spreadView.closingIndex == -1) {
161 // Unless we're closing the app ourselves in the spread,
162 // lets make sure the spread doesn't mess up by the changing app list.
163 spreadView.phase = 0;
164 spreadView.contentX = -spreadView.shift;
165 ApplicationManager.focusApplication(ApplicationManager.get(0).appId);
173 interactive: (spreadDragArea.status == DirectionalDragArea.Recognized || phase > 1)
174 && draggedDelegateCount === 0
175 contentWidth: spreadRow.width - shift
178 property int tileDistance: units.gu(20)
179 property int sideStageWidth: units.gu(40)
180 property bool sideStageVisible: priv.sideStageAppId
182 // This indicates when the spreadView is active. That means, all the animations
183 // are activated and tiles need to line up for the spread.
184 readonly property bool active: shiftedContentX > 0 || spreadDragArea.dragging
186 // The flickable needs to fill the screen in order to get touch events all over.
187 // However, we don't want to the user to be able to scroll back all the way. For
188 // that, the beginning of the gesture starts with a negative value for contentX
189 // so the flickable wants to pull it into the view already. "shift" tunes the
190 // distance where to "lock" the content.
191 readonly property real shift: width / 2
192 readonly property real shiftedContentX: contentX + shift
194 // Phase of the animation:
195 // 0: Starting from right edge, a new app (index 1) comes in from the right
196 // 1: The app has reached the first snap position.
197 // 2: The list is dragged further and snaps into the spread view when entering phase 2
200 readonly property int phase0Width: sideStageWidth
201 readonly property int phase1Width: sideStageWidth
203 // Those markers mark the various positions in the spread (ratio to screen width from right to left):
204 // 0 - 1: following finger, snap back to the beginning on release
205 readonly property real positionMarker1: 0.2
206 // 1 - 2: curved snapping movement, snap to nextInStack on release
207 readonly property real positionMarker2: sideStageWidth / spreadView.width
208 // 2 - 3: movement follows finger, snaps to phase 2 (full spread) on release
209 readonly property real positionMarker3: 0.6
210 // passing 3, we detach movement from the finger and snap to phase 2 (full spread)
211 readonly property real positionMarker4: 0.8
213 readonly property int startSnapPosition: phase0Width * 0.5
214 readonly property int endSnapPosition: phase0Width * 0.75
215 readonly property real snapPosition: 0.75
217 property int selectedIndex: -1
218 property int draggedDelegateCount: 0
219 property int closingIndex: -1
221 property bool animateX: true
223 property bool sideStageDragging: sideStageDragHandle.dragging
224 property real sideStageDragProgress: sideStageDragHandle.progress
226 onSideStageDragProgressChanged: {
227 if (sideStageDragProgress == 1) {
228 ApplicationManager.focusApplication(priv.mainStageAppId);
229 priv.sideStageAppId = "";
233 // In case the ApplicationManager already holds an app when starting up we're missing animations
234 // Make sure we end up in the same state
235 Component.onCompleted: {
236 spreadView.contentX = -spreadView.shift
239 property int nextInStack: {
242 if (ApplicationManager.count > 1) {
246 case "mainAndOverlay":
247 if (ApplicationManager.count <= 2) {
250 if (priv.appId0 == priv.mainStageAppId || priv.appId0 == priv.sideStageAppId) {
251 if (priv.appId1 == priv.mainStageAppId || priv.appId1 == priv.sideStageAppId) {
260 print("Unhandled nextInStack case! This shouldn't happen any more when the Dash is an app!");
263 property int nextZInStack: indexToZIndex(nextInStack)
272 State { // Side Stage only in overlay mode
275 State { // Main Stage and Side Stage in overlay mode
276 name: "mainAndOverlay"
278 State { // Main Stage and Side Stage in split mode
283 if (priv.mainStageAppId && !priv.sideStageAppId) {
286 if (!priv.mainStageAppId && priv.sideStageAppId) {
289 if (priv.mainStageAppId && priv.sideStageAppId) {
290 return "mainAndOverlay";
295 onShiftedContentXChanged: {
296 if (spreadView.phase == 0 && spreadView.shiftedContentX > spreadView.width * spreadView.positionMarker2) {
297 spreadView.phase = 1;
298 } else if (spreadView.phase == 1 && spreadView.shiftedContentX > spreadView.width * spreadView.positionMarker4) {
299 spreadView.phase = 2;
300 } else if (spreadView.phase == 1 && spreadView.shiftedContentX < spreadView.width * spreadView.positionMarker2) {
301 spreadView.phase = 0;
306 if (shiftedContentX < phase0Width) {
307 snapAnimation.targetContentX = -shift;
308 snapAnimation.start();
309 } else if (shiftedContentX < phase1Width) {
312 // Add 1 pixel to make sure we definitely hit positionMarker4 even with rounding errors of the animation.
313 snapAnimation.targetContentX = spreadView.width * spreadView.positionMarker4 + 1 - shift;
314 snapAnimation.start();
317 function snapTo(index) {
318 spreadView.selectedIndex = index;
319 snapAnimation.targetContentX = -shift;
320 snapAnimation.start();
323 // We need to shuffle z ordering a bit in order to keep side stage apps above main stage apps.
324 // We don't want to really reorder them in the model because that allows us to keep track
325 // of the last focused order.
326 function indexToZIndex(index) {
327 var app = ApplicationManager.get(index);
332 var active = app.appId == priv.mainStageAppId || app.appId == priv.sideStageAppId;
333 if (active && app.stage == ApplicationInfoInterface.MainStage) {
334 // if this app is active, and its the MainStage, always put it to index 0
337 if (active && app.stage == ApplicationInfoInterface.SideStage) {
338 if (!priv.mainStageAppId) {
339 // Only have SS apps running. Put the active one at 0
343 // Precondition now: There's an active MS app and this is SS app:
344 if (spreadView.nextInStack >= 0 && ApplicationManager.get(spreadView.nextInStack).stage == ApplicationInfoInterface.MainStage) {
345 // If the next app coming from the right is a MS app, we need to elevate this SS ap above it.
346 // Put it to at least level 2, or higher if there's more apps coming in before this one.
347 return Math.max(index, 2);
349 // if this is no next app to come in from the right, place this one at index 1, just on top the active MS app.
353 if (index <= 2 && app.stage == ApplicationInfoInterface.MainStage && priv.sideStageAppId) {
354 // Ok, this is an inactive MS app. If there's an active SS app around, we need to place this one
355 // in between the active MS app and the active SS app, so that it comes in from there when dragging from the right.
356 // If there's now active SS app, just leave it where it is.
357 return priv.indexOf(priv.sideStageAppId) < index ? index - 1 : index;
359 if (index == spreadView.nextInStack && app.stage == ApplicationInfoInterface.SideStage) {
360 // This is a SS app and the next one to come in from the right:
361 if (priv.sideStageAppId && priv.mainStageAppId) {
362 // If there's both, an active MS and an active SS app, put this one right on top of that
365 // Or if there's only one other active app, put it on top of that.
366 // The case that there isn't any other active app is already handled above.
369 if (index == 2 && spreadView.nextInStack == 1 && priv.sideStageAppId) {
370 // If its index 2 but not the next one to come in, it means
371 // we've pulled another one down to index 2. Move this one up to 2 instead.
374 // don't touch all others... (mostly index > 3 + simple cases where the above doesn't shuffle much)
378 SequentialAnimation {
380 property int targetContentX: -spreadView.shift
382 UbuntuNumberAnimation {
385 to: snapAnimation.targetContentX
386 duration: UbuntuAnimation.FastDuration
391 if (spreadView.selectedIndex >= 0) {
392 var newIndex = spreadView.selectedIndex;
393 spreadView.selectedIndex = -1;
394 ApplicationManager.focusApplication(ApplicationManager.get(newIndex).appId);
395 spreadView.phase = 0;
396 spreadView.contentX = -spreadView.shift;
404 x: spreadView.contentX
406 width: spreadView.width + Math.max(spreadView.width, ApplicationManager.count * spreadView.tileDistance)
409 spreadView.snapTo(0);
413 id: sideStageBackground
416 anchors.leftMargin: spreadView.width - (1 - sideStageDragHandle.progress) * spreadView.sideStageWidth
417 z: spreadView.indexToZIndex(priv.indexOf(priv.sideStageAppId))
418 opacity: spreadView.phase == 0 ? 1 : 0
419 Behavior on opacity { UbuntuNumberAnimation {} }
423 id: sideStageDragHandle
424 anchors { top: parent.top; bottom: parent.bottom; left: parent.left; leftMargin: spreadView.width - spreadView.sideStageWidth - width }
426 z: sideStageBackground.z
427 opacity: spreadView.phase <= 0 && spreadView.sideStageVisible ? 1 : 0
428 property real progress: 0
429 property bool dragging: false
431 Behavior on opacity { UbuntuNumberAnimation {} }
435 onSideStageVisibleChanged: {
436 if (spreadView.sideStageVisible) {
437 sideStageDragHandle.progress = 0;
443 anchors.centerIn: parent
444 anchors.horizontalCenterOffset: parent.progress * spreadView.sideStageWidth - (width - parent.width) / 2
445 width: sideStageDragHandleMouseArea.pressed ? parent.width * 2 : parent.width
446 height: parent.height
447 source: "graphics/sidestage_handle@20.png"
448 Behavior on width { UbuntuNumberAnimation {} }
452 id: sideStageDragHandleMouseArea
454 enabled: spreadView.shiftedContentX == 0
456 property var gesturePoints: new Array()
461 sideStageDragHandle.progress = 0;
462 sideStageDragHandle.dragging = true;
465 if (priv.mainStageAppId) {
466 sideStageDragHandle.progress = Math.max(0, (-startX + mouseX) / spreadView.sideStageWidth);
468 gesturePoints.push(mouseX);
471 if (priv.mainStageAppId) {
472 var oneWayFlick = priv.evaluateOneWayFlick(gesturePoints);
473 sideStageDragSnapAnimation.to = sideStageDragHandle.progress > 0.5 || oneWayFlick ? 1 : 0;
474 sideStageDragSnapAnimation.start();
476 sideStageDragHandle.dragging = false;
480 UbuntuNumberAnimation {
481 id: sideStageDragSnapAnimation
482 target: sideStageDragHandle
487 sideStageDragHandle.dragging = false;
495 model: ApplicationManager
497 delegate: TransformedTabletSpreadDelegate {
499 height: spreadView.height
500 width: model.stage == ApplicationInfoInterface.MainStage ? spreadView.width : spreadView.sideStageWidth
501 active: model.appId == priv.mainStageAppId || model.appId == priv.sideStageAppId
502 zIndex: spreadView.indexToZIndex(index)
503 selected: spreadView.selectedIndex == index
504 otherSelected: spreadView.selectedIndex >= 0 && !selected
505 isInSideStage: priv.sideStageAppId == model.appId
506 interactive: !spreadView.interactive && spreadView.phase === 0 && root.interactive
507 swipeToCloseEnabled: spreadView.interactive && !snapAnimation.running
508 maximizedAppTopMargin: root.maximizedAppTopMargin
509 dragOffset: !isDash && model.appId == priv.mainStageAppId && root.inverseProgress > 0 && spreadView.phase === 0 ? root.inverseProgress : 0
510 application: ApplicationManager.get(index)
513 readonly property bool isDash: model.appId == "unity8-dash"
515 // FIXME: A regular binding doesn't update any more after closing an app.
516 // Using a Binding for now.
520 value: (!spreadView.active && isDash && !active) ? -1 : spreadTile.zIndex
524 property real behavioredZIndex: zIndex
525 Behavior on behavioredZIndex {
526 enabled: spreadView.closingIndex >= 0
527 UbuntuNumberAnimation {}
530 // This is required because none of the bindings are triggered in some cases:
531 // When an app is closed, it might happen that ApplicationManager.get(nextInStack)
532 // returns a different app even though the nextInStackIndex and all the related
533 // bindings (index, mainStageApp, sideStageApp, etc) don't change. Let's force a
534 // binding update in that case.
536 target: ApplicationManager
537 onApplicationRemoved: spreadTile.z = Qt.binding(function() {
538 return spreadView.indexToZIndex(index);
543 var tileProgress = (spreadView.shiftedContentX - behavioredZIndex * spreadView.tileDistance) / spreadView.width;
544 // Some tiles (nextInStack, active) need to move directly from the beginning, normalize progress to immediately start at 0
545 if ((index == spreadView.nextInStack && spreadView.phase < 2) || (active && spreadView.phase < 1)) {
546 tileProgress += behavioredZIndex * spreadView.tileDistance / spreadView.width;
552 if (spreadView.phase == 0 && (spreadTile.active || spreadView.nextInStack == index)) {
553 if (progress < spreadView.positionMarker1) {
555 } else if (progress < spreadView.positionMarker1 + snappingCurve.period) {
556 return spreadView.positionMarker1 + snappingCurve.value * 3;
558 return spreadView.positionMarker2;
566 property: "orientation"
567 when: spreadTile.interactive
568 value: root.orientation
572 if (spreadView.phase == 2) {
573 spreadView.snapTo(index);
579 spreadView.draggedDelegateCount++;
581 spreadView.draggedDelegateCount--;
586 spreadView.closingIndex = index;
587 ApplicationManager.stopApplication(ApplicationManager.get(index).appId);
592 type: EasingCurve.Linear
593 period: (spreadView.positionMarker2 - spreadView.positionMarker1) / 3
594 progress: spreadTile.progress - spreadView.positionMarker1
603 anchors { top: parent.top; right: parent.right; bottom: parent.bottom }
604 width: root.dragAreaWidth
605 direction: Direction.Leftwards
607 property var gesturePoints: new Array()
611 spreadView.phase = 0;
612 spreadView.contentX = -spreadView.shift;
616 var dragX = -touchX + spreadDragArea.width - spreadView.shift;
617 var maxDrag = spreadView.width * spreadView.positionMarker4 - spreadView.shift;
618 spreadView.contentX = Math.min(dragX, maxDrag);
620 gesturePoints.push(touchX);
625 // Gesture recognized. Start recording this gesture
630 // Ok. The user released. Find out if it was a one-way movement.
631 var oneWayFlick = priv.evaluateOneWayFlick(gesturePoints);
634 if (oneWayFlick && spreadView.shiftedContentX < spreadView.positionMarker1 * spreadView.width) {
635 // If it was a short one-way movement, do the Alt+Tab switch
636 // no matter if we didn't cross positionMarker1 yet.
637 spreadView.snapTo(spreadView.nextInStack);
638 } else if (!dragging) {
639 if (spreadView.shiftedContentX < spreadView.width * spreadView.positionMarker1) {
641 } else if (spreadView.shiftedContentX < spreadView.width * spreadView.positionMarker2) {
642 spreadView.snapTo(spreadView.nextInStack);
644 // otherwise snap to the closest snap position we can find
645 // (might be back to start, to app 1 or to spread)