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: "white"
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: "white"
232  visible: priv.applicationStarting
233  }
234  Image {
235  id: mainScreenshotImage
236  anchors { left: parent.left; bottom: parent.bottom }
237  width: parent.width
238  visible: false
239  }
240 
241  EdgeDragArea {
242  id: spreadDragArea
243  direction: Direction.Leftwards
244  enabled: ApplicationManager.count > 1 && spreadView.phase != 2
245 
246  anchors { top: parent.top; right: parent.right; bottom: parent.bottom }
247  width: root.dragAreaWidth
248 
249  // Sitting at the right edge of the screen, this EdgeDragArea directly controls the spreadView when
250  // attachedToView is true. When the finger movement passes positionMarker3 we detach it from the
251  // spreadView and make the spreadView snap to positionMarker4.
252  property bool attachedToView: true
253 
254  property var gesturePoints: new Array()
255 
256  onTouchXChanged: {
257  if (!dragging && !priv.waitingForScreenshot) {
258  // Initial touch. Let's update the screenshot and reset the spreadView to the starting position.
259  priv.requestNewScreenshot();
260  spreadView.phase = 0;
261  spreadView.contentX = -spreadView.shift;
262  }
263  if (dragging && attachedToView) {
264  // Gesture recognized. Let's move the spreadView with the finger
265  spreadView.contentX = -touchX - spreadView.shift;
266  }
267  if (attachedToView && spreadView.shiftedContentX >= spreadView.width * spreadView.positionMarker3) {
268  // We passed positionMarker3. Detach from spreadView and snap it.
269  attachedToView = false;
270  spreadView.snap();
271  }
272  gesturePoints.push(touchX);
273  }
274 
275  onStatusChanged: {
276  if (status == DirectionalDragArea.Recognized) {
277  attachedToView = true;
278  }
279  }
280 
281  onDraggingChanged: {
282  if (dragging) {
283  // Gesture recognized. Start recording this gesture
284  gesturePoints = [];
285  return;
286  }
287 
288  // Ok. The user released. Find out if it was a one-way movement.
289  var oneWayFlick = true;
290  var smallestX = spreadDragArea.width;
291  for (var i = 0; i < gesturePoints.length; i++) {
292  if (gesturePoints[i] >= smallestX) {
293  oneWayFlick = false;
294  break;
295  }
296  smallestX = gesturePoints[i];
297  }
298  gesturePoints = [];
299 
300  if (oneWayFlick && spreadView.shiftedContentX > units.gu(2) &&
301  spreadView.shiftedContentX < spreadView.positionMarker1 * spreadView.width) {
302  // If it was a short one-way movement, do the Alt+Tab switch
303  // no matter if we didn't cross positionMarker1 yet.
304  spreadView.snapTo(1);
305  } else if (!dragging && attachedToView) {
306  // otherwise snap to the closest snap position we can find
307  // (might be back to start, to app 1 or to spread)
308  spreadView.snap();
309  }
310  }
311  }
312 
313  Rectangle {
314  id: coverFlipBackground
315  anchors.fill: parent
316  color: "black"
317  visible: spreadView.visible
318  }
319 
320  InputFilterArea {
321  anchors.fill: root
322  blockInput: spreadView.visible
323  }
324 
325  Flickable {
326  id: spreadView
327  objectName: "spreadView"
328  anchors.fill: parent
329  visible: spreadDragArea.status == DirectionalDragArea.Recognized || phase > 1 || snapAnimation.running
330  contentWidth: spreadRow.width - shift
331  contentX: -shift
332 
333  // The flickable needs to fill the screen in order to get touch events all over.
334  // However, we don't want to the user to be able to scroll back all the way. For
335  // that, the beginning of the gesture starts with a negative value for contentX
336  // so the flickable wants to pull it into the view already. "shift" tunes the
337  // distance where to "lock" the content.
338  property real shift: width / 2
339  property real shiftedContentX: contentX + shift
340 
341  property int tileDistance: width / 4
342 
343  // Those markers mark the various positions in the spread (ratio to screen width from right to left):
344  // 0 - 1: following finger, snap back to the beginning on release
345  property real positionMarker1: 0.3
346  // 1 - 2: curved snapping movement, snap to app 1 on release
347  property real positionMarker2: 0.45
348  // 2 - 3: movement follows finger, snaps back to app 1 on release
349  property real positionMarker3: 0.6
350  // passing 3, we detach movement from the finger and snap to 4
351  property real positionMarker4: 0.9
352 
353  // This is where the first app snaps to when bringing it in from the right edge.
354  property real snapPosition: 0.75
355 
356  // Phase of the animation:
357  // 0: Starting from right edge, a new app (index 1) comes in from the right
358  // 1: The app has reached the first snap position.
359  // 2: The list is dragged further and snaps into the spread view when entering phase 2
360  property int phase: 0
361 
362  property int selectedIndex: -1
363 
364  onShiftedContentXChanged: {
365  switch (phase) {
366  case 0:
367  if (shiftedContentX > width * positionMarker2) {
368  phase = 1;
369  }
370  break;
371  case 1:
372  if (shiftedContentX < width * positionMarker2) {
373  phase = 0;
374  } else if (shiftedContentX >= width * positionMarker4) {
375  phase = 2;
376  }
377  break;
378  }
379  }
380 
381  function snap() {
382  if (shiftedContentX < positionMarker1 * width) {
383  snapAnimation.targetContentX = -shift;
384  snapAnimation.start();
385  } else if (shiftedContentX < positionMarker2 * width) {
386  snapTo(1)
387  } else if (shiftedContentX < positionMarker3 * width) {
388  snapTo(1)
389  } else if (phase < 2){
390  // Add 1 pixel to make sure we definitely hit positionMarker4 even with rounding errors of the animation.
391  snapAnimation.targetContentX = width * positionMarker4 + 1 - shift;
392  snapAnimation.start();
393  }
394  }
395  function snapTo(index) {
396  spreadView.selectedIndex = index;
397  root.fullscreen = ApplicationManager.get(index).fullscreen;
398  snapAnimation.targetContentX = -shift;
399  snapAnimation.start();
400  }
401 
402  SequentialAnimation {
403  id: snapAnimation
404  property int targetContentX: -spreadView.shift
405 
406  UbuntuNumberAnimation {
407  target: spreadView
408  property: "contentX"
409  to: snapAnimation.targetContentX
410  duration: UbuntuAnimation.FastDuration
411  }
412 
413  ScriptAction {
414  script: {
415  if (spreadView.selectedIndex >= 0) {
416  ApplicationManager.focusApplication(ApplicationManager.get(spreadView.selectedIndex).appId);
417  spreadView.selectedIndex = -1
418  spreadView.phase = 0;
419  spreadView.contentX = -spreadView.shift;
420  }
421  }
422  }
423  }
424 
425  Item {
426  id: spreadRow
427  // This width controls how much the spread can be flicked left/right. It's composed of:
428  // tileDistance * app count (with a minimum of 3 apps, in order to also allow moving 1 and 2 apps a bit)
429  // + some constant value (still scales with the screen width) which looks good and somewhat fills the screen
430  width: Math.max(3, ApplicationManager.count) * spreadView.tileDistance + (spreadView.width - spreadView.tileDistance) * 1.5
431 
432  x: spreadView.contentX
433 
434  Repeater {
435  id: spreadRepeater
436  model: ApplicationManager
437  delegate: TransformedSpreadDelegate {
438  id: appDelegate
439  objectName: "appDelegate" + index
440  startAngle: 45
441  endAngle: 5
442  startScale: 1.1
443  endScale: 0.7
444  startDistance: spreadView.tileDistance
445  endDistance: units.gu(.5)
446  width: spreadView.width
447  height: spreadView.height
448  selected: spreadView.selectedIndex == index
449  otherSelected: spreadView.selectedIndex >= 0 && !selected
450 
451  z: index
452  x: index == 0 ? 0 : spreadView.width + (index - 1) * spreadView.tileDistance
453 
454  // Each tile has a different progress value running from 0 to 1.
455  // A progress value of 0 means the tile is at the right edge. 1 means the tile has reched the left edge.
456  progress: {
457  var tileProgress = (spreadView.shiftedContentX - index * spreadView.tileDistance) / spreadView.width;
458  // Tile 1 needs to move directly from the beginning...
459  if (index == 1 && spreadView.phase < 2) {
460  tileProgress += spreadView.tileDistance / spreadView.width;
461  }
462  return tileProgress;
463  }
464 
465  // This mostly is the same as progress, just adds the snapping to phase 1 for tiles 0 and 1
466  animatedProgress: {
467  if (spreadView.phase == 0 && index < 2) {
468  if (progress < spreadView.positionMarker1) {
469  return progress;
470  } else if (progress < spreadView.positionMarker1 + snappingCurve.period){
471  return spreadView.positionMarker1 + snappingCurve.value * 3;
472  } else {
473  return spreadView.positionMarker2;
474  }
475  }
476  return progress;
477  }
478 
479  EasingCurve {
480  id: snappingCurve
481  type: EasingCurve.OutQuad
482  period: 0.05
483  progress: appDelegate.progress - spreadView.positionMarker1
484  }
485 
486  onClicked: {
487  if (spreadView.phase == 2) {
488  if (ApplicationManager.focusedApplicationId == ApplicationManager.get(index).appId) {
489  spreadView.snapTo(index);
490  } else {
491  ApplicationManager.requestFocusApplication(ApplicationManager.get(index).appId);
492  }
493  }
494  }
495  }
496  }
497  }
498  }
499 }
The EasingCurve class.
Definition: easingcurve.h:36