Unity 8
 All Classes Functions
PhoneStage.qml
1 /*
2  * Copyright (C) 2014 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.2
18 import Ubuntu.Components 0.1
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 "../Components"
24 import "../Components/Flickables" as Flickables
25 
26 Rectangle {
27  id: root
28 
29  // Controls to be set from outside
30  property int dragAreaWidth
31  property real maximizedAppTopMargin
32  property bool interactive
33  property bool spreadEnabled: true // If false, animations and right edge will be disabled
34  property real inverseProgress: 0 // This is the progress for left edge drags, in pixels.
35  property int orientation: Qt.PortraitOrientation
36 
37  color: "black"
38 
39  function select(appId) {
40  spreadView.snapTo(priv.indexOf(appId));
41  }
42 
43  onWidthChanged: {
44  spreadView.selectedIndex = -1;
45  spreadView.phase = 0;
46  spreadView.contentX = -spreadView.shift;
47  }
48 
49  onInverseProgressChanged: {
50  // This can't be a simple binding because that would be triggered after this handler
51  // while we need it active before doing the anition left/right
52  priv.animateX = (inverseProgress == 0)
53  if (inverseProgress == 0 && priv.oldInverseProgress > 0) {
54  // left edge drag released. Minimum distance is given by design.
55  if (priv.oldInverseProgress > units.gu(22)) {
56  ApplicationManager.focusApplication("unity8-dash");
57  }
58  }
59  priv.oldInverseProgress = inverseProgress;
60  }
61 
62  Connections {
63  target: ApplicationManager
64 
65  onFocusRequested: {
66  if (spreadView.phase > 0) {
67  spreadView.snapTo(priv.indexOf(appId));
68  } else {
69  ApplicationManager.focusApplication(appId);
70  }
71  }
72 
73  onApplicationAdded: {
74  if (spreadView.phase == 2) {
75  spreadView.snapTo(ApplicationManager.count - 1);
76  } else {
77  spreadView.phase = 0;
78  spreadView.contentX = -spreadView.shift;
79  ApplicationManager.focusApplication(appId);
80  }
81  }
82 
83  onApplicationRemoved: {
84  // Unless we're closing the app ourselves in the spread,
85  // lets make sure the spread doesn't mess up by the changing app list.
86  if (spreadView.closingIndex == -1) {
87  spreadView.phase = 0;
88  spreadView.contentX = -spreadView.shift;
89  focusTopMostApp();
90  }
91  }
92 
93  function focusTopMostApp() {
94  if (ApplicationManager.count > 0) {
95  var topmostApp = ApplicationManager.get(0);
96  ApplicationManager.focusApplication(topmostApp.appId);
97  }
98  }
99  }
100 
101  QtObject {
102  id: priv
103 
104  property string focusedAppId: ApplicationManager.focusedApplicationId
105  property var focusedApplication: ApplicationManager.findApplication(focusedAppId)
106  property var focusedAppDelegate: null
107 
108  property real oldInverseProgress: 0
109  property bool animateX: true
110 
111  onFocusedAppIdChanged: focusedAppDelegate = spreadRepeater.itemAt(0);
112 
113  function indexOf(appId) {
114  for (var i = 0; i < ApplicationManager.count; i++) {
115  if (ApplicationManager.get(i).appId == appId) {
116  return i;
117  }
118  }
119  return -1;
120  }
121 
122  }
123 
124  Flickables.Flickable {
125  id: spreadView
126  objectName: "spreadView"
127  anchors.fill: parent
128  interactive: (spreadDragArea.status == DirectionalDragArea.Recognized || phase > 1)
129  && draggedDelegateCount === 0
130  contentWidth: spreadRow.width - shift
131  contentX: -shift
132 
133  // This indicates when the spreadView is active. That means, all the animations
134  // are activated and tiles need to line up for the spread.
135  readonly property bool active: shiftedContentX > 0 || spreadDragArea.status === DirectionalDragArea.Recognized
136 
137  // The flickable needs to fill the screen in order to get touch events all over.
138  // However, we don't want to the user to be able to scroll back all the way. For
139  // that, the beginning of the gesture starts with a negative value for contentX
140  // so the flickable wants to pull it into the view already. "shift" tunes the
141  // distance where to "lock" the content.
142  readonly property real shift: width / 2
143  readonly property real shiftedContentX: contentX + shift
144 
145  property int tileDistance: width / 4
146 
147  // Those markers mark the various positions in the spread (ratio to screen width from right to left):
148  // 0 - 1: following finger, snap back to the beginning on release
149  property real positionMarker1: 0.3
150  // 1 - 2: curved snapping movement, snap to app 1 on release
151  property real positionMarker2: 0.45
152  // 2 - 3: movement follows finger, snaps back to app 1 on release
153  property real positionMarker3: 0.6
154  // passing 3, we detach movement from the finger and snap to 4
155  property real positionMarker4: 0.9
156 
157  // This is where the first app snaps to when bringing it in from the right edge.
158  property real snapPosition: 0.75
159 
160  // Phase of the animation:
161  // 0: Starting from right edge, a new app (index 1) comes in from the right
162  // 1: The app has reached the first snap position.
163  // 2: The list is dragged further and snaps into the spread view when entering phase 2
164  property int phase: 0
165 
166  property int selectedIndex: -1
167  property int draggedDelegateCount: 0
168  property int closingIndex: -1
169 
170  property bool focusChanging: false
171 
172  onShiftedContentXChanged: {
173  switch (phase) {
174  case 0:
175  if (shiftedContentX > width * positionMarker2) {
176  phase = 1;
177  }
178  break;
179  case 1:
180  if (shiftedContentX < width * positionMarker2) {
181  phase = 0;
182  } else if (shiftedContentX >= width * positionMarker4) {
183  phase = 2;
184  }
185  break;
186  }
187  }
188 
189  function snap() {
190  if (shiftedContentX < positionMarker1 * width) {
191  snapAnimation.targetContentX = -shift;
192  snapAnimation.start();
193  } else if (shiftedContentX < positionMarker2 * width) {
194  snapTo(1);
195  } else if (shiftedContentX < positionMarker3 * width) {
196  snapTo(1);
197  } else if (phase < 2){
198  // Add 1 pixel to make sure we definitely hit positionMarker4 even with rounding errors of the animation.
199  snapAnimation.targetContentX = width * positionMarker4 + 1 - shift;
200  snapAnimation.start();
201  }
202  }
203  function snapTo(index) {
204  if (ApplicationManager.count <= index) {
205  // In case we're trying to snap to some non existing app, lets snap back to the first one
206  index = 0;
207  }
208  spreadView.selectedIndex = index;
209  // If we're not in full spread mode yet, always unwind to start pos
210  // otherwise unwind up to progress 0 of the selected index
211  if (spreadView.phase < 2) {
212  snapAnimation.targetContentX = -shift;
213  } else {
214  snapAnimation.targetContentX = -shift + index * spreadView.tileDistance;
215  }
216  snapAnimation.start();
217  }
218 
219  // In case the ApplicationManager already holds an app when starting up we're missing animations
220  // Make sure we end up in the same state
221  Component.onCompleted: {
222  spreadView.contentX = -spreadView.shift
223  }
224 
225  SequentialAnimation {
226  id: snapAnimation
227  property int targetContentX: -spreadView.shift
228 
229  UbuntuNumberAnimation {
230  target: spreadView
231  property: "contentX"
232  to: snapAnimation.targetContentX
233  duration: UbuntuAnimation.FastDuration
234  }
235 
236  ScriptAction {
237  script: {
238  if (spreadView.selectedIndex >= 0) {
239  ApplicationManager.focusApplication(ApplicationManager.get(spreadView.selectedIndex).appId);
240 
241  spreadView.selectedIndex = -1;
242  spreadView.phase = 0;
243  spreadView.contentX = -spreadView.shift;
244  }
245  }
246  }
247  }
248 
249  Item {
250  id: spreadRow
251  // This width controls how much the spread can be flicked left/right. It's composed of:
252  // tileDistance * app count (with a minimum of 3 apps, in order to also allow moving 1 and 2 apps a bit)
253  // + some constant value (still scales with the screen width) which looks good and somewhat fills the screen
254  width: Math.max(3, ApplicationManager.count) * spreadView.tileDistance + (spreadView.width - spreadView.tileDistance) * 1.5
255  Behavior on width {
256  enabled: spreadView.closingIndex >= 0
257  UbuntuNumberAnimation {}
258  }
259  onWidthChanged: {
260  if (spreadView.closingIndex >= 0) {
261  spreadView.contentX = Math.min(spreadView.contentX, width - spreadView.width - spreadView.shift);
262  }
263  }
264 
265  x: spreadView.contentX
266 
267  Repeater {
268  id: spreadRepeater
269  model: ApplicationManager
270  delegate: TransformedSpreadDelegate {
271  id: appDelegate
272  objectName: "appDelegate" + index
273  startAngle: 45
274  endAngle: 5
275  startScale: 1.1
276  endScale: 0.7
277  startDistance: spreadView.tileDistance
278  endDistance: units.gu(.5)
279  width: spreadView.width
280  height: spreadView.height
281  selected: spreadView.selectedIndex == index
282  otherSelected: spreadView.selectedIndex >= 0 && !selected
283  interactive: !spreadView.interactive && spreadView.phase === 0
284  && spreadView.shiftedContentX === 0 && root.interactive && index === 0
285  swipeToCloseEnabled: spreadView.interactive
286  maximizedAppTopMargin: root.maximizedAppTopMargin
287  dropShadow: spreadView.active ||
288  (priv.focusedAppDelegate && priv.focusedAppDelegate.x !== 0)
289 
290  readonly property bool isDash: model.appId == "unity8-dash"
291 
292  z: isDash && !spreadView.active ? -1 : behavioredIndex
293 
294  x: {
295  // focused app is always positioned at 0 except when following left edge drag
296  if (index == 0) {
297  if (!isDash && root.inverseProgress > 0) {
298  return root.inverseProgress;
299  }
300  return 0;
301  }
302  if (isDash && !spreadView.active && !spreadDragArea.dragging) {
303  return 0;
304  }
305 
306  // Otherwise line up for the spread
307  return spreadView.width + (index - 1) * spreadView.tileDistance;
308  }
309 
310  application: ApplicationManager.get(index)
311  closeable: !isDash
312 
313  property real behavioredIndex: index
314  Behavior on behavioredIndex {
315  enabled: spreadView.closingIndex >= 0
316  UbuntuNumberAnimation {
317  id: appXAnimation
318  onRunningChanged: {
319  if (!running) {
320  spreadView.closingIndex = -1;
321  }
322  }
323  }
324  }
325 
326  Behavior on x {
327  enabled: root.spreadEnabled &&
328  !spreadView.active &&
329  !snapAnimation.running &&
330  priv.animateX
331  UbuntuNumberAnimation {
332  duration: UbuntuAnimation.BriskDuration
333  onRunningChanged: {
334  if (!running && root.inverseProgress == 0) {
335  spreadView.focusChanging = false;
336  }
337  }
338  }
339  }
340 
341  // Each tile has a different progress value running from 0 to 1.
342  // 0: means the tile is at the right edge.
343  // 1: means the tile has finished the main animation towards the left edge.
344  // >1: after the main animation has finished, tiles will continue to move very slowly to the left
345  progress: {
346  var tileProgress = (spreadView.shiftedContentX - behavioredIndex * spreadView.tileDistance) / spreadView.width;
347  // Tile 1 needs to move directly from the beginning...
348  if (behavioredIndex == 1 && spreadView.phase < 2) {
349  tileProgress += spreadView.tileDistance / spreadView.width;
350  }
351  // Limiting progress to ~0 and 1.7 to avoid binding calculations when tiles are not
352  // visible.
353  // < 0 : The tile is outside the screen on the right
354  // > 1.7: The tile is *very* close to the left edge and covered by other tiles now.
355  // Using 0.0001 to differentiate when a tile should still be visible (==0)
356  // or we can hide it (< 0)
357  tileProgress = Math.max(-0.0001, Math.min(1.7, tileProgress));
358  return tileProgress;
359  }
360 
361  // This mostly is the same as progress, just adds the snapping to phase 1 for tiles 0 and 1
362  animatedProgress: {
363  if (spreadView.phase == 0 && index < 2) {
364  if (progress < spreadView.positionMarker1) {
365  return progress;
366  } else if (progress < spreadView.positionMarker1 + snappingCurve.period){
367  return spreadView.positionMarker1 + snappingCurve.value * 3;
368  } else {
369  return spreadView.positionMarker2;
370  }
371  }
372  return progress;
373  }
374 
375  // Hiding tiles when their progress is negative or reached the maximum
376  visible: (progress >= 0 && progress < 1.7) ||
377  (isDash && priv.focusedAppDelegate.x !== 0)
378 
379  EasingCurve {
380  id: snappingCurve
381  type: EasingCurve.Linear
382  period: 0.05
383  progress: appDelegate.progress - spreadView.positionMarker1
384  }
385 
386  Binding {
387  target: appDelegate
388  property: "orientation"
389  when: appDelegate.interactive
390  value: root.orientation
391  }
392 
393  onClicked: {
394  if (spreadView.phase == 2) {
395  if (ApplicationManager.focusedApplicationId == ApplicationManager.get(index).appId) {
396  spreadView.snapTo(index);
397  } else {
398  ApplicationManager.requestFocusApplication(ApplicationManager.get(index).appId);
399  }
400  }
401  }
402 
403  onDraggedChanged: {
404  if (dragged) {
405  spreadView.draggedDelegateCount++;
406  } else {
407  spreadView.draggedDelegateCount--;
408  }
409  }
410 
411  onClosed: {
412  spreadView.closingIndex = index;
413  ApplicationManager.stopApplication(ApplicationManager.get(index).appId);
414  }
415  }
416  }
417  }
418  }
419 
420  EdgeDragArea {
421  id: spreadDragArea
422  objectName: "spreadDragArea"
423  direction: Direction.Leftwards
424  enabled: spreadView.phase != 2 && root.spreadEnabled
425 
426  anchors { top: parent.top; right: parent.right; bottom: parent.bottom }
427  width: root.dragAreaWidth
428 
429  // Sitting at the right edge of the screen, this EdgeDragArea directly controls the spreadView when
430  // attachedToView is true. When the finger movement passes positionMarker3 we detach it from the
431  // spreadView and make the spreadView snap to positionMarker4.
432  property bool attachedToView: true
433 
434  property var gesturePoints: new Array()
435 
436  onTouchXChanged: {
437  if (!dragging) {
438  // Initial touch. Let's reset the spreadView to the starting position.
439  spreadView.phase = 0;
440  spreadView.contentX = -spreadView.shift;
441  }
442  if (dragging && status == DirectionalDragArea.Recognized && attachedToView) {
443  // Gesture recognized. Let's move the spreadView with the finger
444  var finalX = Math.min(touchX + width, width);
445  spreadView.contentX = -finalX + spreadDragArea.width - spreadView.shift;
446  }
447  if (attachedToView && spreadView.shiftedContentX >= spreadView.width * spreadView.positionMarker3) {
448  // We passed positionMarker3. Detach from spreadView and snap it.
449  attachedToView = false;
450  spreadView.snap();
451  }
452  gesturePoints.push(touchX);
453  }
454 
455  property int previousStatus: -1
456  property int currentStatus: DirectionalDragArea.WaitingForTouch
457 
458  onStatusChanged: {
459  previousStatus = currentStatus;
460  currentStatus = status;
461 
462  if (status == DirectionalDragArea.Recognized) {
463  attachedToView = true;
464  }
465  }
466 
467  onDraggingChanged: {
468  if (dragging) {
469  // A potential edge-drag gesture has started. Start recording it
470  gesturePoints = [];
471  return;
472  }
473 
474  // Ok. The user released. Find out if it was a one-way movement.
475  var oneWayFlick = true;
476  var smallestX = spreadDragArea.width;
477  for (var i = 0; i < gesturePoints.length; i++) {
478  if (gesturePoints[i] >= smallestX) {
479  oneWayFlick = false;
480  break;
481  }
482  smallestX = gesturePoints[i];
483  }
484  gesturePoints = [];
485 
486  if (previousStatus == DirectionalDragArea.Recognized &&
487  oneWayFlick && spreadView.shiftedContentX > units.gu(2) &&
488  spreadView.shiftedContentX < spreadView.positionMarker1 * spreadView.width) {
489  // If it was a short one-way movement, do the Alt+Tab switch
490  // no matter if we didn't cross positionMarker1 yet.
491  spreadView.snapTo(1);
492  } else if (!dragging && attachedToView) {
493  // otherwise snap to the closest snap position we can find
494  // (might be back to start, to app 1 or to spread)
495  spreadView.snap();
496  }
497  }
498  }
499 }