Unity 8
 All Classes Functions Properties
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 Item {
25  id: root
26 
27  // Controls to be set from outside
28  property bool shown: false
29  property bool moving: false
30  property int dragAreaWidth
31 
32  // State information propagated to the outside
33  readonly property bool painting: mainScreenshotImage.visible || fadeInScreenshotImage.visible || appSplash.visible || spreadView.visible
34  property bool fullscreen: priv.focusedApplication ? priv.focusedApplication.fullscreen : false
35  property bool locked: spreadView.visible
36 
37  // Not used for PhoneStage, only useful for SideStage and similar
38  property bool overlayMode: false
39  property int overlayWidth: 0
40 
41  function select(appId) {
42  spreadView.snapTo(priv.indexOf(appId))
43  }
44 
45  onMovingChanged: {
46  if (moving) {
47  if (ApplicationManager.focusedApplicationId) {
48  priv.requestNewScreenshot();
49  } else {
50  mainScreenshotImage.anchors.leftMargin = 0;
51  mainScreenshotImage.source = ApplicationManager.get(0).screenshot;
52  mainScreenshotImage.visible = true;
53  }
54  } else {
55  mainScreenshotImage.visible = false;
56  }
57  }
58 
59  Connections {
60  target: ApplicationManager
61 
62  onFocusRequested: {
63  if (spreadView.visible) {
64  spreadView.snapTo(priv.indexOf(appId));
65  } else {
66  priv.switchToApp(appId);
67  }
68  }
69 
70  onFocusedApplicationIdChanged: {
71  if (ApplicationManager.focusedApplicationId.length > 0) {
72  if (priv.secondApplicationStarting || priv.applicationStarting) {
73  appSplashTimer.restart();
74  } else {
75  var application = priv.focusedApplication;
76  root.fullscreen = application.fullscreen;
77  mainScreenshotImage.source = application.screenshot;
78  }
79  } else {
80  spreadView.selectedIndex = -1;
81  spreadView.phase = 0;
82  spreadView.contentX = -spreadView.shift;
83  }
84  }
85 
86  onApplicationAdded: {
87  if (!priv.focusedApplication) {
88  mainScreenshotImage.source = "";
89  mainScreenshotImage.visible = false;
90  priv.applicationStarting = true;
91  } else {
92  mainScreenshotImage.source = "";
93  priv.newFocusedAppId = appId;
94  priv.secondApplicationStarting = true;
95  priv.requestNewScreenshot();
96  }
97 
98  if (spreadView.visible) {
99  spreadView.snapTo(0);
100  }
101  }
102 
103  onApplicationRemoved: {
104  if (ApplicationManager.count == 0) {
105  mainScreenshotImage.source = ""
106  mainScreenshotImage.visible = false;
107  } else {
108  mainScreenshotImage.source = ApplicationManager.get(0).screenshot;
109  }
110  }
111  }
112 
113  QtObject {
114  id: priv
115 
116  property string focusedAppId: ApplicationManager.focusedApplicationId
117  property var focusedApplication: ApplicationManager.findApplication(focusedAppId)
118  property url focusedScreenshot: focusedApplication ? focusedApplication.screenshot : ""
119 
120  property bool waitingForScreenshot: false
121 
122  property bool applicationStarting: false
123  property bool secondApplicationStarting: false
124 
125  property string newFocusedAppId
126 
127  onFocusedScreenshotChanged: {
128  if (root.moving && priv.waitingForScreenshot) {
129  mainScreenshotImage.anchors.leftMargin = 0;
130  mainScreenshotImage.source = priv.focusedScreenshot
131  mainScreenshotImage.visible = true;
132  } else if (priv.secondApplicationStarting && priv.waitingForScreenshot) {
133  applicationSwitchingAnimation.start();
134  }
135  waitingForScreenshot = false;
136  }
137 
138  function requestNewScreenshot() {
139  waitingForScreenshot = true;
140  ApplicationManager.updateScreenshot(focusedAppId);
141  }
142 
143  function switchToApp(appId) {
144  if (priv.focusedAppId) {
145  priv.newFocusedAppId = appId;
146  root.fullscreen = ApplicationManager.findApplication(appId).fullscreen;
147  applicationSwitchingAnimation.start();
148  } else {
149  ApplicationManager.focusApplication(appId);
150  }
151  }
152 
153  function indexOf(appId) {
154  for (var i = 0; i < ApplicationManager.count; i++) {
155  if (ApplicationManager.get(i).appId == appId) {
156  return i;
157  }
158  }
159  return -1;
160  }
161 
162  }
163 
164  // FIXME: the signal connections seems to get lost.
165  Connections {
166  target: priv.focusedApplication
167  onScreenshotChanged: priv.focusedScreenshot = priv.focusedApplication.screenshot
168  }
169  Binding {
170  target: root
171  property: "fullscreen"
172  value: priv.focusedApplication ? priv.focusedApplication.fullscreen : false
173  }
174 
175  Timer {
176  id: appSplashTimer
177  // FIXME: We really need to show something meaningful in the app surface instead of guessing
178  // when it might be ready
179  interval: 500
180  repeat: false
181  onTriggered: {
182  priv.applicationStarting = false;
183  priv.secondApplicationStarting = false;
184  }
185  }
186 
187  SequentialAnimation {
188  id: applicationSwitchingAnimation
189  // setup
190  PropertyAction { target: mainScreenshotImage; property: "anchors.leftMargin"; value: 0 }
191  PropertyAction { target: mainScreenshotImage; property: "source"; value: priv.focusedScreenshot }
192  PropertyAction { targets: [mainScreenshotImage, fadeInScreenshotImage]; property: "visible"; value: true }
193  PropertyAction { target: fadeInScreenshotImage; property: "source"; value: {
194  var newFocusedApp = ApplicationManager.findApplication(priv.newFocusedAppId);
195  return newFocusedApp ? newFocusedApp.screenshot : "" }
196  }
197  PropertyAction { target: fadeInScreenshotImage; property: "opacity"; value: 0 }
198  PropertyAction { target: fadeInScreenshotImage; property: "scale"; value: .8 }
199 
200 
201  // The actual animation
202  ParallelAnimation {
203  UbuntuNumberAnimation { target: mainScreenshotImage; property: "anchors.leftMargin"; to: root.width; duration: UbuntuAnimation.SlowDuration }
204  UbuntuNumberAnimation { target: fadeInScreenshotImage; properties: "opacity,scale"; to: 1; duration: UbuntuAnimation.SlowDuration }
205  }
206 
207  // restore stuff
208  ScriptAction { script: ApplicationManager.focusApplication(priv.newFocusedAppId); }
209  PropertyAction { target: fadeInScreenshotImage; property: "visible"; value: false }
210  PropertyAction { target: mainScreenshotImage; property: "visible"; value: false }
211  }
212 
213  // FIXME: Drop this and make the imageprovider show a splashscreen instead
214  Rectangle {
215  id: appSplash2
216  anchors.fill: parent
217  color: "black"
218  visible: priv.secondApplicationStarting
219  }
220  Image {
221  id: fadeInScreenshotImage
222  anchors { left: parent.left; bottom: parent.bottom }
223  width: parent.width
224  scale: .7
225  visible: false
226  }
227 
228  Rectangle {
229  id: appSplash
230  anchors.fill: parent
231  color: "black"
232  visible: priv.applicationStarting
233 
234  WaitingDots {
235  }
236  }
237  Image {
238  id: mainScreenshotImage
239  anchors { left: parent.left; bottom: parent.bottom }
240  width: parent.width
241  visible: false
242  }
243 
244  EdgeDragArea {
245  id: spreadDragArea
246  direction: Direction.Leftwards
247  enabled: ApplicationManager.count > 1 && spreadView.phase != 2
248 
249  anchors { top: parent.top; right: parent.right; bottom: parent.bottom }
250  width: root.dragAreaWidth
251 
252  // Sitting at the right edge of the screen, this EdgeDragArea directly controls the spreadView when
253  // attachedToView is true. When the finger movement passes positionMarker3 we detach it from the
254  // spreadView and make the spreadView snap to positionMarker4.
255  property bool attachedToView: true
256 
257  property var gesturePoints: new Array()
258 
259  onTouchXChanged: {
260  if (!dragging && !priv.waitingForScreenshot) {
261  // Initial touch. Let's update the screenshot and reset the spreadView to the starting position.
262  priv.requestNewScreenshot();
263  spreadView.phase = 0;
264  spreadView.contentX = -spreadView.shift;
265  }
266  if (dragging && attachedToView) {
267  // Gesture recognized. Let's move the spreadView with the finger
268  spreadView.contentX = -touchX - spreadView.shift;
269  }
270  if (attachedToView && spreadView.shiftedContentX >= spreadView.width * spreadView.positionMarker3) {
271  // We passed positionMarker3. Detach from spreadView and snap it.
272  attachedToView = false;
273  spreadView.snap();
274  }
275  gesturePoints.push(touchX);
276  }
277 
278  onStatusChanged: {
279  if (status == DirectionalDragArea.Recognized) {
280  attachedToView = true;
281  }
282  }
283 
284  onDraggingChanged: {
285  if (dragging) {
286  // Gesture recognized. Start recording this gesture
287  gesturePoints = [];
288  return;
289  }
290 
291  // Ok. The user released. Find out if it was a one-way movement.
292  var oneWayFlick = true;
293  var smallestX = spreadDragArea.width;
294  for (var i = 0; i < gesturePoints.length; i++) {
295  if (gesturePoints[i] >= smallestX) {
296  oneWayFlick = false;
297  break;
298  }
299  smallestX = gesturePoints[i];
300  }
301  gesturePoints = [];
302 
303  if (oneWayFlick && spreadView.shiftedContentX > units.gu(2) &&
304  spreadView.shiftedContentX < spreadView.positionMarker1 * spreadView.width) {
305  // If it was a short one-way movement, do the Alt+Tab switch
306  // no matter if we didn't cross positionMarker1 yet.
307  spreadView.snapTo(1);
308  } else if (!dragging && attachedToView) {
309  // otherwise snap to the closest snap position we can find
310  // (might be back to start, to app 1 or to spread)
311  spreadView.snap();
312  }
313  }
314  }
315 
316  Rectangle {
317  id: coverFlipBackground
318  anchors.fill: parent
319  color: "black"
320  visible: spreadView.visible
321  }
322 
323  InputFilterArea {
324  anchors.fill: root
325  blockInput: spreadView.visible
326  }
327 
328  Flickable {
329  id: spreadView
330  objectName: "spreadView"
331  anchors.fill: parent
332  visible: spreadDragArea.status == DirectionalDragArea.Recognized || phase > 1 || snapAnimation.running
333  contentWidth: spreadRow.width - shift
334  contentX: -shift
335 
336  // The flickable needs to fill the screen in order to get touch events all over.
337  // However, we don't want to the user to be able to scroll back all the way. For
338  // that, the beginning of the gesture starts with a negative value for contentX
339  // so the flickable wants to pull it into the view already. "shift" tunes the
340  // distance where to "lock" the content.
341  property real shift: width / 2
342  property real shiftedContentX: contentX + shift
343 
344  property int tileDistance: width / 4
345 
346  // Those markers mark the various positions in the spread (ratio to screen width from right to left):
347  // 0 - 1: following finger, snap back to the beginning on release
348  property real positionMarker1: 0.3
349  // 1 - 2: curved snapping movement, snap to app 1 on release
350  property real positionMarker2: 0.45
351  // 2 - 3: movement follows finger, snaps back to app 1 on release
352  property real positionMarker3: 0.6
353  // passing 3, we detach movement from the finger and snap to 4
354  property real positionMarker4: 0.9
355 
356  // This is where the first app snaps to when bringing it in from the right edge.
357  property real snapPosition: 0.75
358 
359  // Phase of the animation:
360  // 0: Starting from right edge, a new app (index 1) comes in from the right
361  // 1: The app has reached the first snap position.
362  // 2: The list is dragged further and snaps into the spread view when entering phase 2
363  property int phase: 0
364 
365  property int selectedIndex: -1
366 
367  onShiftedContentXChanged: {
368  switch (phase) {
369  case 0:
370  if (shiftedContentX > width * positionMarker2) {
371  phase = 1;
372  }
373  break;
374  case 1:
375  if (shiftedContentX < width * positionMarker2) {
376  phase = 0;
377  } else if (shiftedContentX >= width * positionMarker4) {
378  phase = 2;
379  }
380  break;
381  }
382  }
383 
384  function snap() {
385  if (shiftedContentX < positionMarker1 * width) {
386  snapAnimation.targetContentX = -shift;
387  snapAnimation.start();
388  } else if (shiftedContentX < positionMarker2 * width) {
389  snapTo(1)
390  } else if (shiftedContentX < positionMarker3 * width) {
391  snapTo(1)
392  } else if (phase < 2){
393  // Add 1 pixel to make sure we definitely hit positionMarker4 even with rounding errors of the animation.
394  snapAnimation.targetContentX = width * positionMarker4 + 1 - shift;
395  snapAnimation.start();
396  }
397  }
398  function snapTo(index) {
399  spreadView.selectedIndex = index;
400  root.fullscreen = ApplicationManager.get(index).fullscreen;
401  snapAnimation.targetContentX = -shift;
402  snapAnimation.start();
403  }
404 
405  SequentialAnimation {
406  id: snapAnimation
407  property int targetContentX: -spreadView.shift
408 
409  UbuntuNumberAnimation {
410  target: spreadView
411  property: "contentX"
412  to: snapAnimation.targetContentX
413  duration: UbuntuAnimation.FastDuration
414  }
415 
416  ScriptAction {
417  script: {
418  if (spreadView.selectedIndex >= 0) {
419  ApplicationManager.focusApplication(ApplicationManager.get(spreadView.selectedIndex).appId);
420  spreadView.selectedIndex = -1
421  spreadView.phase = 0;
422  spreadView.contentX = -spreadView.shift;
423  }
424  }
425  }
426  }
427 
428  Item {
429  id: spreadRow
430  // This width controls how much the spread can be flicked left/right. It's composed of:
431  // tileDistance * app count (with a minimum of 3 apps, in order to also allow moving 1 and 2 apps a bit)
432  // + some constant value (still scales with the screen width) which looks good and somewhat fills the screen
433  width: Math.max(3, ApplicationManager.count) * spreadView.tileDistance + (spreadView.width - spreadView.tileDistance) * 1.5
434 
435  x: spreadView.contentX
436 
437  Repeater {
438  id: spreadRepeater
439  model: ApplicationManager
440  delegate: TransformedSpreadDelegate {
441  id: appDelegate
442  objectName: "appDelegate" + index
443  startAngle: 45
444  endAngle: 5
445  startScale: 1.1
446  endScale: 0.7
447  startDistance: spreadView.tileDistance
448  endDistance: units.gu(.5)
449  width: spreadView.width
450  height: spreadView.height
451  selected: spreadView.selectedIndex == index
452  otherSelected: spreadView.selectedIndex >= 0 && !selected
453 
454  z: index
455  x: index == 0 ? 0 : spreadView.width + (index - 1) * spreadView.tileDistance
456 
457  // Each tile has a different progress value running from 0 to 1.
458  // A progress value of 0 means the tile is at the right edge. 1 means the tile has reched the left edge.
459  progress: {
460  var tileProgress = (spreadView.shiftedContentX - index * spreadView.tileDistance) / spreadView.width;
461  // Tile 1 needs to move directly from the beginning...
462  if (index == 1 && spreadView.phase < 2) {
463  tileProgress += spreadView.tileDistance / spreadView.width;
464  }
465  return tileProgress;
466  }
467 
468  // This mostly is the same as progress, just adds the snapping to phase 1 for tiles 0 and 1
469  animatedProgress: {
470  if (spreadView.phase == 0 && index < 2) {
471  if (progress < spreadView.positionMarker1) {
472  return progress;
473  } else if (progress < spreadView.positionMarker1 + snappingCurve.period){
474  return spreadView.positionMarker1 + snappingCurve.value * 3;
475  } else {
476  return spreadView.positionMarker2;
477  }
478  }
479  return progress;
480  }
481 
482  EasingCurve {
483  id: snappingCurve
484  type: EasingCurve.OutQuad
485  period: 0.05
486  progress: appDelegate.progress - spreadView.positionMarker1
487  }
488 
489  onClicked: {
490  if (spreadView.phase == 2) {
491  if (ApplicationManager.focusedApplicationId == ApplicationManager.get(index).appId) {
492  spreadView.snapTo(index);
493  } else {
494  ApplicationManager.requestFocusApplication(ApplicationManager.get(index).appId);
495  }
496  }
497  }
498  }
499  }
500  }
501  }
502 }
The EasingCurve class.
Definition: easingcurve.h:36