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