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