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