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"
28 // Controls to be set from outside
29 property int dragAreaWidth
30 property real maximizedAppTopMargin
31 property bool interactive
32 property bool spreadEnabled: true // If false, animations and right edge will be disabled
33 property real inverseProgress: 0 // This is the progress for left edge drags, in pixels.
34 property int orientation: Qt.PortraitOrientation
35 property QtObject applicationManager: ApplicationManager
36 property bool focusFirstApp: true // If false, focused app will appear on right edge like other apps
37 property bool altTabEnabled: true
38 property real startScale: 1.1
39 property real endScale: 0.7
41 // How far left the stage has been dragged
42 readonly property real dragProgress: spreadRepeater.count > 0 ? -spreadRepeater.itemAt(0).xTranslate : 0
44 readonly property alias dragging: spreadDragArea.dragging
46 // Only used by the tutorial right now, when it is teasing the right edge
47 property real dragAreaOverlap
53 function select(appId) {
54 spreadView.snapTo(priv.indexOf(appId));
58 spreadView.selectedIndex = -1;
60 spreadView.contentX = -spreadView.shift;
63 onInverseProgressChanged: {
64 // This can't be a simple binding because that would be triggered after this handler
65 // while we need it active before doing the anition left/right
66 priv.animateX = (inverseProgress == 0)
67 if (inverseProgress == 0 && priv.oldInverseProgress > 0) {
68 // left edge drag released. Minimum distance is given by design.
69 if (priv.oldInverseProgress > units.gu(22)) {
70 applicationManager.requestFocusApplication("unity8-dash");
73 priv.oldInverseProgress = inverseProgress;
77 target: applicationManager
80 if (spreadView.phase > 0) {
81 spreadView.snapTo(priv.indexOf(appId));
83 applicationManager.focusApplication(appId);
88 if (spreadView.phase == 2) {
89 spreadView.snapTo(applicationManager.count - 1);
92 spreadView.contentX = -spreadView.shift;
93 applicationManager.focusApplication(appId);
97 onApplicationRemoved: {
98 // Unless we're closing the app ourselves in the spread,
99 // lets make sure the spread doesn't mess up by the changing app list.
100 if (spreadView.closingIndex == -1) {
101 spreadView.phase = 0;
102 spreadView.contentX = -spreadView.shift;
107 function focusTopMostApp() {
108 if (applicationManager.count > 0) {
109 var topmostApp = applicationManager.get(0);
110 applicationManager.focusApplication(topmostApp.appId);
118 readonly property int firstSpreadIndex: root.focusFirstApp ? 1 : 0
119 property string focusedAppId: applicationManager.focusedApplicationId
120 property var focusedApplication: applicationManager.findApplication(focusedAppId)
121 property var focusedAppDelegate: null
123 property real oldInverseProgress: 0
124 property bool animateX: false
126 onFocusedAppIdChanged: focusedAppDelegate = spreadRepeater.itemAt(0);
128 onFocusedAppDelegateChanged: {
129 if (focusedAppDelegate) {
130 focusedAppDelegate.focus = true;
134 function indexOf(appId) {
135 for (var i = 0; i < applicationManager.count; i++) {
136 if (applicationManager.get(i).appId == appId) {
147 objectName: "spreadView"
149 interactive: (spreadDragArea.status == DirectionalDragArea.Recognized || phase > 1)
150 && draggedDelegateCount === 0
151 contentWidth: spreadRow.width - shift
154 // This indicates when the spreadView is active. That means, all the animations
155 // are activated and tiles need to line up for the spread.
156 readonly property bool active: shiftedContentX > 0 || spreadDragArea.status === DirectionalDragArea.Recognized || !root.focusFirstApp
158 // The flickable needs to fill the screen in order to get touch events all over.
159 // However, we don't want to the user to be able to scroll back all the way. For
160 // that, the beginning of the gesture starts with a negative value for contentX
161 // so the flickable wants to pull it into the view already. "shift" tunes the
162 // distance where to "lock" the content.
163 readonly property real shift: width / 2
164 readonly property real shiftedContentX: contentX + shift
166 property int tileDistance: width / 4
168 // Those markers mark the various positions in the spread (ratio to screen width from right to left):
169 // 0 - 1: following finger, snap back to the beginning on release
170 property real positionMarker1: 0.2
171 // 1 - 2: curved snapping movement, snap to app 1 on release
172 property real positionMarker2: 0.3
173 // 2 - 3: movement follows finger, snaps back to app 1 on release
174 property real positionMarker3: 0.35
175 // passing 3, we detach movement from the finger and snap to 4
176 property real positionMarker4: 0.9
178 // This is where the first app snaps to when bringing it in from the right edge.
179 property real snapPosition: 0.7
181 // Phase of the animation:
182 // 0: Starting from right edge, a new app (index 1) comes in from the right
183 // 1: The app has reached the first snap position.
184 // 2: The list is dragged further and snaps into the spread view when entering phase 2
185 property int phase: 0
187 property int selectedIndex: -1
188 property int draggedDelegateCount: 0
189 property int closingIndex: -1
191 property bool focusChanging: false
193 onShiftedContentXChanged: {
196 if (shiftedContentX > width * positionMarker2) {
201 if (shiftedContentX < width * positionMarker2) {
203 } else if (shiftedContentX >= width * positionMarker4 && !spreadDragArea.dragging) {
211 if (shiftedContentX < positionMarker1 * width) {
212 snapAnimation.targetContentX = -shift;
213 snapAnimation.start();
214 } else if (shiftedContentX < positionMarker2 * width) {
216 } else if (shiftedContentX < positionMarker3 * width) {
218 } else if (phase < 2){
219 // Add 1 pixel to make sure we definitely hit positionMarker4 even with rounding errors of the animation.
220 snapAnimation.targetContentX = width * positionMarker4 + 1 - shift;
221 snapAnimation.start();
225 function snapTo(index) {
226 if (!root.altTabEnabled) {
227 // Reset to start instead
228 snapAnimation.targetContentX = -shift;
229 snapAnimation.start();
232 if (applicationManager.count <= index) {
233 // In case we're trying to snap to some non existing app, lets snap back to the first one
236 spreadView.selectedIndex = index;
237 // If we're not in full spread mode yet, always unwind to start pos
238 // otherwise unwind up to progress 0 of the selected index
239 if (spreadView.phase < 2) {
240 snapAnimation.targetContentX = -shift;
242 snapAnimation.targetContentX = -shift + index * spreadView.tileDistance;
244 snapAnimation.start();
247 // In case the applicationManager already holds an app when starting up we're missing animations
248 // Make sure we end up in the same state
249 Component.onCompleted: {
250 spreadView.contentX = -spreadView.shift
251 priv.animateX = true;
252 snapAnimation.complete();
255 SequentialAnimation {
257 property int targetContentX: -spreadView.shift
259 UbuntuNumberAnimation {
262 to: snapAnimation.targetContentX
263 duration: UbuntuAnimation.FastDuration
268 if (spreadView.selectedIndex >= 0) {
269 applicationManager.focusApplication(applicationManager.get(spreadView.selectedIndex).appId);
271 spreadView.selectedIndex = -1;
272 spreadView.phase = 0;
273 spreadView.contentX = -spreadView.shift;
281 // This width controls how much the spread can be flicked left/right. It's composed of:
282 // tileDistance * app count (with a minimum of 3 apps, in order to also allow moving 1 and 2 apps a bit)
283 // + some constant value (still scales with the screen width) which looks good and somewhat fills the screen
284 width: Math.max(3, applicationManager.count) * spreadView.tileDistance + (spreadView.width - spreadView.tileDistance) * 1.5
285 height: parent.height
287 enabled: spreadView.closingIndex >= 0
288 UbuntuNumberAnimation {}
291 if (spreadView.closingIndex >= 0) {
292 spreadView.contentX = Math.min(spreadView.contentX, width - spreadView.width - spreadView.shift);
296 x: spreadView.contentX
299 if (root.altTabEnabled) {
300 spreadView.snapTo(0);
306 objectName: "spreadRepeater"
307 model: applicationManager
308 delegate: TransformedSpreadDelegate {
310 objectName: "appDelegate" + index
313 startScale: root.startScale
314 endScale: root.endScale
315 startDistance: spreadView.tileDistance
316 endDistance: units.gu(.5)
317 width: spreadView.width
318 height: spreadView.height
319 selected: spreadView.selectedIndex == index
320 otherSelected: spreadView.selectedIndex >= 0 && !selected
321 interactive: !spreadView.interactive && spreadView.phase === 0
322 && spreadView.shiftedContentX === 0 && root.interactive && isFocused
323 swipeToCloseEnabled: spreadView.interactive && root.interactive && !snapAnimation.running
324 maximizedAppTopMargin: root.maximizedAppTopMargin
325 dropShadow: spreadView.active ||
326 (priv.focusedAppDelegate && priv.focusedAppDelegate.x !== 0)
327 focusFirstApp: root.focusFirstApp
329 readonly property bool isDash: model.appId == "unity8-dash"
331 z: isDash && !spreadView.active ? -1 : behavioredIndex
334 // focused app is always positioned at 0 except when following left edge drag
336 if (!isDash && root.inverseProgress > 0 && spreadView.phase === 0) {
337 return root.inverseProgress;
341 if (isDash && !spreadView.active && !spreadDragArea.dragging) {
345 // Otherwise line up for the spread
346 return spreadView.width + spreadIndex * spreadView.tileDistance;
349 application: applicationManager.get(index)
352 property real behavioredIndex: index
353 Behavior on behavioredIndex {
354 enabled: spreadView.closingIndex >= 0
355 UbuntuNumberAnimation {
359 spreadView.closingIndex = -1;
366 enabled: root.spreadEnabled &&
367 !spreadView.active &&
368 !snapAnimation.running &&
370 UbuntuNumberAnimation {
371 duration: UbuntuAnimation.BriskDuration
373 if (!running && root.inverseProgress == 0) {
374 spreadView.focusChanging = false;
380 // Each tile has a different progress value running from 0 to 1.
381 // 0: means the tile is at the right edge.
382 // 1: means the tile has finished the main animation towards the left edge.
383 // >1: after the main animation has finished, tiles will continue to move very slowly to the left
385 var tileProgress = (spreadView.shiftedContentX - behavioredIndex * spreadView.tileDistance) / spreadView.width;
386 // Tile 1 needs to move directly from the beginning...
387 if (root.focusFirstApp && behavioredIndex == 1 && spreadView.phase < 2) {
388 tileProgress += spreadView.tileDistance / spreadView.width;
390 // Limiting progress to ~0 and 1.7 to avoid binding calculations when tiles are not
392 // < 0 : The tile is outside the screen on the right
393 // > 1.7: The tile is *very* close to the left edge and covered by other tiles now.
394 // Using 0.0001 to differentiate when a tile should still be visible (==0)
395 // or we can hide it (< 0)
396 tileProgress = Math.max(-0.0001, Math.min(1.7, tileProgress));
400 // This mostly is the same as progress, just adds the snapping to phase 1 for tiles 0 and 1
402 if (spreadView.phase == 0 && index <= priv.firstSpreadIndex) {
403 if (progress < spreadView.positionMarker1) {
405 } else if (progress < spreadView.positionMarker1 + 0.05){
406 // p : 0.05 = x : pm2
407 return spreadView.positionMarker1 + (progress - spreadView.positionMarker1) * (spreadView.positionMarker2 - spreadView.positionMarker1) / 0.05
409 return spreadView.positionMarker2;
415 // Hiding tiles when their progress is negative or reached the maximum
416 visible: (progress >= 0 && progress < 1.7) ||
417 (isDash && priv.focusedAppDelegate.x !== 0)
421 property: "orientation"
422 when: appDelegate.interactive
423 value: root.orientation
427 if (root.altTabEnabled && spreadView.phase == 2) {
428 if (applicationManager.focusedApplicationId == applicationManager.get(index).appId) {
429 spreadView.snapTo(index);
431 applicationManager.requestFocusApplication(applicationManager.get(index).appId);
438 spreadView.draggedDelegateCount++;
440 spreadView.draggedDelegateCount--;
445 spreadView.closingIndex = index;
446 applicationManager.stopApplication(applicationManager.get(index).appId);
455 objectName: "spreadDragArea"
456 direction: Direction.Leftwards
457 enabled: (spreadView.phase != 2 && root.spreadEnabled) || dragging
459 anchors { top: parent.top; right: parent.right; bottom: parent.bottom; rightMargin: -root.dragAreaOverlap }
460 width: root.dragAreaWidth
462 property var gesturePoints: new Array()
466 // Initial touch. Let's reset the spreadView to the starting position.
467 spreadView.phase = 0;
468 spreadView.contentX = -spreadView.shift;
470 if (dragging && status == DirectionalDragArea.Recognized) {
471 // Gesture recognized. Let's move the spreadView with the finger
472 var dragX = Math.min(touchX + width, width); // Prevent dragging rightwards
473 dragX = -dragX + spreadDragArea.width - spreadView.shift;
474 // Don't allow dragging further than the animation crossing with phase2's animation
475 var maxMovement = spreadView.width * spreadView.positionMarker4 - spreadView.shift;
476 spreadView.contentX = Math.min(dragX, maxMovement);
478 gesturePoints.push(touchX);
481 property int previousStatus: -1
482 property int currentStatus: DirectionalDragArea.WaitingForTouch
485 previousStatus = currentStatus;
486 currentStatus = status;
491 // A potential edge-drag gesture has started. Start recording it
496 // Ok. The user released. Find out if it was a one-way movement.
497 var oneWayFlick = true;
498 var smallestX = spreadDragArea.width;
499 for (var i = 0; i < gesturePoints.length; i++) {
500 if (gesturePoints[i] >= smallestX) {
504 smallestX = gesturePoints[i];
508 if (previousStatus == DirectionalDragArea.Recognized &&
509 oneWayFlick && spreadView.shiftedContentX > units.gu(2) &&
510 spreadView.shiftedContentX < spreadView.positionMarker1 * spreadView.width) {
511 // If it was a short one-way movement, do the Alt+Tab switch
512 // no matter if we didn't cross positionMarker1 yet.
513 spreadView.snapTo(1);
514 } else if (!dragging) {
515 // otherwise snap to the closest snap position we can find
516 // (might be back to start, to app 1 or to spread)