2 * Copyright (C) 2014-2016 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 1.3
19 import Ubuntu.Gestures 0.1
20 import Unity.Application 0.1
21 import Unity.Session 0.1
24 import "../Components"
29 property QtObject applicationManager: ApplicationManager
30 property bool focusFirstApp: true // If false, focused app will appear on right edge like other apps
31 property bool altTabEnabled: true
32 property real startScale: 1.1
33 property real endScale: 0.7
35 onBeingResizedChanged: {
37 // Brace yourselves for impact!
41 onSpreadEnabledChanged: {
47 onAltTabPressedChanged: {
48 if (!spreadEnabled || !altTabEnabled) {
52 spreadView.snapToSpread();
53 priv.highlightIndex = Math.min(spreadRepeater.count - 1, 1);
55 spreadView.snapTo(priv.highlightIndex)
60 focus: root.altTabPressed
65 priv.highlightIndex = (priv.highlightIndex + 1) % spreadRepeater.count
68 priv.highlightIndex = (priv.highlightIndex + spreadRepeater.count - 1) % spreadRepeater.count
74 // Functions to be called from outside
75 function updateFocusedAppOrientation() {
76 if (spreadRepeater.count > 0) {
77 spreadRepeater.itemAt(0).matchShellOrientation();
80 for (var i = 1; i < spreadRepeater.count; ++i) {
82 var spreadDelegate = spreadRepeater.itemAt(i);
84 var delta = spreadDelegate.appWindowOrientationAngle - root.shellOrientationAngle;
85 if (delta < 0) { delta += 360; }
88 var supportedOrientations = spreadDelegate.application.supportedOrientations;
89 if (supportedOrientations === Qt.PrimaryOrientation) {
90 supportedOrientations = root.orientations.primary;
93 if (delta === 180 && (supportedOrientations & spreadDelegate.shellOrientation)) {
94 spreadDelegate.matchShellOrientation();
98 function updateFocusedAppOrientationAnimated() {
99 if (spreadRepeater.count > 0) {
100 spreadRepeater.itemAt(0).animateToShellOrientation();
104 function pushRightEdge(amount) {
105 if (spreadView.contentX == -spreadView.shift) {
106 edgeBarrier.push(amount);
110 mainApp: applicationManager.focusedApplicationId
111 ? applicationManager.findApplication(applicationManager.focusedApplicationId)
114 mainAppWindow: priv.focusedAppDelegate ? priv.focusedAppDelegate.appWindow : null
116 orientationChangesEnabled: priv.focusedAppOrientationChangesEnabled
117 && !priv.focusedAppDelegateIsDislocated
118 && !(priv.focusedAppDelegate && priv.focusedAppDelegate.xBehavior.running)
119 && spreadView.phase === 0
121 supportedOrientations: mainApp ? mainApp.supportedOrientations
122 : (Qt.PortraitOrientation | Qt.LandscapeOrientation
123 | Qt.InvertedPortraitOrientation | Qt.InvertedLandscapeOrientation)
125 // How far left the stage has been dragged, used externally by tutorial code
126 dragProgress: spreadRepeater.count > 0 ? spreadRepeater.itemAt(0).animatedProgress : 0
128 readonly property alias dragging: spreadDragArea.dragging
132 function select(appId) {
133 spreadView.snapTo(priv.indexOf(appId));
136 onInverseProgressChanged: {
137 // This can't be a simple binding because that would be triggered after this handler
138 // while we need it active before doing the anition left/right
139 priv.animateX = (inverseProgress == 0)
140 if (inverseProgress == 0 && priv.oldInverseProgress > 0) {
141 // left edge drag released. Minimum distance is given by design.
142 if (priv.oldInverseProgress > units.gu(22)) {
143 applicationManager.requestFocusApplication("unity8-dash");
146 priv.oldInverseProgress = inverseProgress;
149 // <FIXME-contentX> See rationale in the next comment with this tag
151 if (!root.beingResized) {
152 // we're being resized without a warning (ie, the corresponding property wasn't set
153 root.beingResized = true;
154 beingResizedTimer.start();
158 id: beingResizedTimer
160 onTriggered: { root.beingResized = false; }
164 target: applicationManager
167 if (spreadView.phase > 0) {
168 spreadView.snapTo(priv.indexOf(appId));
170 applicationManager.focusApplication(appId);
174 onApplicationAdded: {
175 if (spreadView.phase == 2) {
176 spreadView.snapTo(applicationManager.count - 1);
178 spreadView.phase = 0;
179 spreadView.contentX = -spreadView.shift;
180 applicationManager.focusApplication(appId);
184 onApplicationRemoved: {
185 // Unless we're closing the app ourselves in the spread,
186 // lets make sure the spread doesn't mess up by the changing app list.
187 if (spreadView.closingIndex == -1) {
188 spreadView.phase = 0;
189 spreadView.contentX = -spreadView.shift;
194 function focusTopMostApp() {
195 if (applicationManager.count > 0) {
196 var topmostApp = applicationManager.get(0);
197 applicationManager.focusApplication(topmostApp.appId);
205 property string focusedAppId: root.applicationManager.focusedApplicationId
206 property bool focusedAppOrientationChangesEnabled: false
207 readonly property int firstSpreadIndex: root.focusFirstApp ? 1 : 0
208 readonly property var focusedAppDelegate: {
209 var index = indexOf(focusedAppId);
210 return index >= 0 && index < spreadRepeater.count ? spreadRepeater.itemAt(index) : null
213 property real oldInverseProgress: 0
214 property bool animateX: false
215 property int highlightIndex: 0
217 onFocusedAppDelegateChanged: {
218 if (focusedAppDelegate) {
219 focusedAppDelegate.focus = true;
223 property bool focusedAppDelegateIsDislocated: focusedAppDelegate &&
224 (focusedAppDelegate.x !== 0 || focusedAppDelegate.xBehavior.running)
226 function indexOf(appId) {
227 for (var i = 0; i < root.applicationManager.count; i++) {
228 if (root.applicationManager.get(i).appId == appId) {
235 // Is more stable than "spreadView.shiftedContentX === 0" as it filters out noise caused by
236 // Flickable.contentX changing due to resizes.
237 property bool fullyShowingFocusedApp: true
240 // The app that's about to go to foreground has to be focused, otherwise
241 // it would leave us in an inconsistent state.
242 if (!root.applicationManager.focusedApplicationId && root.applicationManager.count > 0) {
243 root.applicationManager.focusApplication(root.applicationManager.get(0).appId);
246 spreadView.selectedIndex = -1;
247 spreadView.phase = 0;
248 spreadView.contentX = -spreadView.shift;
251 onHighlightIndexChanged: {
252 spreadView.contentX = highlightIndex * spreadView.contentWidth / (spreadRepeater.count + 2)
256 id: fullyShowingFocusedAppUpdateTimer
259 priv.fullyShowingFocusedApp = spreadView.shiftedContentX === 0;
265 objectName: "spreadView"
267 interactive: (spreadDragArea.dragging || phase > 1) && draggedDelegateCount === 0
268 contentWidth: spreadRow.width - shift
271 // This indicates when the spreadView is active. That means, all the animations
272 // are activated and tiles need to line up for the spread.
273 readonly property bool active: shiftedContentX > 0 || spreadDragArea.dragging || !root.focusFirstApp
275 // The flickable needs to fill the screen in order to get touch events all over.
276 // However, we don't want to the user to be able to scroll back all the way. For
277 // that, the beginning of the gesture starts with a negative value for contentX
278 // so the flickable wants to pull it into the view already. "shift" tunes the
279 // distance where to "lock" the content.
280 readonly property real shift: width / 2
281 readonly property real shiftedContentX: contentX + shift
283 property int tileDistance: width / 4
285 // Those markers mark the various positions in the spread (ratio to screen width from right to left):
286 // 0 - 1: following finger, snap back to the beginning on release
287 property real positionMarker1: 0.2
288 // 1 - 2: curved snapping movement, snap to app 1 on release
289 property real positionMarker2: 0.3
290 // 2 - 3: movement follows finger, snaps back to app 1 on release
291 property real positionMarker3: 0.35
292 // passing 3, we detach movement from the finger and snap to 4
293 property real positionMarker4: 0.9
295 // This is where the first app snaps to when bringing it in from the right edge.
296 property real snapPosition: 0.7
298 // Phase of the animation:
299 // 0: Starting from right edge, a new app (index 1) comes in from the right
300 // 1: The app has reached the first snap position.
301 // 2: The list is dragged further and snaps into the spread view when entering phase 2
302 property int phase: 0
304 property int selectedIndex: -1
305 property int draggedDelegateCount: 0
306 property int closingIndex: -1
308 // <FIXME-contentX> Workaround Flickable's behavior of bringing contentX back between valid boundaries
309 // when resized. The proper way to fix this is refactoring PhoneStage so that it doesn't
310 // rely on having Flickable.contentX keeping an out-of-bounds value when it's set programatically
311 // (as opposed to having contentX reaching an out-of-bounds value through dragging, which will trigger
312 // the Flickable.boundsBehavior upon release).
313 onContentXChanged: { forceItToRemainStillIfBeingResized(); }
314 onShiftChanged: { forceItToRemainStillIfBeingResized(); }
315 function forceItToRemainStillIfBeingResized() {
316 if (root.beingResized && contentX != -spreadView.shift) {
317 contentX = -spreadView.shift;
321 Behavior on contentX {
322 enabled: root.altTabPressed
323 UbuntuNumberAnimation {}
326 onShiftedContentXChanged: {
327 if (root.beingResized) {
328 // Flickabe.contentX wiggles during resizes. Don't react to it.
334 // the "spreadEnabled" part is because when code does "phase = 0; contentX = -shift" to
335 // dismiss the spread because spreadEnabled went to false, for some reason, during tests,
336 // Flickable might jump in and change contentX value back, causing the code below to do
337 // "phase = 1" which will make the spread stay.
338 // It sucks that we have no control whatsoever over whether or when Flickable animates its
340 if (root.spreadEnabled && shiftedContentX > width * positionMarker2) {
345 if (shiftedContentX < width * positionMarker2) {
347 } else if (shiftedContentX >= width * positionMarker4 && !spreadDragArea.dragging) {
352 fullyShowingFocusedAppUpdateTimer.restart();
356 if (shiftedContentX < positionMarker1 * width) {
357 snapAnimation.targetContentX = -shift;
358 snapAnimation.start();
359 } else if (shiftedContentX < positionMarker2 * width) {
361 } else if (shiftedContentX < positionMarker3 * width) {
363 } else if (phase < 2){
369 function snapToSpread() {
370 // Add 1 pixel to make sure we definitely hit positionMarker4 even with rounding errors of the animation.
371 snapAnimation.targetContentX = (root.width * spreadView.positionMarker4) + 1 - spreadView.shift;
372 snapAnimation.start();
375 function snapTo(index) {
376 if (!root.altTabEnabled) {
377 // Reset to start instead
378 snapAnimation.targetContentX = -shift;
379 snapAnimation.start();
382 if (root.applicationManager.count <= index) {
383 // In case we're trying to snap to some non existing app, lets snap back to the first one
386 spreadView.selectedIndex = index;
387 // If we're not in full spread mode yet, always unwind to start pos
388 // otherwise unwind up to progress 0 of the selected index
389 if (spreadView.phase < 2) {
390 snapAnimation.targetContentX = -shift;
392 snapAnimation.targetContentX = -shift + index * spreadView.tileDistance;
394 snapAnimation.start();
397 // In case the applicationManager already holds an app when starting up we're missing animations
398 // Make sure we end up in the same state
399 Component.onCompleted: {
400 spreadView.contentX = -spreadView.shift
401 priv.animateX = true;
402 snapAnimation.complete();
405 SequentialAnimation {
407 property int targetContentX: -spreadView.shift
409 UbuntuNumberAnimation {
412 to: snapAnimation.targetContentX
413 duration: UbuntuAnimation.FastDuration
418 if (spreadView.selectedIndex >= 0) {
419 root.applicationManager.focusApplication(root.applicationManager.get(spreadView.selectedIndex).appId);
421 spreadView.selectedIndex = -1;
422 spreadView.phase = 0;
423 spreadView.contentX = -spreadView.shift;
431 // This width controls how much the spread can be flicked left/right. It's composed of:
432 // tileDistance * app count (with a minimum of 3 apps, in order to also allow moving 1 and 2 apps a bit)
433 // + some constant value (still scales with the screen width) which looks good and somewhat fills the screen
434 width: Math.max(3, root.applicationManager.count) * spreadView.tileDistance + (spreadView.width - spreadView.tileDistance) * 1.5
435 height: parent.height
437 enabled: spreadView.closingIndex >= 0
438 UbuntuNumberAnimation {}
441 if (spreadView.closingIndex >= 0) {
442 spreadView.contentX = Math.min(spreadView.contentX, width - spreadView.width - spreadView.shift);
446 x: spreadView.contentX
449 if (root.altTabEnabled) {
450 spreadView.snapTo(0);
456 objectName: "spreadRepeater"
457 model: root.applicationManager
458 delegate: TransformedSpreadDelegate {
460 objectName: "appDelegate" + index
463 startScale: root.startScale
464 endScale: root.endScale
465 startDistance: spreadView.tileDistance
466 endDistance: units.gu(.5)
467 width: spreadView.width
468 height: spreadView.height
469 selected: spreadView.selectedIndex == index
470 otherSelected: spreadView.selectedIndex >= 0 && !selected
471 interactive: !spreadView.interactive && spreadView.phase === 0
472 && priv.fullyShowingFocusedApp && root.interactive && isFocused
473 swipeToCloseEnabled: spreadView.interactive && root.interactive && !snapAnimation.running
474 maximizedAppTopMargin: root.maximizedAppTopMargin
475 dropShadow: spreadView.active || priv.focusedAppDelegateIsDislocated
476 focusFirstApp: root.focusFirstApp
477 highlightShown: root.altTabPressed && index === priv.highlightIndex
479 readonly property bool isDash: model.appId == "unity8-dash"
482 target: appDelegate.application
483 property: "exemptFromLifecycle"
484 value: !model.isTouchApp || isExemptFromLifecycle(model.appId)
488 target: appDelegate.application
489 property: "requestedState"
490 value: (isDash && root.keepDashRunning)
491 || (!root.suspended && appDelegate.focus)
492 ? ApplicationInfoInterface.RequestedRunning
493 : ApplicationInfoInterface.RequestedSuspended
496 z: isDash && !spreadView.active ? -1 : behavioredIndex
499 // focused app is always positioned at 0 except when following left edge drag
501 if (!isDash && root.inverseProgress > 0 && spreadView.phase === 0) {
502 return root.inverseProgress;
506 if (isDash && !spreadView.active && !spreadDragArea.dragging) {
510 // Otherwise line up for the spread
511 return spreadView.width + spreadIndex * spreadView.tileDistance;
514 application: root.applicationManager.get(index)
517 property real behavioredIndex: index
518 Behavior on behavioredIndex {
519 enabled: spreadView.closingIndex >= 0
520 UbuntuNumberAnimation {
524 spreadView.closingIndex = -1;
530 property var xBehavior: xBehavior
532 enabled: root.spreadEnabled &&
533 !spreadView.active &&
534 !snapAnimation.running &&
535 !spreadDragArea.pressed &&
538 UbuntuNumberAnimation {
540 duration: UbuntuAnimation.BriskDuration
544 // Each tile has a different progress value running from 0 to 1.
545 // 0: means the tile is at the right edge.
546 // 1: means the tile has finished the main animation towards the left edge.
547 // >1: after the main animation has finished, tiles will continue to move very slowly to the left
549 var tileProgress = (spreadView.shiftedContentX - behavioredIndex * spreadView.tileDistance) / spreadView.width;
550 // Tile 1 needs to move directly from the beginning...
551 if (root.focusFirstApp && behavioredIndex == 1 && spreadView.phase < 2) {
552 tileProgress += spreadView.tileDistance / spreadView.width;
554 // Limiting progress to ~0 and 1.7 to avoid binding calculations when tiles are not
556 // < 0 : The tile is outside the screen on the right
557 // > 1.7: The tile is *very* close to the left edge and covered by other tiles now.
558 // Using 0.0001 to differentiate when a tile should still be visible (==0)
559 // or we can hide it (< 0)
560 tileProgress = Math.max(-0.0001, Math.min(1.7, tileProgress));
564 // This mostly is the same as progress, just adds the snapping to phase 1 for tiles 0 and 1
566 if (spreadView.phase == 0 && index <= priv.firstSpreadIndex) {
567 if (progress < spreadView.positionMarker1) {
569 } else if (progress < spreadView.positionMarker1 + 0.05){
570 // p : 0.05 = x : pm2
571 return spreadView.positionMarker1 + (progress - spreadView.positionMarker1) * (spreadView.positionMarker2 - spreadView.positionMarker1) / 0.05
573 return spreadView.positionMarker2;
579 // Hide tile when progress is such that it will be off screen.
580 property bool occluded: {
581 if (spreadView.active && (progress >= 0 && progress < 1.7)) return false;
582 else if (!spreadView.active && isFocused) return false;
583 else if (xBehavior.running) return false;
584 else if (z <= 1 && priv.focusedAppDelegateIsDislocated) return false;
588 visible: Powerd.status == Powerd.On &&
589 !greeter.fullyShown &&
592 shellOrientationAngle: root.shellOrientationAngle
593 shellOrientation: root.shellOrientation
594 orientations: root.orientations
597 if (root.altTabEnabled && spreadView.phase == 2) {
598 if (root.applicationManager.focusedApplicationId == root.applicationManager.get(index).appId) {
599 spreadView.snapTo(index);
601 root.applicationManager.requestFocusApplication(root.applicationManager.get(index).appId);
608 spreadView.draggedDelegateCount++;
610 spreadView.draggedDelegateCount--;
615 spreadView.closingIndex = index;
616 root.applicationManager.stopApplication(root.applicationManager.get(index).appId);
622 property: "mainAppWindowOrientationAngle"
623 value: appWindowOrientationAngle
628 property: "focusedAppOrientationChangesEnabled"
629 value: orientationChangesEnabled
632 StagedFullscreenPolicy {
634 application: appDelegate.application
639 onStageAboutToBeUnloaded: fullscreenPolicy.active = false
646 //eat touch events during the right edge gesture
648 objectName: "eventEaterArea"
650 enabled: spreadDragArea.dragging
653 DirectionalDragArea {
655 objectName: "spreadDragArea"
656 direction: Direction.Leftwards
657 enabled: (spreadView.phase != 2 && root.spreadEnabled) || dragging
659 anchors { top: parent.top; right: parent.right; bottom: parent.bottom; }
660 width: root.dragAreaWidth
662 property var gesturePoints: new Array()
666 // Gesture recognized. Let's move the spreadView with the finger
667 var dragX = Math.min(touchX + width, width); // Prevent dragging rightwards
668 dragX = -dragX + spreadDragArea.width - spreadView.shift;
669 // Don't allow dragging further than the animation crossing with phase2's animation
670 var maxMovement = spreadView.width * spreadView.positionMarker4 - spreadView.shift;
672 spreadView.contentX = Math.min(dragX, maxMovement);
674 // Initial touch. Let's reset the spreadView to the starting position.
675 spreadView.phase = 0;
676 spreadView.contentX = -spreadView.shift;
679 gesturePoints.push(touchX);
684 // A potential edge-drag gesture has started. Start recording it
687 // Ok. The user released. Find out if it was a one-way movement.
688 var oneWayFlick = true;
689 var smallestX = spreadDragArea.width;
690 for (var i = 0; i < gesturePoints.length; i++) {
691 if (gesturePoints[i] >= smallestX) {
695 smallestX = gesturePoints[i];
699 if (oneWayFlick && spreadView.shiftedContentX > units.gu(2) &&
700 spreadView.shiftedContentX < spreadView.positionMarker1 * spreadView.width) {
701 // If it was a short one-way movement, do the Alt+Tab switch
702 // no matter if we didn't cross positionMarker1 yet.
703 spreadView.snapTo(1);
704 } else if (!dragging) {
705 // otherwise snap to the closest snap position we can find
706 // (might be back to start, to app 1 or to spread)
716 // NB: it does its own positioning according to the specified edge
720 spreadView.snapToSpread();
722 material: Component {
728 anchors.centerIn: parent
730 GradientStop { position: 0.0; color: Qt.rgba(0.16,0.16,0.16,0.7)}
731 GradientStop { position: 1.0; color: Qt.rgba(0.16,0.16,0.16,0)}