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