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 to portrait
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.saveStage(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.saveStage(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  function saveStage(newStage) {
814  stage = newStage;
815  WindowStateStorage.saveStage(application.appId, newStage);
816  }
817 
818  // FIXME: A regular binding doesn't update any more after closing an app.
819  // Using a Binding for now.
820  Binding {
821  target: spreadTile
822  property: "z"
823  value: (!spreadView.active && isDash && !active) ? -1 : spreadTile.zIndex
824  }
825  x: spreadView.width
826 
827  property real behavioredZIndex: zIndex
828  Behavior on behavioredZIndex {
829  enabled: spreadView.closingIndex >= 0
830  UbuntuNumberAnimation {}
831  }
832  Connections {
833  target: priv
834  onSideStageEnabledChanged: refreshStage()
835  }
836 
837  property bool _constructing: true;
838  onStageChanged: {
839  if (!_constructing) {
840  priv.updateMainAndSideStageIndexes();
841  }
842  }
843 
844  Component.onCompleted: {
845  // a top level window is always the focused one when it first appears, unfocusing
846  // any preexisting one
847  focus = true;
848  refreshStage();
849  _constructing = false;
850  }
851 
852  function refreshStage() {
853  var newStage = ApplicationInfoInterface.MainStage;
854  if (priv.sideStageEnabled) { // we're in lanscape rotation.
855  if (!isDash && application && application.supportedOrientations & (Qt.PortraitOrientation|Qt.InvertedPortraitOrientation)) {
856  var defaultStage = ApplicationInfoInterface.SideStage; // if application supports portrait, it defaults to sidestage.
857  if (application.supportedOrientations & (Qt.LandscapeOrientation|Qt.InvertedLandscapeOrientation)) {
858  // if it supports lanscape, it defaults to mainstage.
859  defaultStage = ApplicationInfoInterface.MainStage;
860  }
861  newStage = WindowStateStorage.getStage(application.appId, defaultStage);
862  }
863  }
864 
865  stage = newStage;
866  }
867 
868  progress: {
869  var tileProgress = (spreadView.shiftedContentX - behavioredZIndex * spreadView.tileDistance) / spreadView.width;
870  // Some tiles (nextInStack, active) need to move directly from the beginning, normalize progress to immediately start at 0
871  if ((index == spreadView.nextInStack && spreadView.phase < 2) || (active && spreadView.phase < 1)) {
872  tileProgress += behavioredZIndex * spreadView.tileDistance / spreadView.width;
873  }
874  return tileProgress;
875  }
876 
877  // TODO: Hiding tile when progress is such that it will be off screen.
878  property bool occluded: {
879  if (spreadView.active && !offScreen) return false;
880  else if (spreadTile.active) return false;
881  else if (xTranslateAnimating) return false;
882  else if (z <= 1 && priv.focusedAppDelegateIsDislocated) return false;
883  return true;
884  }
885 
886  visible: Powerd.status == Powerd.On &&
887  !greeter.fullyShown &&
888  !occluded
889 
890  animatedProgress: {
891  if (spreadView.phase == 0 && (spreadTile.active || spreadView.nextInStack == index)) {
892  if (progress < spreadView.positionMarker1) {
893  return progress;
894  } else if (progress < spreadView.positionMarker1 + snappingCurve.period) {
895  return spreadView.positionMarker1 + snappingCurve.value * 3;
896  } else {
897  return spreadView.positionMarker2;
898  }
899  }
900  return progress;
901  }
902 
903  shellOrientationAngle: root.shellOrientationAngle
904  shellOrientation: root.shellOrientation
905  orientations: root.orientations
906 
907  states: [
908  State {
909  name: "MainStage"
910  when: spreadTile.stage == ApplicationInfoInterface.MainStage
911  },
912  State {
913  name: "SideStage"
914  when: spreadTile.stage == ApplicationInfoInterface.SideStage
915 
916  PropertyChanges {
917  target: spreadTile
918  width: spreadView.sideStageWidth
919  height: priv.landscapeHeight
920 
921  supportedOrientations: Qt.PortraitOrientation
922  shellOrientationAngle: 0
923  shellOrientation: Qt.PortraitOrientation
924  orientations: sideStageOrientations
925  }
926  }
927  ]
928 
929  Orientations {
930  id: sideStageOrientations
931  primary: Qt.PortraitOrientation
932  native_: Qt.PortraitOrientation
933  portrait: root.orientations.portrait
934  invertedPortrait: root.orientations.invertedPortrait
935  landscape: root.orientations.landscape
936  invertedLandscape: root.orientations.invertedLandscape
937  }
938 
939  transitions: [
940  Transition {
941  to: "SideStage"
942  SequentialAnimation {
943  PropertyAction {
944  target: spreadTile
945  properties: "width,height,supportedOrientations,shellOrientationAngle,shellOrientation,orientations"
946  }
947  ScriptAction {
948  script: {
949  // rotate immediately.
950  spreadTile.matchShellOrientation();
951  if (priv.focusedAppDelegate === spreadTile &&
952  priv.sideStageEnabled && !sideStage.shown) {
953  // Sidestage was focused, so show the side stage.
954  sideStage.show();
955  }
956  }
957  }
958  }
959  },
960  Transition {
961  from: "SideStage"
962  SequentialAnimation {
963  ScriptAction {
964  script: {
965  if (priv.sideStageDelegate === spreadTile &&
966  mainApp && (mainApp.supportedOrientations & (Qt.PortraitOrientation|Qt.InvertedPortraitOrientation)) == 0) {
967  // The mainstage app did not natively support portrait orientation, so focus the sidestage.
968  spreadTile.focus = true;
969  }
970  }
971  }
972  PropertyAction {
973  target: spreadTile
974  properties: "width,height,supportedOrientations,shellOrientationAngle,shellOrientation,orientations"
975  }
976  ScriptAction { script: { spreadTile.matchShellOrientation(); } }
977  }
978  }
979  ]
980 
981  onClicked: {
982  if (spreadView.phase == 2) {
983  spreadView.snapTo(index);
984  }
985  }
986 
987  onDraggedChanged: {
988  if (dragged) {
989  spreadView.draggedDelegateCount++;
990  } else {
991  spreadView.draggedDelegateCount--;
992  }
993  }
994 
995  onClosed: {
996  spreadView.closingIndex = index;
997  if (spreadTile.surface) {
998  spreadTile.surface.close();
999  } else if (spreadTile.application) {
1000  root.applicationManager.stopApplication(spreadTile.application.appId);
1001  } else {
1002  // should never happen
1003  console.warn("Can't close topLevelSurfaceList entry as it has neither"
1004  + " a surface nor an application");
1005  }
1006  }
1007 
1008  Binding {
1009  target: root
1010  when: model.id == priv.mainStageItemId
1011  property: "mainAppWindowOrientationAngle"
1012  value: appWindowOrientationAngle
1013  }
1014  Binding {
1015  target: priv
1016  when: model.id == priv.mainStageItemId
1017  property: "mainAppOrientationChangesEnabled"
1018  value: orientationChangesEnabled
1019  }
1020 
1021  EasingCurve {
1022  id: snappingCurve
1023  type: EasingCurve.Linear
1024  period: (spreadView.positionMarker2 - spreadView.positionMarker1) / 3
1025  progress: spreadTile.progress - spreadView.positionMarker1
1026  }
1027 
1028  StagedFullscreenPolicy {
1029  id: fullscreenPolicy
1030  surface: model.surface
1031  }
1032  Connections {
1033  target: root
1034  onStageAboutToBeUnloaded: fullscreenPolicy.active = false
1035  }
1036  }
1037  }
1038  }
1039  }
1040 
1041  TabletSideStageTouchGesture {
1042  id: triGestureArea
1043  anchors.fill: parent
1044  enabled: priv.sideStageEnabled && !spreadView.active
1045 
1046  property Item spreadDelegate
1047 
1048  dragComponent: dragComponent
1049  dragComponentProperties: { "spreadDelegate": spreadDelegate }
1050 
1051  onPressed: {
1052  function matchDelegate(obj) { return String(obj.objectName).indexOf("spreadDelegate") >= 0; }
1053 
1054  var delegateAtCenter = Functions.itemAt(spreadRow, x, y, matchDelegate);
1055  if (!delegateAtCenter) return;
1056 
1057  spreadDelegate = delegateAtCenter;
1058  }
1059 
1060  onClicked: {
1061  if (sideStage.shown) {
1062  sideStage.hide();
1063  } else {
1064  sideStage.show();
1065  }
1066  }
1067 
1068  onDragStarted: {
1069  // If we're dragging to the sidestage.
1070  if (!sideStage.shown) {
1071  sideStage.show();
1072  }
1073  }
1074 
1075  Component {
1076  id: dragComponent
1077  SurfaceContainer {
1078  property Item spreadDelegate
1079 
1080  surface: spreadDelegate ? spreadDelegate.surface : null
1081 
1082  consumesInput: false
1083  interactive: false
1084  resizeSurface: false
1085  focus: false
1086 
1087  width: units.gu(40)
1088  height: units.gu(40)
1089 
1090  Drag.hotSpot.x: width/2
1091  Drag.hotSpot.y: height/2
1092  // only accept opposite stage.
1093  Drag.keys: {
1094  if (!surface) return "Disabled";
1095  if (spreadDelegate.isDash) return "Disabled";
1096 
1097  if (spreadDelegate.stage === ApplicationInfo.MainStage) {
1098  if (spreadDelegate.application.supportedOrientations
1099  & (Qt.PortraitOrientation|Qt.InvertedPortraitOrientation)) {
1100  return "MainStage";
1101  }
1102  return "Disabled";
1103  }
1104  return "SideStage";
1105  }
1106  }
1107  }
1108  }
1109 
1110  //eat touch events during the right edge gesture
1111  MouseArea {
1112  anchors.fill: parent
1113  enabled: spreadDragArea.dragging
1114  }
1115 
1116  SwipeArea {
1117  id: spreadDragArea
1118  objectName: "spreadDragArea"
1119  x: parent.width - root.dragAreaWidth
1120  anchors { top: parent.top; bottom: parent.bottom }
1121  width: root.dragAreaWidth
1122  direction: Direction.Leftwards
1123  enabled: (spreadView.phase != 2 && root.spreadEnabled) || dragging
1124 
1125  property var gesturePoints: new Array()
1126 
1127  onTouchPositionChanged: {
1128  if (!dragging) {
1129  spreadView.phase = 0;
1130  spreadView.contentX = -spreadView.shift;
1131  }
1132 
1133  if (dragging) {
1134  var dragX = -touchPosition.x + spreadDragArea.width - spreadView.shift;
1135  var maxDrag = spreadView.width * spreadView.positionMarker4 - spreadView.shift;
1136  spreadView.contentX = Math.min(dragX, maxDrag);
1137  }
1138  gesturePoints.push(touchPosition.x);
1139  }
1140 
1141  onDraggingChanged: {
1142  if (dragging) {
1143  // Gesture recognized. Start recording this gesture
1144  gesturePoints = [];
1145  } else {
1146  // Ok. The user released. Find out if it was a one-way movement.
1147  var oneWayFlick = priv.evaluateOneWayFlick(gesturePoints);
1148  gesturePoints = [];
1149 
1150  if (oneWayFlick && spreadView.shiftedContentX < spreadView.positionMarker1 * spreadView.width) {
1151  // If it was a short one-way movement, do the Alt+Tab switch
1152  // no matter if we didn't cross positionMarker1 yet.
1153  spreadView.snapTo(spreadView.nextInStack);
1154  } else {
1155  if (spreadView.shiftedContentX < spreadView.width * spreadView.positionMarker1) {
1156  spreadView.snap();
1157  } else if (spreadView.shiftedContentX < spreadView.width * spreadView.positionMarker2) {
1158  spreadView.snapTo(spreadView.nextInStack);
1159  } else {
1160  // otherwise snap to the closest snap position we can find
1161  // (might be back to start, to app 1 or to spread)
1162  spreadView.snap();
1163  }
1164  }
1165  }
1166  }
1167  }
1168 
1169  EdgeBarrier {
1170  id: edgeBarrier
1171 
1172  // NB: it does its own positioning according to the specified edge
1173  edge: Qt.RightEdge
1174 
1175  onPassed: {
1176  spreadView.snapToSpread();
1177  }
1178  material: Component {
1179  Item {
1180  Rectangle {
1181  width: parent.height
1182  height: parent.width
1183  rotation: 90
1184  anchors.centerIn: parent
1185  gradient: Gradient {
1186  GradientStop { position: 0.0; color: Qt.rgba(0.16,0.16,0.16,0.7)}
1187  GradientStop { position: 1.0; color: Qt.rgba(0.16,0.16,0.16,0)}
1188  }
1189  }
1190  }
1191  }
1192  }
1193 }