Unity 8
PhoneStage.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 Unity.Session 0.1
22 import Utils 0.1
23 import Powerd 0.1
24 import "../Components"
25 
26 AbstractStage {
27  id: root
28 
29  property bool focusFirstApp: true // If false, focused app will appear on right edge like other apps
30  property bool altTabEnabled: true
31  property real startScale: 1.1
32  property real endScale: 0.7
33 
34  paintBackground: spreadView.shiftedContentX !== 0
35 
36  onBeingResizedChanged: {
37  if (beingResized) {
38  // Brace yourselves for impact!
39  priv.reset();
40  }
41  }
42  onSpreadEnabledChanged: {
43  if (!spreadEnabled) {
44  priv.reset();
45  }
46  }
47 
48  onAltTabPressedChanged: {
49  if (!spreadEnabled || !altTabEnabled) {
50  return;
51  }
52  if (altTabPressed) {
53  spreadView.snapToSpread();
54  priv.highlightIndex = Math.min(spreadRepeater.count - 1, 1);
55  } else {
56  spreadView.snapTo(priv.highlightIndex)
57  }
58  }
59 
60  FocusScope {
61  focus: root.altTabPressed
62 
63  Keys.onPressed: {
64  switch (event.key) {
65  case Qt.Key_Tab:
66  priv.highlightIndex = (priv.highlightIndex + 1) % spreadRepeater.count
67  break;
68  case Qt.Key_Backtab:
69  priv.highlightIndex = (priv.highlightIndex + spreadRepeater.count - 1) % spreadRepeater.count
70  break;
71  }
72  }
73  }
74 
75  // Functions to be called from outside
76  function updateFocusedAppOrientation() {
77  if (spreadRepeater.count > 0) {
78  spreadRepeater.itemAt(0).matchShellOrientation();
79  }
80 
81  for (var i = 1; i < spreadRepeater.count; ++i) {
82 
83  var spreadDelegate = spreadRepeater.itemAt(i);
84 
85  var delta = spreadDelegate.appWindowOrientationAngle - root.shellOrientationAngle;
86  if (delta < 0) { delta += 360; }
87  delta = delta % 360;
88 
89  var supportedOrientations = spreadDelegate.application.supportedOrientations;
90  if (supportedOrientations === Qt.PrimaryOrientation) {
91  supportedOrientations = root.orientations.primary;
92  }
93 
94  if (delta === 180 && (supportedOrientations & spreadDelegate.shellOrientation)) {
95  spreadDelegate.matchShellOrientation();
96  }
97  }
98  }
99  function updateFocusedAppOrientationAnimated() {
100  if (spreadRepeater.count > 0) {
101  spreadRepeater.itemAt(0).animateToShellOrientation();
102  }
103  }
104 
105  function pushRightEdge(amount) {
106  if (spreadView.contentX == -spreadView.shift) {
107  edgeBarrier.push(amount);
108  }
109  }
110 
111  mainApp: priv.focusedAppDelegate ? priv.focusedAppDelegate.application : null
112 
113  orientationChangesEnabled: priv.focusedAppOrientationChangesEnabled
114  && !priv.focusedAppDelegateIsDislocated
115  && !(priv.focusedAppDelegate && priv.focusedAppDelegate.xBehavior.running)
116  && spreadView.phase === 0
117 
118  supportedOrientations: mainApp ? mainApp.supportedOrientations
119  : (Qt.PortraitOrientation | Qt.LandscapeOrientation
120  | Qt.InvertedPortraitOrientation | Qt.InvertedLandscapeOrientation)
121 
122  // How far left the stage has been dragged, used externally by tutorial code
123  dragProgress: spreadRepeater.count > 0 ? spreadRepeater.itemAt(0).animatedProgress : 0
124 
125  readonly property alias dragging: spreadDragArea.dragging
126 
127  signal opened()
128 
129  function select(appId) {
130  spreadView.snapTo(priv.indexOf(appId));
131  }
132 
133  onInverseProgressChanged: {
134  // This can't be a simple binding because that would be triggered after this handler
135  // while we need it active before doing the anition left/right
136  priv.animateX = (inverseProgress == 0)
137  if (inverseProgress == 0 && priv.oldInverseProgress > 0) {
138  // left edge drag released. Minimum distance is given by design.
139  if (priv.oldInverseProgress > units.gu(22)) {
140  applicationManager.requestFocusApplication("unity8-dash");
141  }
142  }
143  priv.oldInverseProgress = inverseProgress;
144  }
145 
146  // <FIXME-contentX> See rationale in the next comment with this tag
147  onWidthChanged: {
148  if (!root.beingResized) {
149  // we're being resized without a warning (ie, the corresponding property wasn't set
150  root.beingResized = true;
151  beingResizedTimer.start();
152  }
153  }
154  Timer {
155  id: beingResizedTimer
156  interval: 100
157  onTriggered: { root.beingResized = false; }
158  }
159 
160  QtObject {
161  id: priv
162 
163  property bool focusedAppOrientationChangesEnabled: false
164  readonly property int firstSpreadIndex: root.focusFirstApp ? 1 : 0
165  property var focusedAppDelegate
166  // NB! This may differ from applicationManager.focusedApplicationId if focusedAppDelegate
167  // contains a screenshot instead of a surface.
168  property string focusedAppId: focusedAppDelegate ? focusedAppDelegate.application.appId : ""
169 
170  property real oldInverseProgress: 0
171  property bool animateX: false
172  property int highlightIndex: 0
173 
174  property bool focusedAppDelegateIsDislocated: focusedAppDelegate ?
175  (focusedAppDelegate.x !== 0 || focusedAppDelegate.xBehavior.running)
176  : false
177 
178  function indexOf(appId) {
179  for (var i = 0; i < spreadRepeater.count; i++) {
180  if (spreadRepeater.itemAt(i).application.appId == appId) {
181  return i;
182  }
183  }
184  return -1;
185  }
186 
187  // Is more stable than "spreadView.shiftedContentX === 0" as it filters out noise caused by
188  // Flickable.contentX changing due to resizes.
189  property bool fullyShowingFocusedApp: true
190 
191  function reset() {
192  // The app that's about to go to foreground has to be focused, otherwise
193  // it would leave us in an inconsistent state.
194  if (!MirFocusController.focusedSurface && spreadRepeater.count > 0) {
195  spreadRepeater.itemAt(0).focus = true;
196  }
197 
198  spreadView.selectedIndex = -1;
199  spreadView.phase = 0;
200  spreadView.contentX = -spreadView.shift;
201  }
202 
203  onHighlightIndexChanged: {
204  spreadView.contentX = highlightIndex * spreadView.contentWidth / (spreadRepeater.count + 2)
205  }
206  }
207  Timer {
208  id: fullyShowingFocusedAppUpdateTimer
209  interval: 100
210  onTriggered: {
211  priv.fullyShowingFocusedApp = spreadView.shiftedContentX === 0;
212  }
213  }
214 
215  Instantiator {
216  model: root.applicationManager
217  delegate: QtObject {
218  property var stateBinding: Binding {
219  readonly property bool isDash: model.application ? model.application.appId == "unity8-dash" : false
220  target: model.application
221  property: "requestedState"
222  value: (isDash && root.keepDashRunning)
223  || (!root.suspended && model.application && priv.focusedAppId === model.application.appId)
224  ? ApplicationInfoInterface.RequestedRunning
225  : ApplicationInfoInterface.RequestedSuspended
226  }
227 
228  property var lifecycleBinding: Binding {
229  target: model.application
230  property: "exemptFromLifecycle"
231  value: model.application
232  ? (!model.application.isTouchApp || isExemptFromLifecycle(model.application.appId))
233  : false
234  }
235  }
236  }
237 
238  Binding {
239  target: MirFocusController
240  property: "focusedSurface"
241  value: priv.focusedAppDelegate ? priv.focusedAppDelegate.surface : null
242  when: root.parent && !spreadRepeater.startingUp
243  }
244 
245  Flickable {
246  id: spreadView
247  objectName: "spreadView"
248  anchors.fill: parent
249  interactive: (spreadDragArea.dragging || phase > 1) && draggedDelegateCount === 0
250  contentWidth: spreadRow.width - shift
251  contentX: -shift
252 
253  // This indicates when the spreadView is active. That means, all the animations
254  // are activated and tiles need to line up for the spread.
255  readonly property bool active: shiftedContentX > 0 || spreadDragArea.dragging || !root.focusFirstApp
256 
257  // The flickable needs to fill the screen in order to get touch events all over.
258  // However, we don't want to the user to be able to scroll back all the way. For
259  // that, the beginning of the gesture starts with a negative value for contentX
260  // so the flickable wants to pull it into the view already. "shift" tunes the
261  // distance where to "lock" the content.
262  readonly property real shift: width / 2
263  readonly property real shiftedContentX: contentX + shift
264 
265  property int tileDistance: width / 4
266 
267  // Those markers mark the various positions in the spread (ratio to screen width from right to left):
268  // 0 - 1: following finger, snap back to the beginning on release
269  property real positionMarker1: 0.2
270  // 1 - 2: curved snapping movement, snap to app 1 on release
271  property real positionMarker2: 0.3
272  // 2 - 3: movement follows finger, snaps back to app 1 on release
273  property real positionMarker3: 0.35
274  // passing 3, we detach movement from the finger and snap to 4
275  property real positionMarker4: 0.9
276 
277  // This is where the first app snaps to when bringing it in from the right edge.
278  property real snapPosition: 0.7
279 
280  // Phase of the animation:
281  // 0: Starting from right edge, a new app (index 1) comes in from the right
282  // 1: The app has reached the first snap position.
283  // 2: The list is dragged further and snaps into the spread view when entering phase 2
284  property int phase: 0
285 
286  property int selectedIndex: -1
287  property int draggedDelegateCount: 0
288  property int closingIndex: -1
289 
290  // <FIXME-contentX> Workaround Flickable's behavior of bringing contentX back between valid boundaries
291  // when resized. The proper way to fix this is refactoring PhoneStage so that it doesn't
292  // rely on having Flickable.contentX keeping an out-of-bounds value when it's set programatically
293  // (as opposed to having contentX reaching an out-of-bounds value through dragging, which will trigger
294  // the Flickable.boundsBehavior upon release).
295  onContentXChanged: {
296  if (!undoContentXReset()) {
297  forceItToRemainStillIfBeingResized();
298  }
299  }
300  onShiftChanged: { forceItToRemainStillIfBeingResized(); }
301  function forceItToRemainStillIfBeingResized() {
302  if (root.beingResized && contentX != -spreadView.shift) {
303  contentX = -spreadView.shift;
304  }
305  }
306  function undoContentXReset() {
307  if (contentWidth <= 0) {
308  contentWidthOnLastContentXChange = contentWidth;
309  lastContentX = contentX;
310  return false;
311  }
312 
313  if (contentWidth !== contentWidthOnLastContentXChange
314  && lastContentX === -shift && contentX === 0) {
315  // Flickable is resetting contentX because contentWidth has changed. Undo it.
316  contentX = -shift;
317  return true;
318  }
319 
320  contentWidthOnLastContentXChange = contentWidth;
321  lastContentX = contentX;
322  return false;
323  }
324  property real contentWidthOnLastContentXChange: -1
325  property real lastContentX: 0
326  // </FIXME-contentX>
327 
328  Behavior on contentX {
329  enabled: root.altTabPressed
330  UbuntuNumberAnimation {}
331  }
332 
333  onShiftedContentXChanged: {
334  if (root.beingResized) {
335  // Flickabe.contentX wiggles during resizes. Don't react to it.
336  return;
337  }
338 
339  switch (phase) {
340  case 0:
341  // the "spreadEnabled" part is because when code does "phase = 0; contentX = -shift" to
342  // dismiss the spread because spreadEnabled went to false, for some reason, during tests,
343  // Flickable might jump in and change contentX value back, causing the code below to do
344  // "phase = 1" which will make the spread stay.
345  // It sucks that we have no control whatsoever over whether or when Flickable animates its
346  // contentX.
347  if (root.spreadEnabled && shiftedContentX > width * positionMarker2) {
348  phase = 1;
349  }
350  break;
351  case 1:
352  if (shiftedContentX < width * positionMarker2) {
353  phase = 0;
354  } else if (shiftedContentX >= width * positionMarker4 && !spreadDragArea.dragging) {
355  phase = 2;
356  }
357  break;
358  }
359  fullyShowingFocusedAppUpdateTimer.restart();
360  }
361 
362  function snap() {
363  if (shiftedContentX < positionMarker1 * width) {
364  snapAnimation.targetContentX = -shift;
365  snapAnimation.start();
366  } else if (shiftedContentX < positionMarker2 * width) {
367  snapTo(1);
368  } else if (shiftedContentX < positionMarker3 * width) {
369  snapTo(1);
370  } else if (phase < 2){
371  snapToSpread();
372  root.opened();
373  }
374  }
375 
376  function snapToSpread() {
377  // Add 1 pixel to make sure we definitely hit positionMarker4 even with rounding errors of the animation.
378  snapAnimation.targetContentX = (root.width * spreadView.positionMarker4) + 1 - spreadView.shift;
379  snapAnimation.start();
380  }
381 
382  function snapTo(index) {
383  if (!root.altTabEnabled) {
384  // Reset to start instead
385  snapAnimation.targetContentX = -shift;
386  snapAnimation.start();
387  return;
388  }
389  if (topLevelSurfaceList.count <= index) {
390  // In case we're trying to snap to some non existing app, lets snap back to the first one
391  index = 0;
392  }
393  spreadView.selectedIndex = index;
394  // If we're not in full spread mode yet, always unwind to start pos
395  // otherwise unwind up to progress 0 of the selected index
396  if (spreadView.phase < 2) {
397  snapAnimation.targetContentX = -shift;
398  } else {
399  snapAnimation.targetContentX = -shift + index * spreadView.tileDistance;
400  }
401  snapAnimation.start();
402  }
403 
404  // In case the applicationManager already holds an app when starting up we're missing animations
405  // Make sure we end up in the same state
406  Component.onCompleted: {
407  spreadView.contentX = -spreadView.shift
408  priv.animateX = true;
409  snapAnimation.complete();
410  }
411 
412  SequentialAnimation {
413  id: snapAnimation
414  property int targetContentX: -spreadView.shift
415 
416  UbuntuNumberAnimation {
417  target: spreadView
418  property: "contentX"
419  to: snapAnimation.targetContentX
420  duration: UbuntuAnimation.FastDuration
421  }
422 
423  ScriptAction {
424  script: {
425  if (spreadView.selectedIndex >= 0) {
426  var delegate = spreadRepeater.itemAt(spreadView.selectedIndex)
427  delegate.focus = true;
428 
429  spreadView.selectedIndex = -1;
430  spreadView.phase = 0;
431  spreadView.contentX = -spreadView.shift;
432  }
433  }
434  }
435  }
436 
437  MouseArea {
438  id: spreadRow
439  // This width controls how much the spread can be flicked left/right. It's composed of:
440  // tileDistance * app count (with a minimum of 3 apps, in order to also allow moving 1 and 2 apps a bit)
441  // + some constant value (still scales with the screen width) which looks good and somewhat fills the screen
442  width: Math.max(3, topLevelSurfaceList.count) * spreadView.tileDistance + (spreadView.width - spreadView.tileDistance) * 1.5
443  height: parent.height
444  Behavior on width {
445  enabled: spreadView.closingIndex >= 0
446  UbuntuNumberAnimation {}
447  }
448  onWidthChanged: {
449  if (spreadView.closingIndex >= 0) {
450  spreadView.contentX = Math.min(spreadView.contentX, width - spreadView.width - spreadView.shift);
451  }
452  }
453 
454  x: spreadView.contentX
455 
456  onClicked: {
457  if (root.altTabEnabled) {
458  spreadView.snapTo(0);
459  }
460  }
461 
462  TopLevelSurfaceRepeater {
463  id: spreadRepeater
464  objectName: "spreadRepeater"
465  model: topLevelSurfaceList
466 
467  onItemRemoved: {
468  // Unless we're closing the app ourselves in the spread,
469  // lets make sure the spread doesn't mess up by the changing app list.
470  if (spreadView.closingIndex == -1) {
471  spreadView.phase = 0;
472  spreadView.contentX = -spreadView.shift;
473  focusTopMostApp();
474  }
475  }
476  function focusTopMostApp() {
477  if (spreadRepeater.count > 0) {
478  var topmostDelegate = spreadRepeater.itemAt(0);
479  topmostDelegate.focus = true;
480  }
481  }
482 
483  delegate: TransformedSpreadDelegate {
484  id: appDelegate
485  objectName: "spreadDelegate_" + model.id
486  startAngle: 45
487  endAngle: 5
488  startScale: root.startScale
489  endScale: root.endScale
490  startDistance: spreadView.tileDistance
491  endDistance: units.gu(.5)
492  width: spreadView.width
493  height: spreadView.height
494  selected: spreadView.selectedIndex == index
495  otherSelected: spreadView.selectedIndex >= 0 && !selected
496  interactive: !spreadView.interactive && spreadView.phase === 0
497  && priv.fullyShowingFocusedApp && root.interactive && focus
498  swipeToCloseEnabled: spreadView.interactive && root.interactive && !snapAnimation.running
499  maximizedAppTopMargin: root.maximizedAppTopMargin
500  dropShadow: spreadView.active || priv.focusedAppDelegateIsDislocated
501  focusFirstApp: root.focusFirstApp
502  highlightShown: root.altTabPressed && index === priv.highlightIndex
503 
504  readonly property bool isDash: model.application.appId == "unity8-dash"
505 
506  Component.onCompleted: {
507  // NB: We're differentiating if this delegate was created in response to a new entry in the model
508  // or if the Repeater is just populating itself with delegates to match the model it received.
509  if (!spreadRepeater.startingUp) {
510  // a top level window is always the focused one when it first appears, unfocusing
511  // any preexisting one
512  //
513  // new items are appended and must be manually brought to front.
514  // that's how it *must* be in order to get the animation for new
515  // surfaces working
516  claimFocus();
517  }
518  }
519 
520  onFocusChanged: {
521  if (focus && !spreadRepeater.startingUp) {
522  priv.focusedAppDelegate = appDelegate;
523  // If we're orphan (!parent) it means this stage is no longer the current one
524  // and will be deleted shortly. So we should no longer have a say over the model
525  if (root.parent) {
526  topLevelSurfaceList.raiseId(model.id);
527  }
528  }
529  }
530  function claimFocus() {
531  if (spreadView.phase > 0) {
532  spreadView.snapTo(model.index);
533  } else {
534  appDelegate.focus = true;
535  }
536  }
537  Connections {
538  target: model.surface
539  onFocusRequested: claimFocus()
540  }
541  Connections {
542  target: model.application
543  onFocusRequested: {
544  if (!model.surface) {
545  // when an app has no surfaces, we assume there's only one entry representing it:
546  // this delegate.
547  claimFocus();
548  } else {
549  // if the application has surfaces, focus request should be at surface-level.
550  }
551  }
552  }
553 
554  z: isDash && !spreadView.active ? -1 : behavioredIndex
555 
556  x: {
557  // focused app is always positioned at 0 except when following left edge drag
558  if (isFocused) {
559  if (!isDash && root.inverseProgress > 0 && spreadView.phase === 0) {
560  return root.inverseProgress;
561  }
562  return 0;
563  }
564  if (isDash && !spreadView.active && !spreadDragArea.dragging) {
565  return 0;
566  }
567 
568  // Otherwise line up for the spread
569  return spreadView.width + spreadIndex * spreadView.tileDistance;
570  }
571 
572  application: model.application
573  surface: model.surface
574  closeable: !isDash
575 
576  property real behavioredIndex: index
577  Behavior on behavioredIndex {
578  enabled: spreadView.closingIndex >= 0
579  UbuntuNumberAnimation {
580  id: appXAnimation
581  onRunningChanged: {
582  if (!running) {
583  spreadView.closingIndex = -1;
584  }
585  }
586  }
587  }
588 
589  property var xBehavior: xBehavior
590  Behavior on x {
591  enabled: root.spreadEnabled &&
592  !spreadView.active &&
593  !snapAnimation.running &&
594  !spreadDragArea.pressed &&
595  priv.animateX &&
596  !root.beingResized
597  UbuntuNumberAnimation {
598  id: xBehavior
599  duration: UbuntuAnimation.BriskDuration
600  }
601  }
602 
603  // Each tile has a different progress value running from 0 to 1.
604  // 0: means the tile is at the right edge.
605  // 1: means the tile has finished the main animation towards the left edge.
606  // >1: after the main animation has finished, tiles will continue to move very slowly to the left
607  progress: {
608  var tileProgress = (spreadView.shiftedContentX - behavioredIndex * spreadView.tileDistance) / spreadView.width;
609  // Tile 1 needs to move directly from the beginning...
610  if (root.focusFirstApp && behavioredIndex == 1 && spreadView.phase < 2) {
611  tileProgress += spreadView.tileDistance / spreadView.width;
612  }
613  // Limiting progress to ~0 and 1.7 to avoid binding calculations when tiles are not
614  // visible.
615  // < 0 : The tile is outside the screen on the right
616  // > 1.7: The tile is *very* close to the left edge and covered by other tiles now.
617  // Using 0.0001 to differentiate when a tile should still be visible (==0)
618  // or we can hide it (< 0)
619  tileProgress = Math.max(-0.0001, Math.min(1.7, tileProgress));
620  return tileProgress;
621  }
622 
623  // This mostly is the same as progress, just adds the snapping to phase 1 for tiles 0 and 1
624  animatedProgress: {
625  if (spreadView.phase == 0 && index <= priv.firstSpreadIndex) {
626  if (progress < spreadView.positionMarker1) {
627  return progress;
628  } else if (progress < spreadView.positionMarker1 + 0.05){
629  // p : 0.05 = x : pm2
630  return spreadView.positionMarker1 + (progress - spreadView.positionMarker1) * (spreadView.positionMarker2 - spreadView.positionMarker1) / 0.05
631  } else {
632  return spreadView.positionMarker2;
633  }
634  }
635  return progress;
636  }
637 
638  // Hide tile when progress is such that it will be off screen.
639  property bool occluded: {
640  if (spreadView.active && (progress >= 0 && progress < 1.7)) return false;
641  else if (!spreadView.active && isFocused) return false;
642  else if (xBehavior.running) return false;
643  else if (z <= 1 && priv.focusedAppDelegateIsDislocated) return false;
644  return true;
645  }
646 
647  visible: Powerd.status == Powerd.On &&
648  !greeter.fullyShown &&
649  !occluded
650 
651  shellOrientationAngle: root.shellOrientationAngle
652  shellOrientation: root.shellOrientation
653  orientations: root.orientations
654 
655  onClicked: {
656  if (root.altTabEnabled && spreadView.phase == 2) {
657  spreadView.snapTo(index);
658  }
659  }
660 
661  onDraggedChanged: {
662  if (dragged) {
663  spreadView.draggedDelegateCount++;
664  } else {
665  spreadView.draggedDelegateCount--;
666  }
667  }
668 
669  onClosed: {
670  spreadView.closingIndex = index;
671  if (appDelegate.surface) {
672  appDelegate.surface.close();
673  } else if (appDelegate.application) {
674  root.applicationManager.stopApplication(appDelegate.application.appId);
675  } else {
676  // should never happen
677  console.warn("Can't close topLevelSurfaceList entry as it has neither"
678  + " a surface nor an application");
679  }
680  }
681 
682  Binding {
683  target: root
684  when: index == 0
685  property: "mainAppWindowOrientationAngle"
686  value: appWindowOrientationAngle
687  }
688  Binding {
689  target: priv
690  when: index == 0
691  property: "focusedAppOrientationChangesEnabled"
692  value: orientationChangesEnabled
693  }
694 
695  StagedFullscreenPolicy {
696  id: fullscreenPolicy
697  surface: model.surface
698  }
699 
700  Connections {
701  target: root
702  onStageAboutToBeUnloaded: fullscreenPolicy.active = false
703  }
704  }
705  } // Repeater {
706  }
707  }
708 
709  //eat touch events during the right edge gesture
710  MouseArea {
711  objectName: "eventEaterArea"
712  anchors.fill: parent
713  enabled: spreadDragArea.dragging
714  }
715 
716  SwipeArea {
717  id: spreadDragArea
718  objectName: "spreadDragArea"
719  direction: Direction.Leftwards
720  enabled: (spreadView.phase != 2 && root.spreadEnabled) || dragging
721 
722  anchors { top: parent.top; right: parent.right; bottom: parent.bottom; }
723  width: root.dragAreaWidth
724 
725  property var gesturePoints: new Array()
726 
727  onTouchPositionChanged: {
728  if (dragging) {
729  // Gesture recognized. Let's move the spreadView with the finger
730  var dragX = Math.min(touchPosition.x + width, width); // Prevent dragging rightwards
731  dragX = -dragX + spreadDragArea.width - spreadView.shift;
732  // Don't allow dragging further than the animation crossing with phase2's animation
733  var maxMovement = spreadView.width * spreadView.positionMarker4 - spreadView.shift;
734 
735  spreadView.contentX = Math.min(dragX, maxMovement);
736  } else {
737  // Initial touch. Let's reset the spreadView to the starting position.
738  spreadView.phase = 0;
739  spreadView.contentX = -spreadView.shift;
740  }
741 
742  gesturePoints.push(touchPosition.x);
743  }
744 
745  onDraggingChanged: {
746  if (dragging) {
747  // A potential edge-drag gesture has started. Start recording it
748  gesturePoints = [];
749  } else {
750  // Ok. The user released. Find out if it was a one-way movement.
751  var oneWayFlick = true;
752  var smallestX = spreadDragArea.width;
753  for (var i = 0; i < gesturePoints.length; i++) {
754  if (gesturePoints[i] >= smallestX) {
755  oneWayFlick = false;
756  break;
757  }
758  smallestX = gesturePoints[i];
759  }
760  gesturePoints = [];
761 
762  if (oneWayFlick && spreadView.shiftedContentX > units.gu(2) &&
763  spreadView.shiftedContentX < spreadView.positionMarker1 * spreadView.width) {
764  // If it was a short one-way movement, do the Alt+Tab switch
765  // no matter if we didn't cross positionMarker1 yet.
766  spreadView.snapTo(1);
767  } else if (!dragging) {
768  // otherwise snap to the closest snap position we can find
769  // (might be back to start, to app 1 or to spread)
770  spreadView.snap();
771  }
772  }
773  }
774  }
775 
776  EdgeBarrier {
777  id: edgeBarrier
778 
779  // NB: it does its own positioning according to the specified edge
780  edge: Qt.RightEdge
781 
782  onPassed: {
783  spreadView.snapToSpread();
784  }
785  material: Component {
786  Item {
787  Rectangle {
788  width: parent.height
789  height: parent.width
790  rotation: 90
791  anchors.centerIn: parent
792  gradient: Gradient {
793  GradientStop { position: 0.0; color: Qt.rgba(0.16,0.16,0.16,0.7)}
794  GradientStop { position: 1.0; color: Qt.rgba(0.16,0.16,0.16,0)}
795  }
796  }
797  }
798  }
799  }
800 }