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