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  // <tutorial-hacks> The Tutorial looks into our implementation details
31  property alias sideStageVisible: spreadView.sideStageVisible
32  property alias sideStageWidth: spreadView.sideStageWidth
33  // The stage the currently focused surface is in
34  property int stageFocusedSurface: priv.focusedAppDelegate ? priv.focusedAppDelegate.stage : ApplicationInfoInterface.MainStage
35  // </tutorial-hacks>
36 
37  paintBackground: spreadView.shiftedContentX !== 0
38 
39  // Functions to be called from outside
40  function updateFocusedAppOrientation() {
41  var mainStageIndex = root.topLevelSurfaceList.indexForId(priv.mainStageItemId);
42 
43  if (priv.mainStageItemId && mainStageIndex >= 0 && mainStageIndex < spreadRepeater.count) {
44  spreadRepeater.itemAt(mainStageIndex).matchShellOrientation();
45  }
46 
47  for (var i = 0; i < spreadRepeater.count; ++i) {
48 
49  if (i === mainStageIndex) {
50  continue;
51  }
52 
53  var spreadDelegate = spreadRepeater.itemAt(i);
54 
55  var delta = spreadDelegate.appWindowOrientationAngle - root.shellOrientationAngle;
56  if (delta < 0) { delta += 360; }
57  delta = delta % 360;
58 
59  var supportedOrientations = spreadDelegate.supportedOrientations;
60  if (supportedOrientations === Qt.PrimaryOrientation) {
61  supportedOrientations = spreadDelegate.orientations.primary;
62  }
63 
64  if (delta === 180 && (supportedOrientations & spreadDelegate.shellOrientation)) {
65  spreadDelegate.matchShellOrientation();
66  }
67  }
68  }
69  function updateFocusedAppOrientationAnimated() {
70  var mainStageIndex = root.topLevelSurfaceList.indexForId(priv.mainStageItemId);
71  if (priv.mainStageItemId && mainStageIndex >= 0 && mainStageIndex < spreadRepeater.count) {
72  spreadRepeater.itemAt(mainStageIndex).animateToShellOrientation();
73  }
74 
75  var sideStageIndex = root.topLevelSurfaceList.indexForId(priv.sideStageItemId);
76  if (sideStageIndex >= 0 && sideStageIndex < spreadRepeater.count) {
77  spreadRepeater.itemAt(sideStageIndex).matchShellOrientation();
78  }
79  }
80 
81  function pushRightEdge(amount) {
82  if (spreadView.contentX == -spreadView.shift) {
83  edgeBarrier.push(amount);
84  }
85  }
86 
87  orientationChangesEnabled: priv.mainAppOrientationChangesEnabled
88 
89  mainApp: {
90  if (priv.mainStageItemId > 0) {
91  var index = root.topLevelSurfaceList.indexForId(priv.mainStageItemId);
92  return root.topLevelSurfaceList.applicationAt(index);
93  } else {
94  return null;
95  }
96  }
97 
98  supportedOrientations: {
99  if (mainApp) {
100  var orientations = mainApp.supportedOrientations;
101  orientations |= Qt.LandscapeOrientation | Qt.InvertedLandscapeOrientation;
102  if (priv.sideStageItemId && !spreadView.surfaceDragging) {
103  // If we have a sidestage app, support Portrait orientation
104  // so that it will switch the sidestage app to mainstage on rotate
105  orientations |= Qt.PortraitOrientation|Qt.InvertedPortraitOrientation;
106  }
107  return orientations;
108  } else {
109  // we just don't care
110  return Qt.PortraitOrientation |
111  Qt.LandscapeOrientation |
112  Qt.InvertedPortraitOrientation |
113  Qt.InvertedLandscapeOrientation;
114  }
115  }
116 
117  // How far left the stage has been dragged, used externally by tutorial code
118  dragProgress: spreadRepeater.count > 0 ? spreadRepeater.itemAt(0).animatedProgress : 0
119 
120  onWidthChanged: {
121  spreadView.selectedIndex = -1;
122  spreadView.phase = 0;
123  spreadView.contentX = -spreadView.shift;
124  }
125 
126  onInverseProgressChanged: {
127  // This can't be a simple binding because that would be triggered after this handler
128  // while we need it active before doing the anition left/right
129  spreadView.animateX = (inverseProgress == 0)
130  if (inverseProgress == 0 && priv.oldInverseProgress > 0) {
131  // left edge drag released. Minimum distance is given by design.
132  if (priv.oldInverseProgress > units.gu(22)) {
133  root.applicationManager.requestFocusApplication("unity8-dash");
134  }
135  }
136  priv.oldInverseProgress = inverseProgress;
137  }
138 
139  onAltTabPressedChanged: {
140  if (!spreadEnabled) {
141  return;
142  }
143  if (altTabPressed) {
144  priv.highlightIndex = Math.min(spreadRepeater.count - 1, 1);
145  spreadView.snapToSpread();
146  } else {
147  for (var i = 0; i < spreadRepeater.count; i++) {
148  if (spreadRepeater.itemAt(i).zIndex === priv.highlightIndex) {
149  spreadView.snapTo(i);
150  return;
151  }
152  }
153  }
154  }
155 
156  FocusScope {
157  focus: root.altTabPressed
158 
159  Keys.onPressed: {
160  switch (event.key) {
161  case Qt.Key_Tab:
162  priv.highlightIndex = (priv.highlightIndex + 1) % spreadRepeater.count
163  break;
164  case Qt.Key_Backtab:
165  priv.highlightIndex = (priv.highlightIndex + spreadRepeater.count - 1) % spreadRepeater.count
166  break;
167  }
168  }
169  }
170 
171  Connections {
172  target: root.topLevelSurfaceList
173  onListChanged: priv.updateMainAndSideStageIndexes()
174  }
175 
176  QtObject {
177  id: priv
178  objectName: "stagesPriv"
179 
180  function updateMainAndSideStageIndexes() {
181  var choseMainStage = false;
182  var choseSideStage = false;
183 
184  if (!root.topLevelSurfaceList)
185  return;
186 
187  for (var i = 0; i < spreadRepeater.count && (!choseMainStage || !choseSideStage); ++i) {
188  var spreadDelegate = spreadRepeater.itemAt(i);
189  if (sideStage.shown && spreadDelegate.stage == ApplicationInfoInterface.SideStage
190  && !choseSideStage) {
191  priv.sideStageDelegate = spreadDelegate
192  priv.sideStageItemId = root.topLevelSurfaceList.idAt(i);
193  priv.sideStageAppId = root.topLevelSurfaceList.applicationAt(i).appId;
194  choseSideStage = true;
195  } else if (!choseMainStage && spreadDelegate.stage == ApplicationInfoInterface.MainStage) {
196  priv.mainStageDelegate = spreadDelegate;
197  priv.mainStageItemId = root.topLevelSurfaceList.idAt(i);
198  priv.mainStageAppId = root.topLevelSurfaceList.applicationAt(i).appId;
199  choseMainStage = true;
200  }
201  }
202  if (!choseMainStage) {
203  priv.mainStageDelegate = null;
204  priv.mainStageItemId = 0;
205  priv.mainStageAppId = "";
206  }
207  if (!choseSideStage) {
208  priv.sideStageDelegate = null;
209  priv.sideStageItemId = 0;
210  priv.sideStageAppId = "";
211  }
212  }
213 
214  property var focusedAppDelegate: null
215 
216  property bool mainAppOrientationChangesEnabled: false
217 
218  property real landscapeHeight: root.orientations.native_ == Qt.LandscapeOrientation ?
219  root.nativeHeight : root.nativeWidth
220 
221  property bool shellIsLandscape: root.shellOrientation === Qt.LandscapeOrientation
222  || root.shellOrientation === Qt.InvertedLandscapeOrientation
223 
224  property var mainStageDelegate: null
225  property var sideStageDelegate: null
226 
227  property int mainStageItemId: 0
228  property int sideStageItemId: 0
229 
230  property string mainStageAppId: ""
231  property string sideStageAppId: ""
232 
233  property int oldInverseProgress: 0
234 
235  property int highlightIndex: 0
236 
237  property bool focusedAppDelegateIsDislocated: focusedAppDelegate &&
238  (focusedAppDelegate.dragOffset !== 0 || focusedAppDelegate.xTranslateAnimating)
239  function evaluateOneWayFlick(gesturePoints) {
240  // Need to have at least 3 points to recognize it as a flick
241  if (gesturePoints.length < 3) {
242  return false;
243  }
244  // Need to have a movement of at least 2 grid units to recognize it as a flick
245  if (Math.abs(gesturePoints[gesturePoints.length - 1] - gesturePoints[0]) < units.gu(2)) {
246  return false;
247  }
248 
249  var oneWayFlick = true;
250  var smallestX = gesturePoints[0];
251  var leftWards = gesturePoints[1] < gesturePoints[0];
252  for (var i = 1; i < gesturePoints.length; i++) {
253  if ((leftWards && gesturePoints[i] >= smallestX)
254  || (!leftWards && gesturePoints[i] <= smallestX)) {
255  oneWayFlick = false;
256  break;
257  }
258  smallestX = gesturePoints[i];
259  }
260  return oneWayFlick;
261  }
262 
263  onHighlightIndexChanged: {
264  spreadView.contentX = highlightIndex * spreadView.contentWidth / (spreadRepeater.count + 2)
265  }
266 
267  readonly property bool sideStageEnabled: root.shellOrientation == Qt.LandscapeOrientation ||
268  root.shellOrientation == Qt.InvertedLandscapeOrientation
269  }
270 
271  Instantiator {
272  model: root.applicationManager
273  delegate: QtObject {
274  property var stateBinding: Binding {
275  readonly property bool isDash: model.application ? model.application.appId == "unity8-dash" : false
276  target: model.application
277  property: "requestedState"
278 
279  // NB: the first application clause is just to ensure we never get warnings for trying to access
280  // members of a null variable.
281  value: model.application &&
282  (
283  (isDash && root.keepDashRunning)
284  || (!root.suspended && (model.application.appId === priv.mainStageAppId
285  || model.application.appId === priv.sideStageAppId))
286  )
287  ? ApplicationInfoInterface.RequestedRunning
288  : ApplicationInfoInterface.RequestedSuspended
289  }
290 
291  property var lifecycleBinding: Binding {
292  target: model.application
293  property: "exemptFromLifecycle"
294  value: model.application
295  ? (!model.application.isTouchApp || isExemptFromLifecycle(model.application.appId))
296  : false
297  }
298  }
299  }
300 
301  Binding {
302  target: MirFocusController
303  property: "focusedSurface"
304  value: priv.focusedAppDelegate ? priv.focusedAppDelegate.surface : null
305  when: root.parent && !spreadRepeater.startingUp
306  }
307 
308  Flickable {
309  id: spreadView
310  objectName: "spreadView"
311  anchors.fill: parent
312  interactive: (spreadDragArea.dragging || phase > 1) && draggedDelegateCount === 0
313  contentWidth: spreadRow.width - shift
314  contentX: -shift
315 
316  property int tileDistance: units.gu(20)
317 
318  // This indicates when the spreadView is active. That means, all the animations
319  // are activated and tiles need to line up for the spread.
320  readonly property bool active: shiftedContentX > 0 || spreadDragArea.dragging
321 
322  // The flickable needs to fill the screen in order to get touch events all over.
323  // However, we don't want to the user to be able to scroll back all the way. For
324  // that, the beginning of the gesture starts with a negative value for contentX
325  // so the flickable wants to pull it into the view already. "shift" tunes the
326  // distance where to "lock" the content.
327  readonly property real shift: width / 2
328  readonly property real shiftedContentX: contentX + shift
329 
330  // Phase of the animation:
331  // 0: Starting from right edge, a new app (index 1) comes in from the right
332  // 1: The app has reached the first snap position.
333  // 2: The list is dragged further and snaps into the spread view when entering phase 2
334  property int phase
335 
336  readonly property int phase0Width: sideStageWidth
337  readonly property int phase1Width: sideStageWidth
338 
339  // Those markers mark the various positions in the spread (ratio to screen width from right to left):
340  // 0 - 1: following finger, snap back to the beginning on release
341  readonly property real positionMarker1: 0.2
342  // 1 - 2: curved snapping movement, snap to nextInStack on release
343  readonly property real positionMarker2: sideStageWidth / spreadView.width
344  // 2 - 3: movement follows finger, snaps to phase 2 (full spread) on release
345  readonly property real positionMarker3: 0.6
346  // passing 3, we detach movement from the finger and snap to phase 2 (full spread)
347  readonly property real positionMarker4: 0.8
348 
349  readonly property int startSnapPosition: phase0Width * 0.5
350  readonly property int endSnapPosition: phase0Width * 0.75
351  readonly property real snapPosition: 0.75
352 
353  property int selectedIndex: -1
354  property int draggedDelegateCount: 0
355  property int closingIndex: -1
356  property var selectedDelegate: selectedIndex !== -1 ? spreadRepeater.itemAt(selectedIndex) : null
357 
358  // <FIXME-contentX> Workaround Flickable's behavior of bringing contentX back between valid boundaries
359  // when resized. The proper way to fix this is refactoring PhoneStage so that it doesn't
360  // rely on having Flickable.contentX keeping an out-of-bounds value when it's set programatically
361  // (as opposed to having contentX reaching an out-of-bounds value through dragging, which will trigger
362  // the Flickable.boundsBehavior upon release).
363  onContentXChanged: {
364  if (!undoContentXReset()) {
365  forceItToRemainStillIfBeingResized();
366  }
367  }
368  onShiftChanged: { forceItToRemainStillIfBeingResized(); }
369  function forceItToRemainStillIfBeingResized() {
370  if (root.beingResized && contentX != -spreadView.shift) {
371  contentX = -spreadView.shift;
372  }
373  }
374  function undoContentXReset() {
375  if (contentWidth <= 0) {
376  contentWidthOnLastContentXChange = contentWidth;
377  lastContentX = contentX;
378  return false;
379  }
380 
381  if (contentWidth != contentWidthOnLastContentXChange
382  && lastContentX == -shift && contentX == 0) {
383  // Flickable is resetting contentX because contentWidth has changed. Undo it.
384  contentX = -shift;
385  return true;
386  }
387 
388  contentWidthOnLastContentXChange = contentWidth;
389  lastContentX = contentX;
390  return false;
391  }
392  property real contentWidthOnLastContentXChange: -1
393  property real lastContentX: 0
394  // </FIXME-contentX>
395 
396  property bool animateX: true
397  property bool beingResized: root.beingResized
398  onBeingResizedChanged: {
399  if (beingResized) {
400  // Brace yourselves for impact!
401  selectedIndex = -1;
402  phase = 0;
403  contentX = -shift;
404  }
405  }
406 
407  property real sideStageWidth: units.gu(40)
408 
409  property bool surfaceDragging: triGestureArea.recognisedDrag
410 
411  readonly property bool sideStageVisible: priv.sideStageItemId != 0
412 
413  // In case applicationManager already holds an app when starting up we're missing animations
414  // Make sure we end up in the same state
415  Component.onCompleted: {
416  spreadView.contentX = -spreadView.shift
417  }
418 
419  property int nextInStack: {
420  var mainStageIndex = priv.mainStageDelegate ? priv.mainStageDelegate.index : -1;
421  var sideStageIndex = priv.sideStageDelegate ? priv.sideStageDelegate.index : -1;
422  switch (state) {
423  case "main":
424  if (root.topLevelSurfaceList.count > 1) {
425  return 1;
426  }
427  return -1;
428  case "mainAndOverlay":
429  if (root.topLevelSurfaceList.count <= 2) {
430  return -1;
431  }
432  if (mainStageIndex == 0 || sideStageIndex == 0) {
433  if (mainStageIndex == 1 || sideStageIndex == 1) {
434  return 2;
435  }
436  return 1;
437  }
438  return 0;
439  case "overlay":
440  return 1;
441  }
442  return -1;
443  }
444  property int nextZInStack
445 
446  states: [
447  State {
448  name: "empty"
449  },
450  State {
451  name: "main"
452  },
453  State { // Side Stage only in overlay mode
454  name: "overlay"
455  },
456  State { // Main Stage and Side Stage in overlay mode
457  name: "mainAndOverlay"
458  },
459  State { // Main Stage and Side Stage in split mode
460  name: "mainAndSplit"
461  }
462  ]
463  state: {
464  if ((priv.mainStageItemId && !priv.sideStageItemId) || !priv.sideStageEnabled) {
465  return "main";
466  }
467  if (!priv.mainStageItemId && priv.sideStageItemId) {
468  return "overlay";
469  }
470  if (priv.mainStageItemId && priv.sideStageItemId) {
471  return "mainAndOverlay";
472  }
473  return "empty";
474  }
475 
476  onShiftedContentXChanged: {
477  if (root.beingResized) {
478  // Flickabe.contentX wiggles during resizes. Don't react to it.
479  return;
480  }
481 
482  switch (phase) {
483  case 0:
484  // the "spreadEnabled" part is because when code does "phase = 0; contentX = -shift" to
485  // dismiss the spread because spreadEnabled went to false, for some reason, during tests,
486  // Flickable might jump in and change contentX value back, causing the code below to do
487  // "phase = 1" which will make the spread stay.
488  // It sucks that we have no control whatsoever over whether or when Flickable animates its
489  // contentX.
490  if (root.spreadEnabled && shiftedContentX > width * positionMarker2) {
491  phase = 1;
492  }
493  break;
494  case 1:
495  if (shiftedContentX < width * positionMarker2) {
496  phase = 0;
497  } else if (shiftedContentX >= width * positionMarker4 && !spreadDragArea.dragging) {
498  phase = 2;
499  }
500  break;
501  }
502  }
503 
504  function snap() {
505  if (shiftedContentX < phase0Width) {
506  snapAnimation.targetContentX = -shift;
507  snapAnimation.start();
508  } else if (shiftedContentX < phase1Width) {
509  snapTo(1);
510  } else {
511  snapToSpread();
512  }
513  }
514 
515  function snapToSpread() {
516  // Add 1 pixel to make sure we definitely hit positionMarker4 even with rounding errors of the animation.
517  snapAnimation.targetContentX = (spreadView.width * spreadView.positionMarker4) + 1 - shift;
518  snapAnimation.start();
519  }
520 
521  function snapTo(index) {
522  snapAnimation.stop();
523  spreadView.selectedIndex = index;
524  snapAnimation.targetContentX = -shift;
525  snapAnimation.start();
526  }
527 
528  // We need to shuffle z ordering a bit in order to keep side stage apps above main stage apps.
529  // We don't want to really reorder them in the model because that allows us to keep track
530  // of the last focused order.
531  function indexToZIndex(index) {
532  // only shuffle when we've got a main and overlay
533  if (state !== "mainAndOverlay") return index;
534 
535  var app = root.topLevelSurfaceList.applicationAt(index);
536  if (!app) {
537  return index;
538  }
539  var stage = spreadRepeater.itemAt(index) ? spreadRepeater.itemAt(index).stage : app.stage;
540 
541  // don't shuffle indexes greater than "actives or next"
542  if (index > 2) return index;
543 
544  var mainStageIndex = root.topLevelSurfaceList.indexForId(priv.mainStageItemId);
545 
546  if (index == mainStageIndex) {
547  // Active main stage always at 0
548  return 0;
549  }
550 
551  if (spreadView.nextInStack > 0) {
552  var stageOfNextInStack = spreadRepeater.itemAt(spreadView.nextInStack).stage;
553 
554  if (index === spreadView.nextInStack) {
555  // this is the next app in stack.
556 
557  if (stage === ApplicationInfoInterface.SideStage) {
558  // if the next app in stack is a sidestage app, it must order on top of other side stage app
559  return Math.min(2, root.topLevelSurfaceList.count-1);
560  }
561  return 1;
562  }
563  if (stageOfNextInStack === ApplicationInfoInterface.SideStage) {
564  // if the next app in stack is a sidestage app, it must order on top of other side stage app
565  return 1;
566  }
567  return Math.min(2, root.topLevelSurfaceList.count-1);
568  }
569  return Math.min(index+1, root.topLevelSurfaceList.count-1);
570  }
571 
572  SequentialAnimation {
573  id: snapAnimation
574  property int targetContentX: -spreadView.shift
575 
576  UbuntuNumberAnimation {
577  target: spreadView
578  property: "contentX"
579  to: snapAnimation.targetContentX
580  duration: UbuntuAnimation.FastDuration
581  }
582 
583  ScriptAction {
584  script: {
585  if (spreadView.selectedIndex >= 0) {
586  var newIndex = spreadView.selectedIndex;
587  var application = root.topLevelSurfaceList.applicationAt(newIndex);
588  var spreadDelegate = spreadRepeater.itemAt(newIndex);
589  if (spreadDelegate.stage === ApplicationInfoInterface.SideStage) {
590  sideStage.showNow();
591  }
592  spreadView.selectedIndex = -1;
593  spreadDelegate.focus = true;
594  spreadView.phase = 0;
595  spreadView.contentX = -spreadView.shift;
596  }
597  }
598  }
599  }
600 
601  Behavior on contentX {
602  enabled: root.altTabPressed
603  UbuntuNumberAnimation {}
604  }
605 
606  MouseArea {
607  id: spreadRow
608  x: spreadView.contentX
609  width: spreadView.width + Math.max(spreadView.width, root.topLevelSurfaceList.count * spreadView.tileDistance)
610  height: root.height
611 
612  onClicked: {
613  spreadView.snapTo(0);
614  }
615 
616  DropArea {
617  objectName: "MainStageDropArea"
618  anchors {
619  left: parent.left
620  top: parent.top
621  bottom: parent.bottom
622  }
623  width: spreadView.width - sideStage.width
624  enabled: priv.sideStageEnabled
625 
626  onDropped: {
627  drop.source.spreadDelegate.stage = ApplicationInfoInterface.MainStage;
628  drop.source.spreadDelegate.focus = true;
629  }
630  keys: "SideStage"
631  }
632 
633  SideStage {
634  id: sideStage
635  objectName: "sideStage"
636  height: priv.landscapeHeight
637  x: spreadView.width - width
638  z: {
639  if (!priv.mainStageItemId) return 0;
640 
641  if (priv.sideStageItemId && spreadView.nextInStack > 0) {
642  var nextDelegateInStack = spreadRepeater.itemAt(spreadView.nextInStack);
643 
644  if (nextDelegateInStack.stage === ApplicationInfoInterface.MainStage) {
645  // if the next app in stack is a main stage app, put the sidestage on top of it.
646  return 2;
647  }
648  return 1;
649  }
650 
651  return 1;
652  }
653  visible: progress != 0
654  enabled: priv.sideStageEnabled && sideStageDropArea.dropAllowed
655  opacity: priv.sideStageEnabled && !spreadView.active ? 1 : 0
656  Behavior on opacity { UbuntuNumberAnimation {} }
657 
658  onShownChanged: {
659  if (!shown && priv.sideStageDelegate && priv.focusedAppDelegate === priv.sideStageDelegate
660  && priv.mainStageDelegate) {
661  priv.mainStageDelegate.focus = true;
662  } else if (shown && priv.sideStageDelegate) {
663  priv.sideStageDelegate.focus = true;
664  }
665  }
666 
667  DropArea {
668  id: sideStageDropArea
669  objectName: "SideStageDropArea"
670  anchors.fill: parent
671 
672  property bool dropAllowed: true
673 
674  onEntered: {
675  dropAllowed = drag.keys != "Disabled";
676  }
677  onExited: {
678  dropAllowed = true;
679  }
680  onDropped: {
681  if (drop.keys == "MainStage") {
682  drop.source.spreadDelegate.stage = ApplicationInfoInterface.SideStage;
683  drop.source.spreadDelegate.focus = true;
684  }
685  }
686  drag {
687  onSourceChanged: {
688  if (!sideStageDropArea.drag.source) {
689  dropAllowed = true;
690  }
691  }
692  }
693  }
694  }
695 
696  TopLevelSurfaceRepeater {
697  id: spreadRepeater
698  objectName: "spreadRepeater"
699  model: root.topLevelSurfaceList
700 
701  onItemAdded: {
702  priv.updateMainAndSideStageIndexes();
703  if (spreadView.phase == 2) {
704  spreadView.snapTo(index);
705  }
706  }
707 
708  onItemRemoved: {
709  priv.updateMainAndSideStageIndexes();
710  // Unless we're closing the app ourselves in the spread,
711  // lets make sure the spread doesn't mess up by the changing app list.
712  if (spreadView.closingIndex == -1) {
713  spreadView.phase = 0;
714  spreadView.contentX = -spreadView.shift;
715  focusTopMostApp();
716  }
717  }
718  function focusTopMostApp() {
719  if (spreadRepeater.count > 0) {
720  var topmostDelegate = spreadRepeater.itemAt(0);
721  topmostDelegate.focus = true;
722  }
723  }
724 
725  delegate: TransformedTabletSpreadDelegate {
726  id: spreadTile
727  objectName: "spreadDelegate_" + model.id
728 
729  readonly property int index: model.index
730  width: spreadView.width
731  height: spreadView.height
732  active: model.id == priv.mainStageItemId || model.id == priv.sideStageItemId
733  zIndex: selected && stage == ApplicationInfoInterface.MainStage ? 0 : spreadView.indexToZIndex(index)
734  onZIndexChanged: {
735  if (spreadView.nextInStack == model.index) {
736  spreadView.nextZInStack = zIndex;
737  }
738  }
739  selected: spreadView.selectedIndex == index
740  otherSelected: spreadView.selectedIndex >= 0 && !selected
741  isInSideStage: priv.sideStageItemId == model.id
742  interactive: !spreadView.interactive && spreadView.phase === 0 && root.interactive
743  swipeToCloseEnabled: spreadView.interactive && !snapAnimation.running
744  maximizedAppTopMargin: root.maximizedAppTopMargin
745  dragOffset: !isDash && model.id == priv.mainStageItemId && root.inverseProgress > 0
746  && spreadView.phase === 0 ? root.inverseProgress : 0
747  application: model.application
748  surface: model.surface
749  closeable: !isDash
750  highlightShown: root.altTabPressed && priv.highlightIndex == zIndex
751  dropShadow: spreadView.active || priv.focusedAppDelegateIsDislocated
752 
753  readonly property bool wantsMainStage: stage == ApplicationInfoInterface.MainStage
754 
755  readonly property bool isDash: application.appId == "unity8-dash"
756 
757  onFocusChanged: {
758  if (focus && !spreadRepeater.startingUp) {
759  priv.focusedAppDelegate = spreadTile;
760  root.topLevelSurfaceList.raiseId(model.id);
761  }
762  if (focus && priv.sideStageEnabled && stage === ApplicationInfoInterface.SideStage) {
763  sideStage.show();
764  }
765  }
766  Connections {
767  target: model.surface
768  onFocusRequested: spreadTile.focus = true;
769  }
770  Connections {
771  target: spreadTile.application
772  onFocusRequested: {
773  if (!model.surface) {
774  // when an app has no surfaces, we assume there's only one entry representing it:
775  // this delegate.
776  spreadTile.focus = true;
777  } else {
778  // if the application has surfaces, focus request should be at surface-level.
779  }
780  }
781  }
782 
783  fullscreen: {
784  if (priv.mainStageDelegate && stage === ApplicationInfoInterface.SideStage) {
785  return priv.mainStageDelegate.fullscreen;
786  } else if (surface) {
787  return surface.state === Mir.FullscreenState;
788  } else if (application) {
789  return application.fullscreen;
790  } else {
791  return false;
792  }
793  }
794 
795  supportedOrientations: {
796  if (application) {
797  var orientations = application.supportedOrientations;
798  if (stage == ApplicationInfoInterface.MainStage) {
799  // When an app is in the mainstage, it always supports Landscape|InvertedLandscape
800  // so that we can drag it from the main stage to the side stage
801  orientations |= Qt.LandscapeOrientation | Qt.InvertedLandscapeOrientation;
802  }
803  return orientations;
804  } else {
805  // we just don't care
806  return Qt.PortraitOrientation |
807  Qt.LandscapeOrientation |
808  Qt.InvertedPortraitOrientation |
809  Qt.InvertedLandscapeOrientation;
810  }
811  }
812 
813  // FIXME: A regular binding doesn't update any more after closing an app.
814  // Using a Binding for now.
815  Binding {
816  target: spreadTile
817  property: "z"
818  value: (!spreadView.active && isDash && !active) ? -1 : spreadTile.zIndex
819  }
820  x: spreadView.width
821 
822  property real behavioredZIndex: zIndex
823  Behavior on behavioredZIndex {
824  enabled: spreadView.closingIndex >= 0
825  UbuntuNumberAnimation {}
826  }
827  Connections {
828  target: priv
829  onSideStageEnabledChanged: refreshStage()
830  }
831 
832  property bool _constructing: true;
833  onStageChanged: {
834  if (!_constructing) {
835  priv.updateMainAndSideStageIndexes();
836  }
837  }
838 
839  Component.onCompleted: {
840  // a top level window is always the focused one when it first appears, unfocusing
841  // any preexisting one
842  focus = true;
843  refreshStage();
844  _constructing = false;
845  }
846  Component.onDestruction: {
847  WindowStateStorage.saveStage(application.appId, stage);
848  }
849 
850  function refreshStage() {
851  var newStage = ApplicationInfoInterface.MainStage;
852  if (priv.sideStageEnabled) {
853  if (application && application.supportedOrientations & (Qt.PortraitOrientation|Qt.InvertedPortraitOrientation)) {
854  newStage = WindowStateStorage.getStage(application.appId);
855  }
856  }
857 
858  stage = newStage;
859  }
860 
861  progress: {
862  var tileProgress = (spreadView.shiftedContentX - behavioredZIndex * spreadView.tileDistance) / spreadView.width;
863  // Some tiles (nextInStack, active) need to move directly from the beginning, normalize progress to immediately start at 0
864  if ((index == spreadView.nextInStack && spreadView.phase < 2) || (active && spreadView.phase < 1)) {
865  tileProgress += behavioredZIndex * spreadView.tileDistance / spreadView.width;
866  }
867  return tileProgress;
868  }
869 
870  // TODO: Hiding tile when progress is such that it will be off screen.
871  property bool occluded: {
872  if (spreadView.active && !offScreen) return false;
873  else if (spreadTile.active) return false;
874  else if (xTranslateAnimating) return false;
875  else if (z <= 1 && priv.focusedAppDelegateIsDislocated) return false;
876  return true;
877  }
878 
879  visible: Powerd.status == Powerd.On &&
880  !greeter.fullyShown &&
881  !occluded
882 
883  animatedProgress: {
884  if (spreadView.phase == 0 && (spreadTile.active || spreadView.nextInStack == index)) {
885  if (progress < spreadView.positionMarker1) {
886  return progress;
887  } else if (progress < spreadView.positionMarker1 + snappingCurve.period) {
888  return spreadView.positionMarker1 + snappingCurve.value * 3;
889  } else {
890  return spreadView.positionMarker2;
891  }
892  }
893  return progress;
894  }
895 
896  shellOrientationAngle: root.shellOrientationAngle
897  shellOrientation: root.shellOrientation
898  orientations: root.orientations
899 
900  states: [
901  State {
902  name: "MainStage"
903  when: spreadTile.stage == ApplicationInfoInterface.MainStage
904  },
905  State {
906  name: "SideStage"
907  when: spreadTile.stage == ApplicationInfoInterface.SideStage
908 
909  PropertyChanges {
910  target: spreadTile
911  width: spreadView.sideStageWidth
912  height: priv.landscapeHeight
913 
914  supportedOrientations: Qt.PortraitOrientation
915  shellOrientationAngle: 0
916  shellOrientation: Qt.PortraitOrientation
917  orientations: sideStageOrientations
918  }
919  }
920  ]
921 
922  Orientations {
923  id: sideStageOrientations
924  primary: Qt.PortraitOrientation
925  native_: Qt.PortraitOrientation
926  portrait: root.orientations.portrait
927  invertedPortrait: root.orientations.invertedPortrait
928  landscape: root.orientations.landscape
929  invertedLandscape: root.orientations.invertedLandscape
930  }
931 
932  transitions: [
933  Transition {
934  to: "SideStage"
935  SequentialAnimation {
936  PropertyAction {
937  target: spreadTile
938  properties: "width,height,supportedOrientations,shellOrientationAngle,shellOrientation,orientations"
939  }
940  ScriptAction {
941  script: {
942  // rotate immediately.
943  spreadTile.matchShellOrientation();
944  if (priv.focusedAppDelegate === spreadTile &&
945  priv.sideStageEnabled && !sideStage.shown) {
946  // Sidestage was focused, so show the side stage.
947  sideStage.show();
948  }
949  }
950  }
951  }
952  },
953  Transition {
954  from: "SideStage"
955  SequentialAnimation {
956  ScriptAction {
957  script: {
958  if (priv.sideStageDelegate === spreadTile &&
959  mainApp && (mainApp.supportedOrientations & (Qt.PortraitOrientation|Qt.InvertedPortraitOrientation)) == 0) {
960  // The mainstage app did not natively support portrait orientation, so focus the sidestage.
961  spreadTile.focus = true;
962  }
963  }
964  }
965  PropertyAction {
966  target: spreadTile
967  properties: "width,height,supportedOrientations,shellOrientationAngle,shellOrientation,orientations"
968  }
969  ScriptAction { script: { spreadTile.matchShellOrientation(); } }
970  }
971  }
972  ]
973 
974  onClicked: {
975  if (spreadView.phase == 2) {
976  spreadView.snapTo(index);
977  }
978  }
979 
980  onDraggedChanged: {
981  if (dragged) {
982  spreadView.draggedDelegateCount++;
983  } else {
984  spreadView.draggedDelegateCount--;
985  }
986  }
987 
988  onClosed: {
989  spreadView.closingIndex = index;
990  if (spreadTile.surface) {
991  spreadTile.surface.close();
992  } else if (spreadTile.application) {
993  root.applicationManager.stopApplication(spreadTile.application.appId);
994  } else {
995  // should never happen
996  console.warn("Can't close topLevelSurfaceList entry as it has neither"
997  + " a surface nor an application");
998  }
999  }
1000 
1001  Binding {
1002  target: root
1003  when: model.id == priv.mainStageItemId
1004  property: "mainAppWindowOrientationAngle"
1005  value: appWindowOrientationAngle
1006  }
1007  Binding {
1008  target: priv
1009  when: model.id == priv.mainStageItemId
1010  property: "mainAppOrientationChangesEnabled"
1011  value: orientationChangesEnabled
1012  }
1013 
1014  EasingCurve {
1015  id: snappingCurve
1016  type: EasingCurve.Linear
1017  period: (spreadView.positionMarker2 - spreadView.positionMarker1) / 3
1018  progress: spreadTile.progress - spreadView.positionMarker1
1019  }
1020 
1021  StagedFullscreenPolicy {
1022  id: fullscreenPolicy
1023  surface: model.surface
1024  }
1025  Connections {
1026  target: root
1027  onStageAboutToBeUnloaded: fullscreenPolicy.active = false
1028  }
1029  }
1030  }
1031  }
1032  }
1033 
1034  TabletSideStageTouchGesture {
1035  id: triGestureArea
1036  anchors.fill: parent
1037  enabled: priv.sideStageEnabled && !spreadView.active
1038  property var dragObject: null
1039 
1040  property Item spreadDelegate
1041 
1042  dragComponent: dragComponent
1043  dragComponentProperties: { "spreadDelegate": spreadDelegate }
1044 
1045  onPressed: {
1046  function matchDelegate(obj) { return String(obj.objectName).indexOf("spreadDelegate") >= 0; }
1047 
1048  var delegateAtCenter = Functions.itemAt(spreadRow, x, y, matchDelegate);
1049  if (!delegateAtCenter) return;
1050 
1051  spreadDelegate = delegateAtCenter;
1052  }
1053 
1054  onClicked: {
1055  if (sideStage.shown) {
1056  sideStage.hide();
1057  } else {
1058  sideStage.show();
1059  }
1060  }
1061 
1062  onDragStarted: {
1063  // If we're dragging to the sidestage.
1064  if (!sideStage.shown) {
1065  sideStage.show();
1066  }
1067  }
1068 
1069  Component {
1070  id: dragComponent
1071  SurfaceContainer {
1072  property Item spreadDelegate
1073 
1074  surface: spreadDelegate ? spreadDelegate.surface : null
1075 
1076  consumesInput: false
1077  interactive: false
1078  resizeSurface: false
1079  focus: false
1080 
1081  width: units.gu(40)
1082  height: units.gu(40)
1083 
1084  Drag.hotSpot.x: width/2
1085  Drag.hotSpot.y: height/2
1086  // only accept opposite stage.
1087  Drag.keys: {
1088  if (!surface) return "Disabled";
1089 
1090  if (spreadDelegate.stage === ApplicationInfo.MainStage) {
1091  if (spreadDelegate.application.supportedOrientations
1092  & (Qt.PortraitOrientation|Qt.InvertedPortraitOrientation)) {
1093  return "MainStage";
1094  }
1095  return "Disabled";
1096  }
1097  return "SideStage";
1098  }
1099  }
1100  }
1101  }
1102 
1103  //eat touch events during the right edge gesture
1104  MouseArea {
1105  anchors.fill: parent
1106  enabled: spreadDragArea.dragging
1107  }
1108 
1109  SwipeArea {
1110  id: spreadDragArea
1111  objectName: "spreadDragArea"
1112  x: parent.width - root.dragAreaWidth
1113  anchors { top: parent.top; bottom: parent.bottom }
1114  width: root.dragAreaWidth
1115  direction: Direction.Leftwards
1116  enabled: (spreadView.phase != 2 && root.spreadEnabled) || dragging
1117 
1118  property var gesturePoints: new Array()
1119 
1120  onTouchPositionChanged: {
1121  if (!dragging) {
1122  spreadView.phase = 0;
1123  spreadView.contentX = -spreadView.shift;
1124  }
1125 
1126  if (dragging) {
1127  var dragX = -touchPosition.x + spreadDragArea.width - spreadView.shift;
1128  var maxDrag = spreadView.width * spreadView.positionMarker4 - spreadView.shift;
1129  spreadView.contentX = Math.min(dragX, maxDrag);
1130  }
1131  gesturePoints.push(touchPosition.x);
1132  }
1133 
1134  onDraggingChanged: {
1135  if (dragging) {
1136  // Gesture recognized. Start recording this gesture
1137  gesturePoints = [];
1138  } else {
1139  // Ok. The user released. Find out if it was a one-way movement.
1140  var oneWayFlick = priv.evaluateOneWayFlick(gesturePoints);
1141  gesturePoints = [];
1142 
1143  if (oneWayFlick && spreadView.shiftedContentX < spreadView.positionMarker1 * spreadView.width) {
1144  // If it was a short one-way movement, do the Alt+Tab switch
1145  // no matter if we didn't cross positionMarker1 yet.
1146  spreadView.snapTo(spreadView.nextInStack);
1147  } else {
1148  if (spreadView.shiftedContentX < spreadView.width * spreadView.positionMarker1) {
1149  spreadView.snap();
1150  } else if (spreadView.shiftedContentX < spreadView.width * spreadView.positionMarker2) {
1151  spreadView.snapTo(spreadView.nextInStack);
1152  } else {
1153  // otherwise snap to the closest snap position we can find
1154  // (might be back to start, to app 1 or to spread)
1155  spreadView.snap();
1156  }
1157  }
1158  }
1159  }
1160  }
1161 
1162  EdgeBarrier {
1163  id: edgeBarrier
1164 
1165  // NB: it does its own positioning according to the specified edge
1166  edge: Qt.RightEdge
1167 
1168  onPassed: {
1169  spreadView.snapToSpread();
1170  }
1171  material: Component {
1172  Item {
1173  Rectangle {
1174  width: parent.height
1175  height: parent.width
1176  rotation: 90
1177  anchors.centerIn: parent
1178  gradient: Gradient {
1179  GradientStop { position: 0.0; color: Qt.rgba(0.16,0.16,0.16,0.7)}
1180  GradientStop { position: 1.0; color: Qt.rgba(0.16,0.16,0.16,0)}
1181  }
1182  }
1183  }
1184  }
1185  }
1186 }