2 * Copyright (C) 2014-2015 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 QtObject applicationManager: ApplicationManager
35 property bool focusFirstApp: true // If false, focused app will appear on right edge like other apps
36 property bool altTabEnabled: true
37 property real startScale: 1.1
38 property real endScale: 0.7
39 property bool keepDashRunning: true
40 property bool suspended: false
41 property int shellOrientationAngle: 0
42 property int shellOrientation
43 property int shellPrimaryOrientation
44 property int nativeOrientation
45 property real nativeWidth
46 property real nativeHeight
47 property bool beingResized: false
48 onBeingResizedChanged: {
50 // Brace yourselves for impact!
54 onSpreadEnabledChanged: {
59 function updateFocusedAppOrientation() {
60 if (spreadRepeater.count > 0) {
61 spreadRepeater.itemAt(0).matchShellOrientation();
64 for (var i = 1; i < spreadRepeater.count; ++i) {
66 var spreadDelegate = spreadRepeater.itemAt(i);
68 var delta = spreadDelegate.appWindowOrientationAngle - root.shellOrientationAngle;
69 if (delta < 0) { delta += 360; }
72 var supportedOrientations = spreadDelegate.application.supportedOrientations;
73 if (supportedOrientations === Qt.PrimaryOrientation) {
74 supportedOrientations = spreadDelegate.shellPrimaryOrientation;
77 if (delta === 180 && (supportedOrientations & spreadDelegate.shellOrientation)) {
78 spreadDelegate.matchShellOrientation();
82 function updateFocusedAppOrientationAnimated() {
83 if (spreadRepeater.count > 0) {
84 spreadRepeater.itemAt(0).animateToShellOrientation();
88 // To be read from outside
89 readonly property var mainApp: applicationManager.focusedApplicationId
90 ? applicationManager.findApplication(applicationManager.focusedApplicationId)
93 property int mainAppWindowOrientationAngle: 0
94 readonly property bool orientationChangesEnabled: priv.focusedAppOrientationChangesEnabled
95 && !priv.focusedAppDelegateIsDislocated
96 && !(priv.focusedAppDelegate && priv.focusedAppDelegate.xBehavior.running)
97 && spreadView.phase === 0
99 // How far left the stage has been dragged
100 readonly property real dragProgress: spreadRepeater.count > 0 ? -spreadRepeater.itemAt(0).xTranslate : 0
102 readonly property alias dragging: spreadDragArea.dragging
104 // Only used by the tutorial right now, when it is teasing the right edge
105 property real dragAreaOverlap
111 function select(appId) {
112 spreadView.snapTo(priv.indexOf(appId));
115 onInverseProgressChanged: {
116 // This can't be a simple binding because that would be triggered after this handler
117 // while we need it active before doing the anition left/right
118 priv.animateX = (inverseProgress == 0)
119 if (inverseProgress == 0 && priv.oldInverseProgress > 0) {
120 // left edge drag released. Minimum distance is given by design.
121 if (priv.oldInverseProgress > units.gu(22)) {
122 applicationManager.requestFocusApplication("unity8-dash");
125 priv.oldInverseProgress = inverseProgress;
128 // <FIXME-contentX> See rationale in the next comment with this tag
130 if (!root.beingResized) {
131 // we're being resized without a warning (ie, the corresponding property wasn't set
132 root.beingResized = true;
133 beingResizedTimer.start();
137 id: beingResizedTimer
139 onTriggered: { root.beingResized = false; }
143 target: applicationManager
146 if (spreadView.phase > 0) {
147 spreadView.snapTo(priv.indexOf(appId));
149 applicationManager.focusApplication(appId);
153 onApplicationAdded: {
154 if (spreadView.phase == 2) {
155 spreadView.snapTo(applicationManager.count - 1);
157 spreadView.phase = 0;
158 spreadView.contentX = -spreadView.shift;
159 applicationManager.focusApplication(appId);
163 onApplicationRemoved: {
164 // Unless we're closing the app ourselves in the spread,
165 // lets make sure the spread doesn't mess up by the changing app list.
166 if (spreadView.closingIndex == -1) {
167 spreadView.phase = 0;
168 spreadView.contentX = -spreadView.shift;
173 function focusTopMostApp() {
174 if (applicationManager.count > 0) {
175 var topmostApp = applicationManager.get(0);
176 applicationManager.focusApplication(topmostApp.appId);
184 property string focusedAppId: root.applicationManager.focusedApplicationId
185 property bool focusedAppOrientationChangesEnabled: false
186 readonly property int firstSpreadIndex: root.focusFirstApp ? 1 : 0
187 readonly property var focusedAppDelegate: {
188 var index = indexOf(focusedAppId);
189 return index >= 0 && index < spreadRepeater.count ? spreadRepeater.itemAt(index) : null
192 property real oldInverseProgress: 0
193 property bool animateX: false
195 onFocusedAppDelegateChanged: {
196 if (focusedAppDelegate) {
197 focusedAppDelegate.focus = true;
201 property bool focusedAppDelegateIsDislocated: focusedAppDelegate && focusedAppDelegate.x !== 0
203 function indexOf(appId) {
204 for (var i = 0; i < root.applicationManager.count; i++) {
205 if (root.applicationManager.get(i).appId == appId) {
212 // Is more stable than "spreadView.shiftedContentX === 0" as it filters out noise caused by
213 // Flickable.contentX changing due to resizes.
214 property bool fullyShowingFocusedApp: true
217 // The app that's about to go to foreground has to be focused, otherwise
218 // it would leave us in an inconsistent state.
219 if (!root.applicationManager.focusedApplicationId && root.applicationManager.count > 0) {
220 root.applicationManager.focusApplication(root.applicationManager.get(0).appId);
223 spreadView.selectedIndex = -1;
224 spreadView.phase = 0;
225 spreadView.contentX = -spreadView.shift;
229 id: fullyShowingFocusedAppUpdateTimer
232 priv.fullyShowingFocusedApp = spreadView.shiftedContentX === 0;
238 objectName: "spreadView"
240 interactive: (spreadDragArea.dragging || phase > 1) && draggedDelegateCount === 0
241 contentWidth: spreadRow.width - shift
244 // This indicates when the spreadView is active. That means, all the animations
245 // are activated and tiles need to line up for the spread.
246 readonly property bool active: shiftedContentX > 0 || spreadDragArea.dragging || !root.focusFirstApp
248 // The flickable needs to fill the screen in order to get touch events all over.
249 // However, we don't want to the user to be able to scroll back all the way. For
250 // that, the beginning of the gesture starts with a negative value for contentX
251 // so the flickable wants to pull it into the view already. "shift" tunes the
252 // distance where to "lock" the content.
253 readonly property real shift: width / 2
254 readonly property real shiftedContentX: contentX + shift
256 property int tileDistance: width / 4
258 // Those markers mark the various positions in the spread (ratio to screen width from right to left):
259 // 0 - 1: following finger, snap back to the beginning on release
260 property real positionMarker1: 0.2
261 // 1 - 2: curved snapping movement, snap to app 1 on release
262 property real positionMarker2: 0.3
263 // 2 - 3: movement follows finger, snaps back to app 1 on release
264 property real positionMarker3: 0.35
265 // passing 3, we detach movement from the finger and snap to 4
266 property real positionMarker4: 0.9
268 // This is where the first app snaps to when bringing it in from the right edge.
269 property real snapPosition: 0.7
271 // Phase of the animation:
272 // 0: Starting from right edge, a new app (index 1) comes in from the right
273 // 1: The app has reached the first snap position.
274 // 2: The list is dragged further and snaps into the spread view when entering phase 2
275 property int phase: 0
277 property int selectedIndex: -1
278 property int draggedDelegateCount: 0
279 property int closingIndex: -1
281 // <FIXME-contentX> Workaround Flickable's behavior of bringing contentX back between valid boundaries
282 // when resized. The proper way to fix this is refactoring PhoneStage so that it doesn't
283 // rely on having Flickable.contentX keeping an out-of-bounds value when it's set programatically
284 // (as opposed to having contentX reaching an out-of-bounds value through dragging, which will trigger
285 // the Flickable.boundsBehavior upon release).
286 onContentXChanged: { forceItToRemainStillIfBeingResized(); }
287 onShiftChanged: { forceItToRemainStillIfBeingResized(); }
288 function forceItToRemainStillIfBeingResized() {
289 if (root.beingResized && contentX != -spreadView.shift) {
290 contentX = -spreadView.shift;
294 onShiftedContentXChanged: {
295 if (root.beingResized) {
296 // Flickabe.contentX wiggles during resizes. Don't react to it.
302 // the "spreadEnabled" part is because when code does "phase = 0; contentX = -shift" to
303 // dismiss the spread because spreadEnabled went to false, for some reason, during tests,
304 // Flickable might jump in and change contentX value back, causing the code below to do
305 // "phase = 1" which will make the spread stay.
306 // It sucks that we have no control whatsoever over whether or when Flickable animates its
308 if (root.spreadEnabled && shiftedContentX > width * positionMarker2) {
313 if (shiftedContentX < width * positionMarker2) {
315 } else if (shiftedContentX >= width * positionMarker4 && !spreadDragArea.dragging) {
320 fullyShowingFocusedAppUpdateTimer.restart();
324 if (shiftedContentX < positionMarker1 * width) {
325 snapAnimation.targetContentX = -shift;
326 snapAnimation.start();
327 } else if (shiftedContentX < positionMarker2 * width) {
329 } else if (shiftedContentX < positionMarker3 * width) {
331 } else if (phase < 2){
332 // Add 1 pixel to make sure we definitely hit positionMarker4 even with rounding errors of the animation.
333 snapAnimation.targetContentX = width * positionMarker4 + 1 - shift;
334 snapAnimation.start();
338 function snapTo(index) {
339 if (!root.altTabEnabled) {
340 // Reset to start instead
341 snapAnimation.targetContentX = -shift;
342 snapAnimation.start();
345 if (root.applicationManager.count <= index) {
346 // In case we're trying to snap to some non existing app, lets snap back to the first one
349 spreadView.selectedIndex = index;
350 // If we're not in full spread mode yet, always unwind to start pos
351 // otherwise unwind up to progress 0 of the selected index
352 if (spreadView.phase < 2) {
353 snapAnimation.targetContentX = -shift;
355 snapAnimation.targetContentX = -shift + index * spreadView.tileDistance;
357 snapAnimation.start();
360 // In case the applicationManager already holds an app when starting up we're missing animations
361 // Make sure we end up in the same state
362 Component.onCompleted: {
363 spreadView.contentX = -spreadView.shift
364 priv.animateX = true;
365 snapAnimation.complete();
368 SequentialAnimation {
370 property int targetContentX: -spreadView.shift
372 UbuntuNumberAnimation {
375 to: snapAnimation.targetContentX
376 duration: UbuntuAnimation.FastDuration
381 if (spreadView.selectedIndex >= 0) {
382 root.applicationManager.focusApplication(root.applicationManager.get(spreadView.selectedIndex).appId);
384 spreadView.selectedIndex = -1;
385 spreadView.phase = 0;
386 spreadView.contentX = -spreadView.shift;
394 // This width controls how much the spread can be flicked left/right. It's composed of:
395 // tileDistance * app count (with a minimum of 3 apps, in order to also allow moving 1 and 2 apps a bit)
396 // + some constant value (still scales with the screen width) which looks good and somewhat fills the screen
397 width: Math.max(3, root.applicationManager.count) * spreadView.tileDistance + (spreadView.width - spreadView.tileDistance) * 1.5
398 height: parent.height
400 enabled: spreadView.closingIndex >= 0
401 UbuntuNumberAnimation {}
404 if (spreadView.closingIndex >= 0) {
405 spreadView.contentX = Math.min(spreadView.contentX, width - spreadView.width - spreadView.shift);
409 x: spreadView.contentX
412 if (root.altTabEnabled) {
413 spreadView.snapTo(0);
419 objectName: "spreadRepeater"
420 model: root.applicationManager
421 delegate: TransformedSpreadDelegate {
423 objectName: "appDelegate" + index
426 startScale: root.startScale
427 endScale: root.endScale
428 startDistance: spreadView.tileDistance
429 endDistance: units.gu(.5)
430 width: spreadView.width
431 height: spreadView.height
432 selected: spreadView.selectedIndex == index
433 otherSelected: spreadView.selectedIndex >= 0 && !selected
434 interactive: !spreadView.interactive && spreadView.phase === 0
435 && priv.fullyShowingFocusedApp && root.interactive && isFocused
436 swipeToCloseEnabled: spreadView.interactive && root.interactive && !snapAnimation.running
437 maximizedAppTopMargin: root.maximizedAppTopMargin
438 dropShadow: spreadView.active || priv.focusedAppDelegateIsDislocated
439 focusFirstApp: root.focusFirstApp
442 target: appDelegate.application
443 property: "requestedState"
444 value: (isDash && root.keepDashRunning) || (!root.suspended && appDelegate.focus)
445 ? ApplicationInfoInterface.RequestedRunning
446 : ApplicationInfoInterface.RequestedSuspended
449 readonly property bool isDash: model.appId == "unity8-dash"
451 z: isDash && !spreadView.active ? -1 : behavioredIndex
454 // focused app is always positioned at 0 except when following left edge drag
456 if (!isDash && root.inverseProgress > 0 && spreadView.phase === 0) {
457 return root.inverseProgress;
461 if (isDash && !spreadView.active && !spreadDragArea.dragging) {
465 // Otherwise line up for the spread
466 return spreadView.width + spreadIndex * spreadView.tileDistance;
469 application: root.applicationManager.get(index)
472 property real behavioredIndex: index
473 Behavior on behavioredIndex {
474 enabled: spreadView.closingIndex >= 0
475 UbuntuNumberAnimation {
479 spreadView.closingIndex = -1;
485 property var xBehavior: xBehavior
488 enabled: root.spreadEnabled &&
489 !spreadView.active &&
490 !snapAnimation.running &&
491 !spreadDragArea.pressed &&
494 UbuntuNumberAnimation {
495 duration: UbuntuAnimation.BriskDuration
499 // Each tile has a different progress value running from 0 to 1.
500 // 0: means the tile is at the right edge.
501 // 1: means the tile has finished the main animation towards the left edge.
502 // >1: after the main animation has finished, tiles will continue to move very slowly to the left
504 var tileProgress = (spreadView.shiftedContentX - behavioredIndex * spreadView.tileDistance) / spreadView.width;
505 // Tile 1 needs to move directly from the beginning...
506 if (root.focusFirstApp && behavioredIndex == 1 && spreadView.phase < 2) {
507 tileProgress += spreadView.tileDistance / spreadView.width;
509 // Limiting progress to ~0 and 1.7 to avoid binding calculations when tiles are not
511 // < 0 : The tile is outside the screen on the right
512 // > 1.7: The tile is *very* close to the left edge and covered by other tiles now.
513 // Using 0.0001 to differentiate when a tile should still be visible (==0)
514 // or we can hide it (< 0)
515 tileProgress = Math.max(-0.0001, Math.min(1.7, tileProgress));
519 // This mostly is the same as progress, just adds the snapping to phase 1 for tiles 0 and 1
521 if (spreadView.phase == 0 && index <= priv.firstSpreadIndex) {
522 if (progress < spreadView.positionMarker1) {
524 } else if (progress < spreadView.positionMarker1 + 0.05){
525 // p : 0.05 = x : pm2
526 return spreadView.positionMarker1 + (progress - spreadView.positionMarker1) * (spreadView.positionMarker2 - spreadView.positionMarker1) / 0.05
528 return spreadView.positionMarker2;
534 // Hiding tiles when their progress is negative or reached the maximum
535 visible: (progress >= 0 && progress < 1.7)
536 || (isDash && priv.focusedAppDelegateIsDislocated)
539 shellOrientationAngle: root.shellOrientationAngle
540 shellOrientation: root.shellOrientation
541 shellPrimaryOrientation: root.shellPrimaryOrientation
542 nativeOrientation: root.nativeOrientation
545 if (root.altTabEnabled && spreadView.phase == 2) {
546 if (root.applicationManager.focusedApplicationId == root.applicationManager.get(index).appId) {
547 spreadView.snapTo(index);
549 root.applicationManager.requestFocusApplication(root.applicationManager.get(index).appId);
556 spreadView.draggedDelegateCount++;
558 spreadView.draggedDelegateCount--;
563 spreadView.closingIndex = index;
564 root.applicationManager.stopApplication(root.applicationManager.get(index).appId);
570 property: "mainAppWindowOrientationAngle"
571 value: appWindowOrientationAngle
576 property: "focusedAppOrientationChangesEnabled"
577 value: orientationChangesEnabled
584 //eat touch events during the right edge gesture
586 objectName: "eventEaterArea"
588 enabled: spreadDragArea.dragging
591 DirectionalDragArea {
593 objectName: "spreadDragArea"
594 direction: Direction.Leftwards
595 enabled: (spreadView.phase != 2 && root.spreadEnabled) || dragging
597 anchors { top: parent.top; right: parent.right; bottom: parent.bottom; rightMargin: -root.dragAreaOverlap }
598 width: root.dragAreaWidth
600 property var gesturePoints: new Array()
604 // Gesture recognized. Let's move the spreadView with the finger
605 var dragX = Math.min(touchX + width, width); // Prevent dragging rightwards
606 dragX = -dragX + spreadDragArea.width - spreadView.shift;
607 // Don't allow dragging further than the animation crossing with phase2's animation
608 var maxMovement = spreadView.width * spreadView.positionMarker4 - spreadView.shift;
610 spreadView.contentX = Math.min(dragX, maxMovement);
612 // Initial touch. Let's reset the spreadView to the starting position.
613 spreadView.phase = 0;
614 spreadView.contentX = -spreadView.shift;
617 gesturePoints.push(touchX);
622 // A potential edge-drag gesture has started. Start recording it
625 // Ok. The user released. Find out if it was a one-way movement.
626 var oneWayFlick = true;
627 var smallestX = spreadDragArea.width;
628 for (var i = 0; i < gesturePoints.length; i++) {
629 if (gesturePoints[i] >= smallestX) {
633 smallestX = gesturePoints[i];
637 if (oneWayFlick && spreadView.shiftedContentX > units.gu(2) &&
638 spreadView.shiftedContentX < spreadView.positionMarker1 * spreadView.width) {
639 // If it was a short one-way movement, do the Alt+Tab switch
640 // no matter if we didn't cross positionMarker1 yet.
641 spreadView.snapTo(1);
642 } else if (!dragging) {
643 // otherwise snap to the closest snap position we can find
644 // (might be back to start, to app 1 or to spread)