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