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