on bf461a2 help sign in
unity8/Components/Stage.qml
554 lines 22 kB history raw
1/*
2 * Copyright (C) 2013 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 */
17import QtQuick 2.0
18import Unity.Application 0.1
19import Ubuntu.Components 0.1
20import Ubuntu.Gestures 0.1
22/*
23 Responsible for application switching.
25 Drag from the right edge to switch to the next application.
26 While being dragged, this component will show an animation for the user to
27 visualize that he's changing the stacking of applications (next/new app comes from
28 right edge and current/old app fades out).
29 */
30Showable {
31 id: stage
33 width: units.gu(40)
34 height: units.gu(71)
36 property int type: ApplicationInfo.MainStage
37 property var applicationManager
38 readonly property var applications:
39 type == ApplicationInfo.MainStage ? applicationManager.mainStageApplications
40 : applicationManager.sideStageApplications
41 property bool fullyShown: false
42 property bool fullyHidden: true
43 readonly property var focusedApplication:
44 type == ApplicationInfo.MainStage ? applicationManager.mainStageFocusedApplication
45 : applicationManager.sideStageFocusedApplication
46 property bool shouldUseScreenshots: true
48 /* Will be true whenever application screenshots are being shown.
49 i.e. during application window transitions */
50 readonly property bool usingScreenshots: {
51 if (newApplicationScreenshot.application != null && oldApplicationScreenshot.application != null) {
52 return newApplicationScreenshot.ready && oldApplicationScreenshot.ready;
53 } else {
54 return newApplicationScreenshot.ready || oldApplicationScreenshot.ready;
55 }
56 }
58 property bool rightEdgeEnabled: true
59 property bool switchingFromFullscreenToFullscreen: false
61 property alias rightEdgeDraggingAreaWidth: rightEdgeDraggingArea.width
63 // Y coordinate for non-fullscreen applications
64 property int normalApplicationY: 0
66 function activateApplication(desktopFile, addDelay) {
67 var application = applicationManager.getApplicationFromDesktopFile(desktopFile, stage.type);
68 if (application != null && application == stage.focusedApplication) {
69 stage.show();
70 return;
71 }
73 showStartingApplicationAnimation.prepare(application);
74 if (!stage.fullyShown) {
75 // if launching this app reveals the stage, make new application screenshot visible
76 newApplicationScreenshot.visible = true;
77 stage.show();
78 } else {
79 showStartingApplicationAnimation.start();
80 }
82 if (addDelay) {
83 stage.__startingApplicationDesktopFile = desktopFile;
84 }
85 }
87 /* Keep a reference to the focused application so that we can safely
88 unfocus it when the stage is not fully shown and refocus it when the stage
89 is fully shown again.
90 */
91 property var focusedApplicationWhenUsingScreenshots
92 onShouldUseScreenshotsChanged: {
93 if (showStartingApplicationAnimation.running || switchToApplicationAnimation.running) {
94 return;
95 }
97 if (shouldUseScreenshots) {
98 stage.__focusApplicationUsingScreenshots(stage.focusedApplication);
99 } else {
100 stage.__focusActualApplication(stage.focusedApplicationWhenUsingScreenshots);
101 }
102 }
105 property Item newApplicationScreenshot: ApplicationScreenshot {
106 parent: stage
107 width: stage.width
108 height: stage.height - y
109 }
110 property Item oldApplicationScreenshot: ApplicationScreenshot {
111 parent: stage
112 width: stage.width
113 height: stage.height - y
114 }
116 function __getApplicationIndex(application) {
117 if (!application)
118 return -1
119 for (var i = 0; i < stage.applications.count; i++ ) {
120 if (stage.applications.get(i).desktopFile == application.desktopFile) {
121 return i
122 }
123 }
124 return -1
125 }
128 property string __startingApplicationDesktopFile
130 Timer {
131 id: delayedHideScreenshots
132 interval: 500 // on Galaxy Nexus, Telephony app is slowest application to draw a frame so needs this delay
133 onTriggered: {
134 __hideScreenshots();
135 }
136 }
138 function __hideScreenshots() {
139 newApplicationScreenshot.clearApplication();
140 oldApplicationScreenshot.clearApplication();
141 newApplicationScreenshot.visible = false;
142 oldApplicationScreenshot.visible = false;
143 }
145 function __focusApplication(application) {
146 if (!application) return
147 if (shouldUseScreenshots) {
148 stage.__focusApplicationUsingScreenshots(application);
149 } else {
150 stage.__focusActualApplication(application);
151 }
152 }
155 function __focusActualApplication(application) {
156 if (application == null) {
157 return;
158 }
160 if (stage.focusedApplication == application) {
161 /* FIXME: this case is reached when showing the sidestage, we
162 need to refocus the side stage app in order to show it.
163 FIXME: this case is reached when doing a right edge swipe
164 while in the dash to bring back the stage.
165 FIXME: this case is reached when showStartingApplicationAnimation
166 finishes after the newly started application has been focused
167 automatically by the ApplicationManager.
168 */
169 if (stage.type == ApplicationInfo.SideStage) {
170 applicationManager.focusApplication(application);
171 }
172 if (!delayedHideScreenshots.running) {
173 stage.__hideScreenshots();
174 }
175 } else {
176 /* FIXME: calling ApplicationManager::focusApplication does not focus
177 the application immediately nor show the application's surface
178 immediately. Delay hiding the screenshots until it has done so.
179 */
180 delayedHideScreenshots.stop();
181 applicationManager.focusApplication(application);
182 }
183 stage.focusedApplicationWhenUsingScreenshots = null;
184 }
186 function __focusApplicationUsingScreenshots(application) {
187 delayedHideScreenshots.stop();
188 if (application == null) {
189 return;
190 }
192 if (newApplicationScreenshot.application != null) {
193 stage.focusedApplicationWhenUsingScreenshots = application;
194 return;
195 }
197 newApplicationScreenshot.setApplication(application);
198 newApplicationScreenshot.scheduleUpdate();
199 newApplicationScreenshot.opacity = 1.0;
200 newApplicationScreenshot.x = 0.0;
201 newApplicationScreenshot.y = __getApplicationY(application);
202 newApplicationScreenshot.scale = 1.0;
203 newApplicationScreenshot.visible = true;
204 stage.focusedApplicationWhenUsingScreenshots = application;
205 stage.switchingFromFullscreenToFullscreen = false;
206 }
208 function __getApplicationY(application) {
209 return Qt.binding( function(){
210 return (application && application.fullscreen
211 && application.stage !== ApplicationInfo.SideStage)
212 ? 0 : normalApplicationY;
213 } )
214 }
216 /* FIXME: should hide the screenshots when the focused application's surface
217 is visible. There is no API in QtUbuntu to support that yet.
218 */
219 Connections {
220 target: stage
221 onFocusedApplicationChanged: {
222 if (stage.focusedApplication != null) {
223 /* Move application to the beginning of the list of running applications */
224 var index = stage.__getApplicationIndex(stage.focusedApplication);
225 applicationManager.moveRunningApplicationStackPosition(index, 0, stage.type);
227 var startingApplication = applicationManager.getApplicationFromDesktopFile(
228 stage.__startingApplicationDesktopFile, stage.type);
230 if (stage.focusedApplication == startingApplication) {
231 stage.__startingApplicationDesktopFile = "";
233 /* Screenshots might still be in use at this point and their
234 use can be stopped before delayedHideScreenshots is complete.
235 Prepare for that case by setting appropriately
236 stage.focusedApplicationWhenUsingScreenshots.
237 */
238 stage.focusedApplicationWhenUsingScreenshots = stage.focusedApplication;
240 /* FIXME: should hide the screenshots when the focused application's window
241 is visible. There is no API in qtubuntu to support that yet. Therefore
242 we delay their hiding to avoid a state where neither the screenshot nor
243 the application's window is visible.
244 */
245 delayedHideScreenshots.start();
246 } else {
247 stage.__hideScreenshots();
248 }
249 }
250 }
251 }
253 Connections {
254 target: stage.applications
255 onCountChanged: { __discardObsoleteScreenshots(); }
256 ignoreUnknownSignals: true
257 }
258 onFullyHiddenChanged: { __discardObsoleteScreenshots(); }
259 function __discardObsoleteScreenshots() {
260 if (stage.fullyHidden && stage.applications && stage.applications.count == 0) {
261 stage.focusedApplicationWhenUsingScreenshots = undefined;
262 stage.__hideScreenshots();
263 }
264 }
267 SequentialAnimation {
268 id: showStartingApplicationAnimation
270 function prepare(newApplication) {
271 var oldApplication = stage.focusedApplication;
272 newApplicationScreenshot.setApplication(newApplication);
273 newApplicationScreenshot.updateFromCache();
274 oldApplicationScreenshot.setApplication(oldApplication);
275 oldApplicationScreenshot.scheduleUpdate();
276 newApplicationScreenshot.withBackground = true; // FIXME: should be withBackground = newApplication.state == Application.Starting
277 newApplicationScreenshot.y = __getApplicationY(newApplication);
278 oldApplicationScreenshot.y = __getApplicationY(oldApplication);
279 stage.focusedApplicationWhenUsingScreenshots = newApplication;
280 stage.switchingFromFullscreenToFullscreen = false;
281 }
283 PropertyAction {target: newApplicationScreenshot; property: "visible"; value: false}
284 PropertyAction {target: oldApplicationScreenshot; property: "visible"; value: false}
285 PropertyAction {target: newApplicationScreenshot; property: "z"; value: 0}
286 PropertyAction {target: oldApplicationScreenshot; property: "z"; value: 1}
287 PropertyAction {target: newApplicationScreenshot; property: "scale"; value: 0.8}
288 PropertyAction {target: oldApplicationScreenshot; property: "scale"; value: 1.0}
289 PropertyAction {target: newApplicationScreenshot; property: "opacity"; value: 0.0}
290 PropertyAction {target: oldApplicationScreenshot; property: "opacity"; value: 1.0}
291 PropertyAction {target: newApplicationScreenshot; property: "x"; value: 0.0}
292 PropertyAction {target: oldApplicationScreenshot; property: "x"; value: 0.0}
294 ParallelAnimation {
295 PropertyAction {target: newApplicationScreenshot; property: "visible"; value: true}
296 NumberAnimation {
297 target: newApplicationScreenshot
298 property: "scale"
299 duration: 500
300 easing.type: Easing.OutQuad
301 to: 1.0
302 }
303 NumberAnimation {
304 target: newApplicationScreenshot
305 property: "opacity"
306 duration: 500
307 easing.type: Easing.OutQuad
308 to: 1.0
309 }
311 PropertyAction {target: oldApplicationScreenshot; property: "visible"; value: true}
313 NumberAnimation {
314 target: oldApplicationScreenshot
315 property: "x"
316 duration: 400
317 easing.type: Easing.OutQuad
318 to: stage.width
319 }
320 }
322 ScriptAction {
323 script: {
324 if (!stage.focusedApplicationWhenUsingScreenshots) {
325 // FIXME: only here to support case where application does
326 // not exist yet but is about to
327 return;
328 }
330 stage.__focusApplication(stage.focusedApplicationWhenUsingScreenshots);
331 }
332 }
333 }
336 SequentialAnimation {
337 id: switchToApplicationAnimation
339 function prepare(newApplication) {
340 var oldApplication = stage.focusedApplication;
341 newApplicationScreenshot.setApplication(newApplication);
342 newApplicationScreenshot.updateFromCache();
343 oldApplicationScreenshot.setApplication(oldApplication);
344 oldApplicationScreenshot.scheduleUpdate();
345 newApplicationScreenshot.withBackground = false;
346 newApplicationScreenshot.y = __getApplicationY(newApplication);
347 oldApplicationScreenshot.y = __getApplicationY(oldApplication);
348 stage.focusedApplicationWhenUsingScreenshots = newApplication;
349 stage.switchingFromFullscreenToFullscreen = newApplication && newApplication.fullscreen;
350 }
352 PropertyAction {target: newApplicationScreenshot; property: "visible"; value: false}
353 PropertyAction {target: oldApplicationScreenshot; property: "visible"; value: false}
354 PropertyAction {target: newApplicationScreenshot; property: "z"; value: 1}
355 PropertyAction {target: oldApplicationScreenshot; property: "z"; value: 0}
356 PropertyAction {target: newApplicationScreenshot; property: "scale"; value: 1.0}
357 PropertyAction {target: oldApplicationScreenshot; property: "scale"; value: 1.0}
358 PropertyAction {target: newApplicationScreenshot; property: "opacity"; value: 1.0}
359 PropertyAction {target: oldApplicationScreenshot; property: "opacity"; value: 1.0}
360 PropertyAction {target: newApplicationScreenshot; property: "x"; value: stage.width}
361 PropertyAction {target: oldApplicationScreenshot; property: "x"; value: 0.0}
362 PropertyAction {target: newApplicationScreenshot; property: "visible"; value: true}
363 PropertyAction {target: oldApplicationScreenshot; property: "visible"; value: true}
365 ParallelAnimation {
366 NumberAnimation {
367 target: oldApplicationScreenshot
368 property: "scale"
369 duration: 500
370 easing.type: Easing.OutQuad
371 to: 0.8
372 }
373 NumberAnimation {
374 target: oldApplicationScreenshot
375 property: "opacity"
376 duration: 500
377 easing.type: Easing.OutQuad
378 to: 0.0
379 }
381 NumberAnimation {
382 target: newApplicationScreenshot
383 property: "x"
384 duration: 500
385 easing.type: Easing.OutQuad
386 to: 0
387 }
388 }
390 ScriptAction {
391 script: {
392 // move previously focused application to the end of the list of running applications
393 var oldApplication = oldApplicationScreenshot.application;
394 if (oldApplication != null) {
395 var oldApplicationIndex = stage.__getApplicationIndex(oldApplicationScreenshot.application);
396 applicationManager.moveRunningApplicationStackPosition(
397 oldApplicationIndex, stage.applications.count - 1, stage.type);
398 }
400 stage.__focusApplication(stage.focusedApplicationWhenUsingScreenshots);
401 }
402 }
403 }
406 function __dragValueToProgress(dragValue, startValue, applicationsCount) {
407 if (applicationsCount == 0) {
408 return 0;
409 } else if (applicationsCount == 1) {
410 // prevent the current app from moving more than 1/4 of the screen
411 var elasticValue = __elastic(dragValue, startValue/4);
412 return elasticValue / startValue;
413 } else {
414 return dragValue / startValue;
415 }
416 }
418 function __elastic(x, limit) {
419 return limit * ( 1 - Math.pow( ( limit - 1 ) / limit, x ) )
420 }
422 property real __startValue: stage.width
424 AnimationControllerWithSignals {
425 id: switchToApplicationAnimationController
427 onAnimationCompletedAtBeginning: {
428 animation = null;
429 stage.__focusApplication(oldApplicationScreenshot.application);
430 }
431 }
433 EdgeDragEvaluator {
434 id: dragEvaluator
435 trackedPosition: rightEdgeDraggingArea.touchX
436 direction: rightEdgeDraggingArea.direction
437 maxDragDistance: stage.width
438 }
440 EdgeDragArea {
441 id: rightEdgeDraggingArea
443 enabled: {
444 if (status == DirectionalDragArea.WaitingForTouch) {
445 return switchToApplicationAnimationController.completed
446 && stage.applications.count > 0 && stage.rightEdgeEnabled
447 && !delayedHideScreenshots.running
448 } else {
449 return true
450 }
451 }
452 width: units.gu(2)
453 anchors.top: parent.top
454 anchors.bottom: parent.bottom
455 anchors.right: parent.right
457 direction: Direction.Leftwards
459 onDistanceChanged: {
460 if (status !== DirectionalDragArea.Recognized)
461 return
463 var targetProgress = __dragValueToProgress(-distance, __startValue,
464 stage.applications.count)
465 var delta = targetProgress - switchToApplicationAnimationController.progress
467 // When the gesture finally gets recognized, the finger will likely be
468 // reasonably far from the edge. If we made the progress immediately
469 // follow the finger position it would be visually unpleasant as there
470 // would be a jump to meet the finger's current position.
471 //
472 // The trick is not to go all the way (1.0) as it would cause the
473 // aforementioned jump.
474 switchToApplicationAnimationController.progress += 0.4 * delta
475 }
477 property bool gotRejected: false
479 onStatusChanged: {
480 if (status == DirectionalDragArea.Undecided) {
481 gotRejected = false
482 onUndecided();
483 } else if (status == DirectionalDragArea.Rejected) {
484 switchToApplicationAnimationController.completeToBeginningWithSignal();
485 gotRejected = true;
486 } else if (status == DirectionalDragArea.WaitingForTouch) {
487 if (!gotRejected)
488 onGestureEnded()
489 }
490 }
492 function onUndecided() {
493 dragEvaluator.reset();
495 var nextApplication = null;
496 if (stage.applications.count > 1) {
497 nextApplication = stage.applications.get(1);
498 }
499 switchToApplicationAnimationController.progress = 0
500 switchToApplicationAnimation.prepare(nextApplication);
501 switchToApplicationAnimationController.animation = switchToApplicationAnimation;
503 }
505 function onGestureEnded() {
506 if (stage.applications.count > 1 && dragEvaluator.shouldAutoComplete()) {
507 switchToApplicationAnimationController.completeToEndWithSignal();
508 } else {
509 switchToApplicationAnimationController.completeToBeginningWithSignal();
510 }
511 }
512 }
515 // FIXME: very bad hack to detect when an application was closed or crashed
516 property var __previouslyFocusedApplication
517 Connections {
518 target: stage
519 onFocusedApplicationChanged: {
520 if (stage.focusedApplication == null
521 && __getApplicationIndex(stage.__previouslyFocusedApplication) == -1) {
522 stage.__onFocusedApplicationClosed();
523 }
524 stage.__previouslyFocusedApplication = stage.focusedApplication;
525 }
526 }
528 function __onFocusedApplicationClosed() {
529 if (!stage.shown) {
530 return;
531 }
533 if (stage.applications.count > 0) {
534 // Do not change the focused application from within onFocusedApplicationChanged.
535 // This causes binding loop warnings besides being a bad practice in general
536 // (setting a property from within its "changed" signal handling)
537 // Hence the timer.
538 focusTopmostAppTimer.start();
539 } else {
540 stage.hide();
541 }
542 }
544 Timer {
545 id: focusTopmostAppTimer
546 interval: 1; repeat: false
547 onTriggered: {
548 if (stage.applications.count > 0) {
549 stage.__focusApplication(stage.applications.get(0));
550 }
551 }
552 }