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