Unity 8
TabletStage.qml
1 /*
2  * Copyright (C) 2014-2016 Canonical, Ltd.
3  *
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.
7  *
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.
12  *
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/>.
15  */
16 
17 import QtQuick 2.4
18 import Ubuntu.Components 1.3
19 import Ubuntu.Gestures 0.1
20 import Unity.Application 0.1
21 import Utils 0.1
22 import Powerd 0.1
23 import "../Components"
24 
25 AbstractStage {
26  id: root
27  objectName: "stages"
28  anchors.fill: parent
29 
30  // Functions to be called from outside
31  function updateFocusedAppOrientation() {
32  var mainStageAppIndex = priv.indexOf(priv.mainStageAppId);
33  if (mainStageAppIndex >= 0 && mainStageAppIndex < spreadRepeater.count) {
34  spreadRepeater.itemAt(mainStageAppIndex).matchShellOrientation();
35  }
36 
37  for (var i = 0; i < spreadRepeater.count; ++i) {
38 
39  if (i === mainStageAppIndex) {
40  continue;
41  }
42 
43  var spreadDelegate = spreadRepeater.itemAt(i);
44 
45  var delta = spreadDelegate.appWindowOrientationAngle - root.shellOrientationAngle;
46  if (delta < 0) { delta += 360; }
47  delta = delta % 360;
48 
49  var supportedOrientations = spreadDelegate.application.supportedOrientations;
50  if (supportedOrientations === Qt.PrimaryOrientation) {
51  supportedOrientations = spreadDelegate.orientations.primary;
52  }
53 
54  if (delta === 180 && (supportedOrientations & spreadDelegate.shellOrientation)) {
55  spreadDelegate.matchShellOrientation();
56  }
57  }
58  }
59  function updateFocusedAppOrientationAnimated() {
60  var mainStageAppIndex = priv.indexOf(priv.mainStageAppId);
61  if (mainStageAppIndex >= 0 && mainStageAppIndex < spreadRepeater.count) {
62  spreadRepeater.itemAt(mainStageAppIndex).animateToShellOrientation();
63  }
64 
65  if (priv.sideStageAppId) {
66  var sideStageAppIndex = priv.indexOf(priv.sideStageAppId);
67  if (sideStageAppIndex >= 0 && sideStageAppIndex < spreadRepeater.count) {
68  spreadRepeater.itemAt(sideStageAppIndex).matchShellOrientation();
69  }
70  }
71  }
72 
73  function pushRightEdge(amount) {
74  if (spreadView.contentX == -spreadView.shift) {
75  edgeBarrier.push(amount);
76  }
77  }
78 
79  orientationChangesEnabled: priv.mainAppOrientationChangesEnabled
80 
81  supportedOrientations: mainApp ? mainApp.supportedOrientations
82  : (Qt.PortraitOrientation | Qt.LandscapeOrientation
83  | Qt.InvertedPortraitOrientation | Qt.InvertedLandscapeOrientation)
84 
85  onWidthChanged: {
86  spreadView.selectedIndex = -1;
87  spreadView.phase = 0;
88  spreadView.contentX = -spreadView.shift;
89  }
90 
91  onShellOrientationChanged: {
92  if (shellOrientation == Qt.PortraitOrientation || shellOrientation == Qt.InvertedPortraitOrientation) {
93  ApplicationManager.focusApplication(priv.mainStageAppId);
94  priv.sideStageAppId = "";
95  }
96  }
97 
98  onInverseProgressChanged: {
99  // This can't be a simple binding because that would be triggered after this handler
100  // while we need it active before doing the anition left/right
101  spreadView.animateX = (inverseProgress == 0)
102  if (inverseProgress == 0 && priv.oldInverseProgress > 0) {
103  // left edge drag released. Minimum distance is given by design.
104  if (priv.oldInverseProgress > units.gu(22)) {
105  ApplicationManager.requestFocusApplication("unity8-dash");
106  }
107  }
108  priv.oldInverseProgress = inverseProgress;
109  }
110 
111  onAltTabPressedChanged: {
112  if (!spreadEnabled) {
113  return;
114  }
115  if (altTabPressed) {
116  priv.highlightIndex = Math.min(spreadRepeater.count - 1, 1);
117  spreadView.snapToSpread();
118  } else {
119  for (var i = 0; i < spreadRepeater.count; i++) {
120  if (spreadRepeater.itemAt(i).zIndex === priv.highlightIndex) {
121  spreadView.snapTo(i);
122  return;
123  }
124  }
125  }
126  }
127 
128  FocusScope {
129  focus: root.altTabPressed
130 
131  Keys.onPressed: {
132  switch (event.key) {
133  case Qt.Key_Tab:
134  priv.highlightIndex = (priv.highlightIndex + 1) % spreadRepeater.count
135  break;
136  case Qt.Key_Backtab:
137  priv.highlightIndex = (priv.highlightIndex + spreadRepeater.count - 1) % spreadRepeater.count
138  break;
139  }
140  }
141  }
142 
143  QtObject {
144  id: priv
145  objectName: "stagesPriv"
146 
147  property string focusedAppId: ApplicationManager.focusedApplicationId
148  readonly property var focusedAppDelegate: {
149  var index = indexOf(focusedAppId);
150  return index >= 0 && index < spreadRepeater.count ? spreadRepeater.itemAt(index) : null
151  }
152 
153  property string oldFocusedAppId: ""
154  property bool mainAppOrientationChangesEnabled: false
155 
156  property real landscapeHeight: root.orientations.native_ == Qt.LandscapeOrientation ?
157  root.nativeHeight : root.nativeWidth
158 
159  property bool shellIsLandscape: root.shellOrientation === Qt.LandscapeOrientation
160  || root.shellOrientation === Qt.InvertedLandscapeOrientation
161 
162  property string mainStageAppId
163  property string sideStageAppId
164 
165  // For convenience, keep properties of the first two apps in the model
166  property string appId0
167  property string appId1
168 
169  property int oldInverseProgress: 0
170 
171  property int highlightIndex: 0
172 
173  onFocusedAppIdChanged: {
174  if (priv.focusedAppId.length > 0) {
175  var focusedApp = ApplicationManager.findApplication(focusedAppId);
176  if (focusedApp.stage == ApplicationInfoInterface.SideStage) {
177  priv.sideStageAppId = focusedAppId;
178  } else {
179  priv.mainStageAppId = focusedAppId;
180  root.mainApp = focusedApp;
181  }
182  }
183 
184  appId0 = ApplicationManager.count >= 1 ? ApplicationManager.get(0).appId : "";
185  appId1 = ApplicationManager.count > 1 ? ApplicationManager.get(1).appId : "";
186  }
187 
188  onFocusedAppDelegateChanged: {
189  if (focusedAppDelegate) {
190  focusedAppDelegate.focus = true;
191  }
192  }
193 
194  property bool focusedAppDelegateIsDislocated: focusedAppDelegate &&
195  (focusedAppDelegate.dragOffset !== 0 || focusedAppDelegate.xTranslateAnimating)
196  function indexOf(appId) {
197  for (var i = 0; i < ApplicationManager.count; i++) {
198  if (ApplicationManager.get(i).appId == appId) {
199  return i;
200  }
201  }
202  return -1;
203  }
204 
205  function evaluateOneWayFlick(gesturePoints) {
206  // Need to have at least 3 points to recognize it as a flick
207  if (gesturePoints.length < 3) {
208  return false;
209  }
210  // Need to have a movement of at least 2 grid units to recognize it as a flick
211  if (Math.abs(gesturePoints[gesturePoints.length - 1] - gesturePoints[0]) < units.gu(2)) {
212  return false;
213  }
214 
215  var oneWayFlick = true;
216  var smallestX = gesturePoints[0];
217  var leftWards = gesturePoints[1] < gesturePoints[0];
218  for (var i = 1; i < gesturePoints.length; i++) {
219  if ((leftWards && gesturePoints[i] >= smallestX)
220  || (!leftWards && gesturePoints[i] <= smallestX)) {
221  oneWayFlick = false;
222  break;
223  }
224  smallestX = gesturePoints[i];
225  }
226  return oneWayFlick;
227  }
228 
229  onHighlightIndexChanged: {
230  spreadView.contentX = highlightIndex * spreadView.contentWidth / (spreadRepeater.count + 2)
231  }
232  }
233 
234  Connections {
235  target: ApplicationManager
236  onFocusRequested: {
237  if (spreadView.interactive) {
238  spreadView.snapTo(priv.indexOf(appId));
239  } else {
240  ApplicationManager.focusApplication(appId);
241  }
242  }
243 
244  onApplicationAdded: {
245  if (spreadView.phase == 2) {
246  spreadView.snapTo(ApplicationManager.count - 1);
247  } else {
248  spreadView.phase = 0;
249  spreadView.contentX = -spreadView.shift;
250  ApplicationManager.focusApplication(appId);
251  }
252  }
253 
254  onApplicationRemoved: {
255  if (priv.mainStageAppId == appId) {
256  ApplicationManager.focusApplication("unity8-dash")
257  }
258  if (priv.sideStageAppId == appId) {
259  priv.sideStageAppId = "";
260  }
261 
262  if (ApplicationManager.count == 0) {
263  spreadView.phase = 0;
264  spreadView.contentX = -spreadView.shift;
265  } else if (spreadView.closingIndex == -1) {
266  // Unless we're closing the app ourselves in the spread,
267  // lets make sure the spread doesn't mess up by the changing app list.
268  spreadView.phase = 0;
269  spreadView.contentX = -spreadView.shift;
270  ApplicationManager.focusApplication(ApplicationManager.get(0).appId);
271  }
272  }
273  }
274 
275  Flickable {
276  id: spreadView
277  objectName: "spreadView"
278  anchors.fill: parent
279  interactive: (spreadDragArea.dragging || phase > 1) && draggedDelegateCount === 0
280  contentWidth: spreadRow.width - shift
281  contentX: -shift
282 
283  property int tileDistance: units.gu(20)
284  property int sideStageWidth: units.gu(40)
285  property bool sideStageVisible: priv.sideStageAppId
286 
287  // This indicates when the spreadView is active. That means, all the animations
288  // are activated and tiles need to line up for the spread.
289  readonly property bool active: shiftedContentX > 0 || spreadDragArea.dragging
290 
291  // The flickable needs to fill the screen in order to get touch events all over.
292  // However, we don't want to the user to be able to scroll back all the way. For
293  // that, the beginning of the gesture starts with a negative value for contentX
294  // so the flickable wants to pull it into the view already. "shift" tunes the
295  // distance where to "lock" the content.
296  readonly property real shift: width / 2
297  readonly property real shiftedContentX: contentX + shift
298 
299  // Phase of the animation:
300  // 0: Starting from right edge, a new app (index 1) comes in from the right
301  // 1: The app has reached the first snap position.
302  // 2: The list is dragged further and snaps into the spread view when entering phase 2
303  property int phase
304 
305  readonly property int phase0Width: sideStageWidth
306  readonly property int phase1Width: sideStageWidth
307 
308  // Those markers mark the various positions in the spread (ratio to screen width from right to left):
309  // 0 - 1: following finger, snap back to the beginning on release
310  readonly property real positionMarker1: 0.2
311  // 1 - 2: curved snapping movement, snap to nextInStack on release
312  readonly property real positionMarker2: sideStageWidth / spreadView.width
313  // 2 - 3: movement follows finger, snaps to phase 2 (full spread) on release
314  readonly property real positionMarker3: 0.6
315  // passing 3, we detach movement from the finger and snap to phase 2 (full spread)
316  readonly property real positionMarker4: 0.8
317 
318  readonly property int startSnapPosition: phase0Width * 0.5
319  readonly property int endSnapPosition: phase0Width * 0.75
320  readonly property real snapPosition: 0.75
321 
322  property int selectedIndex: -1
323  property int draggedDelegateCount: 0
324  property int closingIndex: -1
325 
326  // FIXME: Workaround Flickable's not keepping its contentX still when resized
327  onContentXChanged: { forceItToRemainStillIfBeingResized(); }
328  onShiftChanged: { forceItToRemainStillIfBeingResized(); }
329  function forceItToRemainStillIfBeingResized() {
330  if (root.beingResized && contentX != -shift) {
331  contentX = -shift;
332  }
333  }
334 
335  property bool animateX: true
336  property bool beingResized: root.beingResized
337  onBeingResizedChanged: {
338  if (beingResized) {
339  // Brace yourselves for impact!
340  selectedIndex = -1;
341  phase = 0;
342  contentX = -shift;
343  }
344  }
345 
346  property bool sideStageDragging: sideStageDragHandle.dragging
347  property real sideStageDragProgress: sideStageDragHandle.progress
348 
349  onSideStageDragProgressChanged: {
350  if (sideStageDragProgress == 1) {
351  ApplicationManager.focusApplication(priv.mainStageAppId);
352  priv.sideStageAppId = "";
353  }
354  }
355 
356  // In case the ApplicationManager already holds an app when starting up we're missing animations
357  // Make sure we end up in the same state
358  Component.onCompleted: {
359  spreadView.contentX = -spreadView.shift
360  }
361 
362  property int nextInStack: {
363  switch (state) {
364  case "main":
365  if (ApplicationManager.count > 1) {
366  return 1;
367  }
368  return -1;
369  case "mainAndOverlay":
370  if (ApplicationManager.count <= 2) {
371  return -1;
372  }
373  if (priv.appId0 == priv.mainStageAppId || priv.appId0 == priv.sideStageAppId) {
374  if (priv.appId1 == priv.mainStageAppId || priv.appId1 == priv.sideStageAppId) {
375  return 2;
376  }
377  return 1;
378  }
379  return 0;
380  case "overlay":
381  return 1;
382  }
383  return -1;
384  }
385  property int nextZInStack: indexToZIndex(nextInStack)
386 
387  states: [
388  State {
389  name: "empty"
390  },
391  State {
392  name: "main"
393  },
394  State { // Side Stage only in overlay mode
395  name: "overlay"
396  },
397  State { // Main Stage and Side Stage in overlay mode
398  name: "mainAndOverlay"
399  },
400  State { // Main Stage and Side Stage in split mode
401  name: "mainAndSplit"
402  }
403  ]
404  state: {
405  if (priv.mainStageAppId && !priv.sideStageAppId) {
406  return "main";
407  }
408  if (!priv.mainStageAppId && priv.sideStageAppId) {
409  return "overlay";
410  }
411  if (priv.mainStageAppId && priv.sideStageAppId) {
412  return "mainAndOverlay";
413  }
414  return "empty";
415  }
416 
417  onShiftedContentXChanged: {
418  if (root.beingResized) {
419  // Flickabe.contentX wiggles during resizes. Don't react to it.
420  return;
421  }
422  if (spreadView.phase == 0 && spreadView.shiftedContentX > spreadView.width * spreadView.positionMarker2) {
423  spreadView.phase = 1;
424  } else if (spreadView.phase == 1 && spreadView.shiftedContentX > spreadView.width * spreadView.positionMarker4) {
425  spreadView.phase = 2;
426  } else if (spreadView.phase == 1 && spreadView.shiftedContentX < spreadView.width * spreadView.positionMarker2) {
427  spreadView.phase = 0;
428  }
429  }
430 
431  function snap() {
432  if (shiftedContentX < phase0Width) {
433  snapAnimation.targetContentX = -shift;
434  snapAnimation.start();
435  } else if (shiftedContentX < phase1Width) {
436  snapTo(1);
437  } else {
438  snapToSpread();
439  }
440  }
441 
442  function snapToSpread() {
443  // Add 1 pixel to make sure we definitely hit positionMarker4 even with rounding errors of the animation.
444  snapAnimation.targetContentX = (spreadView.width * spreadView.positionMarker4) + 1 - shift;
445  snapAnimation.start();
446  }
447 
448  function snapTo(index) {
449  spreadView.selectedIndex = index;
450  snapAnimation.targetContentX = -shift;
451  snapAnimation.start();
452  }
453 
454  // We need to shuffle z ordering a bit in order to keep side stage apps above main stage apps.
455  // We don't want to really reorder them in the model because that allows us to keep track
456  // of the last focused order.
457  function indexToZIndex(index) {
458  var app = ApplicationManager.get(index);
459  if (!app) {
460  return index;
461  }
462 
463  var active = app.appId == priv.mainStageAppId || app.appId == priv.sideStageAppId;
464  if (active && app.stage == ApplicationInfoInterface.MainStage) {
465  // if this app is active, and its the MainStage, always put it to index 0
466  return 0;
467  }
468  if (active && app.stage == ApplicationInfoInterface.SideStage) {
469  if (!priv.mainStageAppId) {
470  // Only have SS apps running. Put the active one at 0
471  return 0;
472  }
473 
474  // Precondition now: There's an active MS app and this is SS app:
475  if (spreadView.nextInStack >= 0 && ApplicationManager.get(spreadView.nextInStack).stage == ApplicationInfoInterface.MainStage) {
476  // If the next app coming from the right is a MS app, we need to elevate this SS ap above it.
477  // Put it to at least level 2, or higher if there's more apps coming in before this one.
478  return Math.max(index, 2);
479  } else {
480  // 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.
481  return 1;
482  }
483  }
484  if (index <= 2 && app.stage == ApplicationInfoInterface.MainStage && priv.sideStageAppId) {
485  // Ok, this is an inactive MS app. If there's an active SS app around, we need to place this one
486  // in between the active MS app and the active SS app, so that it comes in from there when dragging from the right.
487  // If there's now active SS app, just leave it where it is.
488  return priv.indexOf(priv.sideStageAppId) < index ? index - 1 : index;
489  }
490  if (index == spreadView.nextInStack && app.stage == ApplicationInfoInterface.SideStage) {
491  // This is a SS app and the next one to come in from the right:
492  if (priv.sideStageAppId && priv.mainStageAppId) {
493  // If there's both, an active MS and an active SS app, put this one right on top of that
494  return 2;
495  }
496  // Or if there's only one other active app, put it on top of that.
497  // The case that there isn't any other active app is already handled above.
498  return 1;
499  }
500  if (index == 2 && spreadView.nextInStack == 1 && priv.sideStageAppId) {
501  // If its index 2 but not the next one to come in, it means
502  // we've pulled another one down to index 2. Move this one up to 2 instead.
503  return 3;
504  }
505  // don't touch all others... (mostly index > 3 + simple cases where the above doesn't shuffle much)
506  return index;
507  }
508 
509  SequentialAnimation {
510  id: snapAnimation
511  property int targetContentX: -spreadView.shift
512 
513  UbuntuNumberAnimation {
514  target: spreadView
515  property: "contentX"
516  to: snapAnimation.targetContentX
517  duration: UbuntuAnimation.FastDuration
518  }
519 
520  ScriptAction {
521  script: {
522  if (spreadView.selectedIndex >= 0) {
523  var newIndex = spreadView.selectedIndex;
524  spreadView.selectedIndex = -1;
525  ApplicationManager.focusApplication(ApplicationManager.get(newIndex).appId);
526  spreadView.phase = 0;
527  spreadView.contentX = -spreadView.shift;
528  }
529  }
530  }
531  }
532 
533  Behavior on contentX {
534  enabled: root.altTabPressed
535  UbuntuNumberAnimation {}
536  }
537 
538  MouseArea {
539  id: spreadRow
540  x: spreadView.contentX
541  width: spreadView.width + Math.max(spreadView.width, ApplicationManager.count * spreadView.tileDistance)
542  height: root.height
543 
544  onClicked: {
545  spreadView.snapTo(0);
546  }
547 
548  Rectangle {
549  id: sideStageBackground
550  color: "black"
551  width: spreadView.sideStageWidth * (1 - sideStageDragHandle.progress)
552  height: priv.landscapeHeight
553  x: spreadView.width - width
554  z: spreadView.indexToZIndex(priv.indexOf(priv.sideStageAppId))
555  opacity: spreadView.phase == 0 ? 1 : 0
556  Behavior on opacity { UbuntuNumberAnimation {} }
557  }
558 
559  Item {
560  id: sideStageDragHandle
561  anchors.right: sideStageBackground.left
562  anchors.top: sideStageBackground.top
563  width: units.gu(2)
564  height: priv.landscapeHeight
565  z: sideStageBackground.z
566  opacity: spreadView.phase <= 0 && spreadView.sideStageVisible ? 1 : 0
567  property real progress: 0
568  property bool dragging: false
569 
570  Behavior on opacity { UbuntuNumberAnimation {} }
571 
572  Connections {
573  target: spreadView
574  onSideStageVisibleChanged: {
575  if (spreadView.sideStageVisible) {
576  sideStageDragHandle.progress = 0;
577  }
578  }
579  }
580 
581  Image {
582  anchors.centerIn: parent
583  width: sideStageDragHandleMouseArea.pressed ? parent.width * 2 : parent.width
584  height: parent.height
585  source: "graphics/sidestage_handle@20.png"
586  Behavior on width { UbuntuNumberAnimation {} }
587  }
588 
589  MouseArea {
590  id: sideStageDragHandleMouseArea
591  anchors.fill: parent
592  enabled: spreadView.shiftedContentX == 0
593  property int startX
594  property var gesturePoints: new Array()
595  property real totalDiff
596 
597  onPressed: {
598  gesturePoints = [];
599  startX = mouseX;
600  totalDiff = 0.0;
601  sideStageDragHandle.progress = 0;
602  sideStageDragHandle.dragging = true;
603  }
604  onMouseXChanged: {
605  totalDiff += mouseX - startX;
606  if (priv.mainStageAppId) {
607  sideStageDragHandle.progress = Math.max(0, totalDiff / spreadView.sideStageWidth);
608  }
609  gesturePoints.push(mouseX);
610  }
611  onReleased: {
612  if (priv.mainStageAppId) {
613  var oneWayFlick = priv.evaluateOneWayFlick(gesturePoints);
614  sideStageDragSnapAnimation.to = sideStageDragHandle.progress > 0.5 || oneWayFlick ? 1 : 0;
615  sideStageDragSnapAnimation.start();
616  } else {
617  sideStageDragHandle.dragging = false;
618  }
619  }
620  }
621  UbuntuNumberAnimation {
622  id: sideStageDragSnapAnimation
623  target: sideStageDragHandle
624  property: "progress"
625 
626  onRunningChanged: {
627  if (!running) {
628  sideStageDragHandle.dragging = false;
629  }
630  }
631  }
632  }
633 
634  Repeater {
635  id: spreadRepeater
636  objectName: "spreadRepeater"
637  model: ApplicationManager
638 
639  delegate: TransformedTabletSpreadDelegate {
640  id: spreadTile
641  objectName: model.appId ? "tabletSpreadDelegate_" + model.appId
642  : "tabletSpreadDelegate_null";
643  width: {
644  if (wantsMainStage) {
645  return spreadView.width;
646  } else {
647  return spreadView.sideStageWidth;
648  }
649  }
650  height: {
651  if (wantsMainStage) {
652  return spreadView.height;
653  } else {
654  return priv.landscapeHeight;
655  }
656  }
657  active: model.appId == priv.mainStageAppId || model.appId == priv.sideStageAppId
658  zIndex: spreadView.indexToZIndex(index)
659  selected: spreadView.selectedIndex == index
660  otherSelected: spreadView.selectedIndex >= 0 && !selected
661  isInSideStage: priv.sideStageAppId == model.appId
662  interactive: !spreadView.interactive && spreadView.phase === 0 && root.interactive
663  swipeToCloseEnabled: spreadView.interactive && !snapAnimation.running
664  maximizedAppTopMargin: root.maximizedAppTopMargin
665  dragOffset: !isDash && model.appId == priv.mainStageAppId && root.inverseProgress > 0 && spreadView.phase === 0 ? root.inverseProgress : 0
666  application: ApplicationManager.get(index)
667  closeable: !isDash
668  highlightShown: root.altTabPressed && priv.highlightIndex == zIndex
669 
670  readonly property bool wantsMainStage: model.stage == ApplicationInfoInterface.MainStage
671 
672  readonly property bool isDash: model.appId == "unity8-dash"
673 
674  Binding {
675  target: spreadTile.application
676  property: "exemptFromLifecycle"
677  value: !model.isTouchApp || isExemptFromLifecycle(model.appId)
678  }
679 
680  Binding {
681  target: spreadTile.application
682  property: "requestedState"
683  value: (isDash && root.keepDashRunning)
684  || (!root.suspended && (model.appId == priv.mainStageAppId
685  || model.appId == priv.sideStageAppId))
686  ? ApplicationInfoInterface.RequestedRunning
687  : ApplicationInfoInterface.RequestedSuspended
688  }
689 
690  // FIXME: A regular binding doesn't update any more after closing an app.
691  // Using a Binding for now.
692  Binding {
693  target: spreadTile
694  property: "z"
695  value: (!spreadView.active && isDash && !active) ? -1 : spreadTile.zIndex
696  }
697  x: spreadView.width
698 
699  property real behavioredZIndex: zIndex
700  Behavior on behavioredZIndex {
701  enabled: spreadView.closingIndex >= 0
702  UbuntuNumberAnimation {}
703  }
704 
705  // This is required because none of the bindings are triggered in some cases:
706  // When an app is closed, it might happen that ApplicationManager.get(nextInStack)
707  // returns a different app even though the nextInStackIndex and all the related
708  // bindings (index, mainStageApp, sideStageApp, etc) don't change. Let's force a
709  // binding update in that case.
710  Connections {
711  target: ApplicationManager
712  onApplicationRemoved: spreadTile.z = Qt.binding(function() {
713  return spreadView.indexToZIndex(index);
714  })
715  }
716 
717  progress: {
718  var tileProgress = (spreadView.shiftedContentX - behavioredZIndex * spreadView.tileDistance) / spreadView.width;
719  // Some tiles (nextInStack, active) need to move directly from the beginning, normalize progress to immediately start at 0
720  if ((index == spreadView.nextInStack && spreadView.phase < 2) || (active && spreadView.phase < 1)) {
721  tileProgress += behavioredZIndex * spreadView.tileDistance / spreadView.width;
722  }
723  return tileProgress;
724  }
725 
726  // TODO: Hiding tile when progress is such that it will be off screen.
727  property bool occluded: {
728  if (spreadView.active) return false;
729  else if (spreadTile.active) return false;
730  else if (xTranslateAnimating) return false;
731  else if (z <= 1 && priv.focusedAppDelegateIsDislocated) return false;
732  return true;
733  }
734 
735  visible: Powerd.status == Powerd.On &&
736  !greeter.fullyShown &&
737  !occluded
738 
739  animatedProgress: {
740  if (spreadView.phase == 0 && (spreadTile.active || spreadView.nextInStack == index)) {
741  if (progress < spreadView.positionMarker1) {
742  return progress;
743  } else if (progress < spreadView.positionMarker1 + snappingCurve.period) {
744  return spreadView.positionMarker1 + snappingCurve.value * 3;
745  } else {
746  return spreadView.positionMarker2;
747  }
748  }
749  return progress;
750  }
751 
752  shellOrientationAngle: wantsMainStage ? root.shellOrientationAngle : 0
753  shellOrientation: wantsMainStage ? root.shellOrientation : Qt.PortraitOrientation
754  orientations: Orientations {
755  primary: spreadTile.wantsMainStage ? root.orientations.primary : Qt.PortraitOrientation
756  native_: spreadTile.wantsMainStage ? root.orientations.native_ : Qt.PortraitOrientation
757  portrait: root.orientations.portrait
758  invertedPortrait: root.orientations.invertedPortrait
759  landscape: root.orientations.landscape
760  invertedLandscape: root.orientations.invertedLandscape
761  }
762 
763  onClicked: {
764  if (spreadView.phase == 2) {
765  spreadView.snapTo(index);
766  }
767  }
768 
769  onDraggedChanged: {
770  if (dragged) {
771  spreadView.draggedDelegateCount++;
772  } else {
773  spreadView.draggedDelegateCount--;
774  }
775  }
776 
777  onClosed: {
778  spreadView.closingIndex = index;
779  ApplicationManager.stopApplication(ApplicationManager.get(index).appId);
780  }
781 
782  Binding {
783  target: root
784  when: model.appId == priv.mainStageAppId
785  property: "mainAppWindowOrientationAngle"
786  value: appWindowOrientationAngle
787  }
788  Binding {
789  target: priv
790  when: model.appId == priv.mainStageAppId
791  property: "mainAppOrientationChangesEnabled"
792  value: orientationChangesEnabled
793  }
794 
795  EasingCurve {
796  id: snappingCurve
797  type: EasingCurve.Linear
798  period: (spreadView.positionMarker2 - spreadView.positionMarker1) / 3
799  progress: spreadTile.progress - spreadView.positionMarker1
800  }
801  }
802  }
803  }
804  }
805 
806  //eat touch events during the right edge gesture
807  MouseArea {
808  anchors.fill: parent
809  enabled: spreadDragArea.dragging
810  }
811 
812  DirectionalDragArea {
813  id: spreadDragArea
814  objectName: "spreadDragArea"
815  anchors { top: parent.top; right: parent.right; bottom: parent.bottom }
816  width: root.dragAreaWidth
817  direction: Direction.Leftwards
818  enabled: (spreadView.phase != 2 && root.spreadEnabled) || dragging
819 
820  property var gesturePoints: new Array()
821 
822  onTouchXChanged: {
823  if (!dragging) {
824  spreadView.phase = 0;
825  spreadView.contentX = -spreadView.shift;
826  }
827 
828  if (dragging) {
829  var dragX = -touchX + spreadDragArea.width - spreadView.shift;
830  var maxDrag = spreadView.width * spreadView.positionMarker4 - spreadView.shift;
831  spreadView.contentX = Math.min(dragX, maxDrag);
832  }
833  gesturePoints.push(touchX);
834  }
835 
836  onDraggingChanged: {
837  if (dragging) {
838  // Gesture recognized. Start recording this gesture
839  gesturePoints = [];
840  } else {
841  // Ok. The user released. Find out if it was a one-way movement.
842  var oneWayFlick = priv.evaluateOneWayFlick(gesturePoints);
843  gesturePoints = [];
844 
845  if (oneWayFlick && spreadView.shiftedContentX < spreadView.positionMarker1 * spreadView.width) {
846  // If it was a short one-way movement, do the Alt+Tab switch
847  // no matter if we didn't cross positionMarker1 yet.
848  spreadView.snapTo(spreadView.nextInStack);
849  } else {
850  if (spreadView.shiftedContentX < spreadView.width * spreadView.positionMarker1) {
851  spreadView.snap();
852  } else if (spreadView.shiftedContentX < spreadView.width * spreadView.positionMarker2) {
853  spreadView.snapTo(spreadView.nextInStack);
854  } else {
855  // otherwise snap to the closest snap position we can find
856  // (might be back to start, to app 1 or to spread)
857  spreadView.snap();
858  }
859  }
860  }
861  }
862  }
863 
864  EdgeBarrier {
865  id: edgeBarrier
866 
867  // NB: it does its own positioning according to the specified edge
868  edge: Qt.RightEdge
869 
870  onPassed: {
871  spreadView.snapToSpread();
872  }
873  material: Component {
874  Item {
875  Rectangle {
876  width: parent.height
877  height: parent.width
878  rotation: 90
879  anchors.centerIn: parent
880  gradient: Gradient {
881  GradientStop { position: 0.0; color: Qt.rgba(0.16,0.16,0.16,0.7)}
882  GradientStop { position: 1.0; color: Qt.rgba(0.16,0.16,0.16,0)}
883  }
884  }
885  }
886  }
887  }
888 }