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