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
21 import Unity.Session 0.1
23 import "../Components"
24 import "../Components/Flickables" as Flickables
29 // Controls to be set from outside
30 property int dragAreaWidth
31 property real maximizedAppTopMargin
32 property bool interactive
33 property bool spreadEnabled: true // If false, animations and right edge will be disabled
34 property real inverseProgress: 0 // This is the progress for left edge drags, in pixels.
35 property int orientation: Qt.PortraitOrientation
39 function select(appId) {
40 spreadView.snapTo(priv.indexOf(appId));
44 spreadView.selectedIndex = -1;
46 spreadView.contentX = -spreadView.shift;
49 onInverseProgressChanged: {
50 // This can't be a simple binding because that would be triggered after this handler
51 // while we need it active before doing the anition left/right
52 priv.animateX = (inverseProgress == 0)
53 if (inverseProgress == 0 && priv.oldInverseProgress > 0) {
54 // left edge drag released. Minimum distance is given by design.
55 if (priv.oldInverseProgress > units.gu(22)) {
56 ApplicationManager.focusApplication("unity8-dash");
59 priv.oldInverseProgress = inverseProgress;
63 target: ApplicationManager
66 if (spreadView.phase > 0) {
67 spreadView.snapTo(priv.indexOf(appId));
69 ApplicationManager.focusApplication(appId);
74 if (spreadView.phase == 2) {
75 spreadView.snapTo(ApplicationManager.count - 1);
78 spreadView.contentX = -spreadView.shift;
79 ApplicationManager.focusApplication(appId);
83 onApplicationRemoved: {
84 // Unless we're closing the app ourselves in the spread,
85 // lets make sure the spread doesn't mess up by the changing app list.
86 if (spreadView.closingIndex == -1) {
88 spreadView.contentX = -spreadView.shift;
93 function focusTopMostApp() {
94 if (ApplicationManager.count > 0) {
95 var topmostApp = ApplicationManager.get(0);
96 ApplicationManager.focusApplication(topmostApp.appId);
104 property string focusedAppId: ApplicationManager.focusedApplicationId
105 property var focusedApplication: ApplicationManager.findApplication(focusedAppId)
106 property var focusedAppDelegate: null
108 property real oldInverseProgress: 0
109 property bool animateX: true
111 onFocusedAppIdChanged: focusedAppDelegate = spreadRepeater.itemAt(0);
113 function indexOf(appId) {
114 for (var i = 0; i < ApplicationManager.count; i++) {
115 if (ApplicationManager.get(i).appId == appId) {
124 Flickables.Flickable {
126 objectName: "spreadView"
128 interactive: (spreadDragArea.status == DirectionalDragArea.Recognized || phase > 1)
129 && draggedDelegateCount === 0
130 contentWidth: spreadRow.width - shift
133 // This indicates when the spreadView is active. That means, all the animations
134 // are activated and tiles need to line up for the spread.
135 readonly property bool active: shiftedContentX > 0 || spreadDragArea.status === DirectionalDragArea.Recognized
137 // The flickable needs to fill the screen in order to get touch events all over.
138 // However, we don't want to the user to be able to scroll back all the way. For
139 // that, the beginning of the gesture starts with a negative value for contentX
140 // so the flickable wants to pull it into the view already. "shift" tunes the
141 // distance where to "lock" the content.
142 readonly property real shift: width / 2
143 readonly property real shiftedContentX: contentX + shift
145 property int tileDistance: width / 4
147 // Those markers mark the various positions in the spread (ratio to screen width from right to left):
148 // 0 - 1: following finger, snap back to the beginning on release
149 property real positionMarker1: 0.3
150 // 1 - 2: curved snapping movement, snap to app 1 on release
151 property real positionMarker2: 0.45
152 // 2 - 3: movement follows finger, snaps back to app 1 on release
153 property real positionMarker3: 0.6
154 // passing 3, we detach movement from the finger and snap to 4
155 property real positionMarker4: 0.9
157 // This is where the first app snaps to when bringing it in from the right edge.
158 property real snapPosition: 0.75
160 // Phase of the animation:
161 // 0: Starting from right edge, a new app (index 1) comes in from the right
162 // 1: The app has reached the first snap position.
163 // 2: The list is dragged further and snaps into the spread view when entering phase 2
164 property int phase: 0
166 property int selectedIndex: -1
167 property int draggedDelegateCount: 0
168 property int closingIndex: -1
170 property bool focusChanging: false
172 onShiftedContentXChanged: {
175 if (shiftedContentX > width * positionMarker2) {
180 if (shiftedContentX < width * positionMarker2) {
182 } else if (shiftedContentX >= width * positionMarker4) {
190 if (shiftedContentX < positionMarker1 * width) {
191 snapAnimation.targetContentX = -shift;
192 snapAnimation.start();
193 } else if (shiftedContentX < positionMarker2 * width) {
195 } else if (shiftedContentX < positionMarker3 * width) {
197 } else if (phase < 2){
198 // Add 1 pixel to make sure we definitely hit positionMarker4 even with rounding errors of the animation.
199 snapAnimation.targetContentX = width * positionMarker4 + 1 - shift;
200 snapAnimation.start();
203 function snapTo(index) {
204 if (ApplicationManager.count <= index) {
205 // In case we're trying to snap to some non existing app, lets snap back to the first one
208 spreadView.selectedIndex = index;
209 // If we're not in full spread mode yet, always unwind to start pos
210 // otherwise unwind up to progress 0 of the selected index
211 if (spreadView.phase < 2) {
212 snapAnimation.targetContentX = -shift;
214 snapAnimation.targetContentX = -shift + index * spreadView.tileDistance;
216 snapAnimation.start();
219 // In case the ApplicationManager already holds an app when starting up we're missing animations
220 // Make sure we end up in the same state
221 Component.onCompleted: {
222 spreadView.contentX = -spreadView.shift
225 SequentialAnimation {
227 property int targetContentX: -spreadView.shift
229 UbuntuNumberAnimation {
232 to: snapAnimation.targetContentX
233 duration: UbuntuAnimation.FastDuration
238 if (spreadView.selectedIndex >= 0) {
239 ApplicationManager.focusApplication(ApplicationManager.get(spreadView.selectedIndex).appId);
241 spreadView.selectedIndex = -1;
242 spreadView.phase = 0;
243 spreadView.contentX = -spreadView.shift;
251 // This width controls how much the spread can be flicked left/right. It's composed of:
252 // tileDistance * app count (with a minimum of 3 apps, in order to also allow moving 1 and 2 apps a bit)
253 // + some constant value (still scales with the screen width) which looks good and somewhat fills the screen
254 width: Math.max(3, ApplicationManager.count) * spreadView.tileDistance + (spreadView.width - spreadView.tileDistance) * 1.5
256 enabled: spreadView.closingIndex >= 0
257 UbuntuNumberAnimation {}
260 if (spreadView.closingIndex >= 0) {
261 spreadView.contentX = Math.min(spreadView.contentX, width - spreadView.width - spreadView.shift);
265 x: spreadView.contentX
269 model: ApplicationManager
270 delegate: TransformedSpreadDelegate {
272 objectName: "appDelegate" + index
277 startDistance: spreadView.tileDistance
278 endDistance: units.gu(.5)
279 width: spreadView.width
280 height: spreadView.height
281 selected: spreadView.selectedIndex == index
282 otherSelected: spreadView.selectedIndex >= 0 && !selected
283 interactive: !spreadView.interactive && spreadView.phase === 0
284 && spreadView.shiftedContentX === 0 && root.interactive && index === 0
285 swipeToCloseEnabled: spreadView.interactive
286 maximizedAppTopMargin: root.maximizedAppTopMargin
287 dropShadow: spreadView.active ||
288 (priv.focusedAppDelegate && priv.focusedAppDelegate.x !== 0)
290 readonly property bool isDash: model.appId == "unity8-dash"
292 z: isDash && !spreadView.active ? -1 : behavioredIndex
295 // focused app is always positioned at 0 except when following left edge drag
297 if (!isDash && root.inverseProgress > 0) {
298 return root.inverseProgress;
302 if (isDash && !spreadView.active && !spreadDragArea.dragging) {
306 // Otherwise line up for the spread
307 return spreadView.width + (index - 1) * spreadView.tileDistance;
310 application: ApplicationManager.get(index)
313 property real behavioredIndex: index
314 Behavior on behavioredIndex {
315 enabled: spreadView.closingIndex >= 0
316 UbuntuNumberAnimation {
320 spreadView.closingIndex = -1;
327 enabled: root.spreadEnabled &&
328 !spreadView.active &&
329 !snapAnimation.running &&
331 UbuntuNumberAnimation {
332 duration: UbuntuAnimation.BriskDuration
334 if (!running && root.inverseProgress == 0) {
335 spreadView.focusChanging = false;
341 // Each tile has a different progress value running from 0 to 1.
342 // 0: means the tile is at the right edge.
343 // 1: means the tile has finished the main animation towards the left edge.
344 // >1: after the main animation has finished, tiles will continue to move very slowly to the left
346 var tileProgress = (spreadView.shiftedContentX - behavioredIndex * spreadView.tileDistance) / spreadView.width;
347 // Tile 1 needs to move directly from the beginning...
348 if (behavioredIndex == 1 && spreadView.phase < 2) {
349 tileProgress += spreadView.tileDistance / spreadView.width;
351 // Limiting progress to ~0 and 1.7 to avoid binding calculations when tiles are not
353 // < 0 : The tile is outside the screen on the right
354 // > 1.7: The tile is *very* close to the left edge and covered by other tiles now.
355 // Using 0.0001 to differentiate when a tile should still be visible (==0)
356 // or we can hide it (< 0)
357 tileProgress = Math.max(-0.0001, Math.min(1.7, tileProgress));
361 // This mostly is the same as progress, just adds the snapping to phase 1 for tiles 0 and 1
363 if (spreadView.phase == 0 && index < 2) {
364 if (progress < spreadView.positionMarker1) {
366 } else if (progress < spreadView.positionMarker1 + snappingCurve.period){
367 return spreadView.positionMarker1 + snappingCurve.value * 3;
369 return spreadView.positionMarker2;
375 // Hiding tiles when their progress is negative or reached the maximum
376 visible: (progress >= 0 && progress < 1.7) ||
377 (isDash && priv.focusedAppDelegate.x !== 0)
381 type: EasingCurve.Linear
383 progress: appDelegate.progress - spreadView.positionMarker1
388 property: "orientation"
389 when: appDelegate.interactive
390 value: root.orientation
394 if (spreadView.phase == 2) {
395 if (ApplicationManager.focusedApplicationId == ApplicationManager.get(index).appId) {
396 spreadView.snapTo(index);
398 ApplicationManager.requestFocusApplication(ApplicationManager.get(index).appId);
405 spreadView.draggedDelegateCount++;
407 spreadView.draggedDelegateCount--;
412 spreadView.closingIndex = index;
413 ApplicationManager.stopApplication(ApplicationManager.get(index).appId);
422 objectName: "spreadDragArea"
423 direction: Direction.Leftwards
424 enabled: spreadView.phase != 2 && root.spreadEnabled
426 anchors { top: parent.top; right: parent.right; bottom: parent.bottom }
427 width: root.dragAreaWidth
429 // Sitting at the right edge of the screen, this EdgeDragArea directly controls the spreadView when
430 // attachedToView is true. When the finger movement passes positionMarker3 we detach it from the
431 // spreadView and make the spreadView snap to positionMarker4.
432 property bool attachedToView: true
434 property var gesturePoints: new Array()
438 // Initial touch. Let's reset the spreadView to the starting position.
439 spreadView.phase = 0;
440 spreadView.contentX = -spreadView.shift;
442 if (dragging && status == DirectionalDragArea.Recognized && attachedToView) {
443 // Gesture recognized. Let's move the spreadView with the finger
444 var finalX = Math.min(touchX + width, width);
445 spreadView.contentX = -finalX + spreadDragArea.width - spreadView.shift;
447 if (attachedToView && spreadView.shiftedContentX >= spreadView.width * spreadView.positionMarker3) {
448 // We passed positionMarker3. Detach from spreadView and snap it.
449 attachedToView = false;
452 gesturePoints.push(touchX);
455 property int previousStatus: -1
456 property int currentStatus: DirectionalDragArea.WaitingForTouch
459 previousStatus = currentStatus;
460 currentStatus = status;
462 if (status == DirectionalDragArea.Recognized) {
463 attachedToView = true;
469 // A potential edge-drag gesture has started. Start recording it
474 // Ok. The user released. Find out if it was a one-way movement.
475 var oneWayFlick = true;
476 var smallestX = spreadDragArea.width;
477 for (var i = 0; i < gesturePoints.length; i++) {
478 if (gesturePoints[i] >= smallestX) {
482 smallestX = gesturePoints[i];
486 if (previousStatus == DirectionalDragArea.Recognized &&
487 oneWayFlick && spreadView.shiftedContentX > units.gu(2) &&
488 spreadView.shiftedContentX < spreadView.positionMarker1 * spreadView.width) {
489 // If it was a short one-way movement, do the Alt+Tab switch
490 // no matter if we didn't cross positionMarker1 yet.
491 spreadView.snapTo(1);
492 } else if (!dragging && attachedToView) {
493 // otherwise snap to the closest snap position we can find
494 // (might be back to start, to app 1 or to spread)