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