on 3bbd428 help sign in
unity8/Components/Stage.qml
563 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 Connections {
88 target: applicationManager
89 onFocusRequested: activateApplication(appId)
90 }
92 /* Keep a reference to the focused application so that we can safely
93 unfocus it when the stage is not fully shown and refocus it when the stage
94 is fully shown again.
95 */
96 property var focusedApplicationWhenUsingScreenshots
97 onShouldUseScreenshotsChanged: {
98 if (showStartingApplicationAnimation.running || switchToApplicationAnimation.running) {
99 return;
100 }
102 if (shouldUseScreenshots) {
103 stage.__focusApplicationUsingScreenshots(stage.focusedApplication);
104 } else {
105 if (stage.focusedApplicationWhenUsingScreenshots) {
106 stage.__focusActualApplication(stage.focusedApplicationWhenUsingScreenshots);
107 }
108 }
109 }
112 property Item newApplicationScreenshot: ApplicationScreenshot {
113 parent: stage
114 width: stage.width
115 height: stage.height - y
116 }
117 property Item oldApplicationScreenshot: ApplicationScreenshot {
118 parent: stage
119 width: stage.width
120 height: stage.height - y
121 }
123 function __getApplicationIndex(application) {
124 if (!application)
125 return -1
126 for (var i = 0; i < stage.applications.count; i++ ) {
127 if (stage.applications.get(i).desktopFile == application.desktopFile) {
128 return i
129 }
130 }
131 return -1
132 }
135 property string __startingApplicationDesktopFile
137 Timer {
138 id: delayedHideScreenshots
139 interval: 500 // on Galaxy Nexus, Telephony app is slowest application to draw a frame so needs this delay
140 onTriggered: {
141 __hideScreenshots();
142 }
143 }
145 function __hideScreenshots() {
146 newApplicationScreenshot.clearApplication();
147 oldApplicationScreenshot.clearApplication();
148 newApplicationScreenshot.visible = false;
149 oldApplicationScreenshot.visible = false;
150 }
152 function __focusApplication(application) {
153 if (!application) return
154 if (shouldUseScreenshots) {
155 stage.__focusApplicationUsingScreenshots(application);
156 } else {
157 stage.__focusActualApplication(application);
158 }
159 }
162 function __focusActualApplication(application) {
163 if (application == null) {
164 return;
165 }
167 if (stage.focusedApplication == application) {
168 /* FIXME: this case is reached when showing the sidestage, we
169 need to refocus the side stage app in order to show it.
170 FIXME: this case is reached when doing a right edge swipe
171 while in the dash to bring back the stage.
172 FIXME: this case is reached when showStartingApplicationAnimation
173 finishes after the newly started application has been focused
174 automatically by the ApplicationManager.
175 */
176 if (stage.type == ApplicationInfo.SideStage) {
177 applicationManager.focusApplication(application);
178 }
179 if (!delayedHideScreenshots.running) {
180 stage.__hideScreenshots();
181 }
182 } else {
183 /* FIXME: calling ApplicationManager::focusApplication does not focus
184 the application immediately nor show the application's surface
185 immediately. Delay hiding the screenshots until it has done so.
186 */
187 delayedHideScreenshots.stop();
188 applicationManager.focusApplication(application);
189 }
190 stage.focusedApplicationWhenUsingScreenshots = null;
191 }
193 function __focusApplicationUsingScreenshots(application) {
194 delayedHideScreenshots.stop();
195 if (application == null) {
196 return;
197 }
199 if (newApplicationScreenshot.application != null) {
200 stage.focusedApplicationWhenUsingScreenshots = application;
201 return;
202 }
204 newApplicationScreenshot.setApplication(application);
205 newApplicationScreenshot.scheduleUpdate();
206 newApplicationScreenshot.opacity = 1.0;
207 newApplicationScreenshot.x = 0.0;
208 newApplicationScreenshot.y = __getApplicationY(application);
209 newApplicationScreenshot.scale = 1.0;
210 newApplicationScreenshot.visible = true;
211 stage.focusedApplicationWhenUsingScreenshots = application;
212 stage.switchingFromFullscreenToFullscreen = false;
213 }
215 function __getApplicationY(application) {
216 return Qt.binding( function(){
217 return (application && application.fullscreen
218 && application.stage !== ApplicationInfo.SideStage)
219 ? 0 : normalApplicationY;
220 } )
221 }
223 /* FIXME: should hide the screenshots when the focused application's surface
224 is visible. There is no API in QtUbuntu to support that yet.
225 */
226 Connections {
227 target: stage
228 onFocusedApplicationChanged: {
229 if (stage.focusedApplication != null) {
230 /* Move application to the beginning of the list of running applications */
231 var index = stage.__getApplicationIndex(stage.focusedApplication);
232 applicationManager.moveRunningApplicationStackPosition(index, 0, stage.type);
234 var startingApplication = applicationManager.getApplicationFromDesktopFile(
235 stage.__startingApplicationDesktopFile, stage.type);
237 if (stage.focusedApplication == startingApplication) {
238 stage.__startingApplicationDesktopFile = "";
240 /* Screenshots might still be in use at this point and their
241 use can be stopped before delayedHideScreenshots is complete.
242 Prepare for that case by setting appropriately
243 stage.focusedApplicationWhenUsingScreenshots.
244 */
245 stage.focusedApplicationWhenUsingScreenshots = stage.focusedApplication;
247 /* FIXME: should hide the screenshots when the focused application's window
248 is visible. There is no API in qtubuntu to support that yet. Therefore
249 we delay their hiding to avoid a state where neither the screenshot nor
250 the application's window is visible.
251 */
252 delayedHideScreenshots.start();
253 } else {
254 stage.focusedApplicationWhenUsingScreenshots = null;
255 stage.__hideScreenshots();
256 }
257 }
258 }
259 }
261 Connections {
262 target: stage.applications
263 onCountChanged: { __discardObsoleteScreenshots(); }
264 ignoreUnknownSignals: true
265 }
266 onFullyHiddenChanged: { __discardObsoleteScreenshots(); }
267 function __discardObsoleteScreenshots() {
268 if (stage.fullyHidden && stage.applications && stage.applications.count == 0) {
269 stage.focusedApplicationWhenUsingScreenshots = undefined;
270 stage.__hideScreenshots();
271 }
272 }
275 SequentialAnimation {
276 id: showStartingApplicationAnimation
278 function prepare(newApplication) {
279 var oldApplication = stage.focusedApplication;
280 newApplicationScreenshot.setApplication(newApplication);
281 newApplicationScreenshot.updateFromCache();
282 oldApplicationScreenshot.setApplication(oldApplication);
283 oldApplicationScreenshot.scheduleUpdate();
284 newApplicationScreenshot.withBackground = true; // FIXME: should be withBackground = newApplication.state == Application.Starting
285 newApplicationScreenshot.y = __getApplicationY(newApplication);
286 oldApplicationScreenshot.y = __getApplicationY(oldApplication);
287 stage.focusedApplicationWhenUsingScreenshots = newApplication;
288 stage.switchingFromFullscreenToFullscreen = false;
289 }
291 PropertyAction {target: newApplicationScreenshot; property: "visible"; value: false}
292 PropertyAction {target: oldApplicationScreenshot; property: "visible"; value: false}
293 PropertyAction {target: newApplicationScreenshot; property: "z"; value: 0}
294 PropertyAction {target: oldApplicationScreenshot; property: "z"; value: 1}
295 PropertyAction {target: newApplicationScreenshot; property: "scale"; value: 0.8}
296 PropertyAction {target: oldApplicationScreenshot; property: "scale"; value: 1.0}
297 PropertyAction {target: newApplicationScreenshot; property: "opacity"; value: 0.0}
298 PropertyAction {target: oldApplicationScreenshot; property: "opacity"; value: 1.0}
299 PropertyAction {target: newApplicationScreenshot; property: "x"; value: 0.0}
300 PropertyAction {target: oldApplicationScreenshot; property: "x"; value: 0.0}
302 ParallelAnimation {
303 PropertyAction {target: newApplicationScreenshot; property: "visible"; value: true}
304 NumberAnimation {
305 target: newApplicationScreenshot
306 property: "scale"
307 duration: 500
308 easing.type: Easing.OutQuad
309 to: 1.0
310 }
311 NumberAnimation {
312 target: newApplicationScreenshot
313 property: "opacity"
314 duration: 500
315 easing.type: Easing.OutQuad
316 to: 1.0
317 }
319 PropertyAction {target: oldApplicationScreenshot; property: "visible"; value: true}
321 NumberAnimation {
322 target: oldApplicationScreenshot
323 property: "x"
324 duration: 400
325 easing.type: Easing.OutQuad
326 to: stage.width
327 }
328 }
330 ScriptAction {
331 script: {
332 if (!stage.focusedApplicationWhenUsingScreenshots) {
333 // FIXME: only here to support case where application does
334 // not exist yet but is about to
335 return;
336 }
338 stage.__focusApplication(stage.focusedApplicationWhenUsingScreenshots);
339 }
340 }
341 }
344 SequentialAnimation {
345 id: switchToApplicationAnimation
347 function prepare(newApplication) {
348 var oldApplication = stage.focusedApplication;
349 newApplicationScreenshot.setApplication(newApplication);
350 newApplicationScreenshot.updateFromCache();
351 oldApplicationScreenshot.setApplication(oldApplication);
352 oldApplicationScreenshot.scheduleUpdate();
353 newApplicationScreenshot.withBackground = false;
354 newApplicationScreenshot.y = __getApplicationY(newApplication);
355 oldApplicationScreenshot.y = __getApplicationY(oldApplication);
356 stage.focusedApplicationWhenUsingScreenshots = newApplication;
357 stage.switchingFromFullscreenToFullscreen = newApplication && newApplication.fullscreen;
358 }
360 PropertyAction {target: newApplicationScreenshot; property: "visible"; value: false}
361 PropertyAction {target: oldApplicationScreenshot; property: "visible"; value: false}
362 PropertyAction {target: newApplicationScreenshot; property: "z"; value: 1}
363 PropertyAction {target: oldApplicationScreenshot; property: "z"; value: 0}
364 PropertyAction {target: newApplicationScreenshot; property: "scale"; value: 1.0}
365 PropertyAction {target: oldApplicationScreenshot; property: "scale"; value: 1.0}
366 PropertyAction {target: newApplicationScreenshot; property: "opacity"; value: 1.0}
367 PropertyAction {target: oldApplicationScreenshot; property: "opacity"; value: 1.0}
368 PropertyAction {target: newApplicationScreenshot; property: "x"; value: stage.width}
369 PropertyAction {target: oldApplicationScreenshot; property: "x"; value: 0.0}
370 PropertyAction {target: newApplicationScreenshot; property: "visible"; value: true}
371 PropertyAction {target: oldApplicationScreenshot; property: "visible"; value: true}
373 ParallelAnimation {
374 NumberAnimation {
375 target: oldApplicationScreenshot
376 property: "scale"
377 duration: 500
378 easing.type: Easing.OutQuad
379 to: 0.8
380 }
381 NumberAnimation {
382 target: oldApplicationScreenshot
383 property: "opacity"
384 duration: 500
385 easing.type: Easing.OutQuad
386 to: 0.0
387 }
389 NumberAnimation {
390 target: newApplicationScreenshot
391 property: "x"
392 duration: 500
393 easing.type: Easing.OutQuad
394 to: 0
395 }
396 }
398 ScriptAction {
399 script: {
400 // move previously focused application to the end of the list of running applications
401 var oldApplication = oldApplicationScreenshot.application;
402 if (oldApplication != null) {
403 var oldApplicationIndex = stage.__getApplicationIndex(oldApplicationScreenshot.application);
404 applicationManager.moveRunningApplicationStackPosition(
405 oldApplicationIndex, stage.applications.count - 1, stage.type);
406 }
408 stage.__focusApplication(stage.focusedApplicationWhenUsingScreenshots);
409 }
410 }
411 }
414 function __dragValueToProgress(dragValue, startValue, applicationsCount) {
415 if (applicationsCount == 0) {
416 return 0;
417 } else if (applicationsCount == 1) {
418 // prevent the current app from moving more than 1/4 of the screen
419 var elasticValue = __elastic(dragValue, startValue/4);
420 return elasticValue / startValue;
421 } else {
422 return dragValue / startValue;
423 }
424 }
426 function __elastic(x, limit) {
427 return limit * ( 1 - Math.pow( ( limit - 1 ) / limit, x ) )
428 }
430 property real __startValue: stage.width
432 AnimationControllerWithSignals {
433 id: switchToApplicationAnimationController
435 onAnimationCompletedAtBeginning: {
436 animation = null;
437 stage.__focusApplication(oldApplicationScreenshot.application);
438 }
439 }
441 EdgeDragEvaluator {
442 id: dragEvaluator
443 trackedPosition: rightEdgeDraggingArea.touchX
444 direction: rightEdgeDraggingArea.direction
445 maxDragDistance: stage.width
446 }
448 EdgeDragArea {
449 id: rightEdgeDraggingArea
451 enabled: {
452 if (status == DirectionalDragArea.WaitingForTouch) {
453 return switchToApplicationAnimationController.completed
454 && stage.applications.count > 0 && stage.rightEdgeEnabled
455 && !delayedHideScreenshots.running
456 } else {
457 return true
458 }
459 }
460 width: units.gu(2)
461 anchors.top: parent.top
462 anchors.bottom: parent.bottom
463 anchors.right: parent.right
465 direction: Direction.Leftwards
467 onDistanceChanged: {
468 if (status !== DirectionalDragArea.Recognized)
469 return
471 var targetProgress = __dragValueToProgress(-distance, __startValue,
472 stage.applications.count)
473 var delta = targetProgress - switchToApplicationAnimationController.progress
475 // When the gesture finally gets recognized, the finger will likely be
476 // reasonably far from the edge. If we made the progress immediately
477 // follow the finger position it would be visually unpleasant as there
478 // would be a jump to meet the finger's current position.
479 //
480 // The trick is not to go all the way (1.0) as it would cause the
481 // aforementioned jump.
482 switchToApplicationAnimationController.progress += 0.4 * delta
483 }
485 property int lastStatus: -1
487 onStatusChanged: {
488 if (status == DirectionalDragArea.Undecided) {
489 onUndecided();
490 } else if (status == DirectionalDragArea.WaitingForTouch) {
491 if (lastStatus == DirectionalDragArea.Undecided) {
492 // got rejected
493 switchToApplicationAnimationController.completeToBeginningWithSignal();
494 } else {
495 onGestureEnded();
496 }
497 }
498 lastStatus = status;
499 }
501 function onUndecided() {
502 dragEvaluator.reset();
504 var nextApplication = null;
505 if (stage.applications.count > 1) {
506 nextApplication = stage.applications.get(1);
507 }
508 switchToApplicationAnimationController.progress = 0
509 switchToApplicationAnimation.prepare(nextApplication);
510 switchToApplicationAnimationController.animation = switchToApplicationAnimation;
512 }
514 function onGestureEnded() {
515 if (stage.applications.count > 1 && dragEvaluator.shouldAutoComplete()) {
516 switchToApplicationAnimationController.completeToEndWithSignal();
517 } else {
518 switchToApplicationAnimationController.completeToBeginningWithSignal();
519 }
520 }
521 }
524 // FIXME: very bad hack to detect when an application was closed or crashed
525 property var __previouslyFocusedApplication
526 Connections {
527 target: stage
528 onFocusedApplicationChanged: {
529 if (stage.focusedApplication == null
530 && __getApplicationIndex(stage.__previouslyFocusedApplication) == -1) {
531 stage.__onFocusedApplicationClosed();
532 }
533 stage.__previouslyFocusedApplication = stage.focusedApplication;
534 }
535 }
537 function __onFocusedApplicationClosed() {
538 if (!stage.shown) {
539 return;
540 }
542 if (stage.applications.count > 0) {
543 // Do not change the focused application from within onFocusedApplicationChanged.
544 // This causes binding loop warnings besides being a bad practice in general
545 // (setting a property from within its "changed" signal handling)
546 // Hence the timer.
547 focusTopmostAppTimer.start();
548 } else {
549 stage.hide();
550 }
551 }
553 Timer {
554 id: focusTopmostAppTimer
555 interval: 1; repeat: false
556 onTriggered: {
557 if (stage.applications.count > 0) {
558 stage.__focusApplication(stage.applications.get(0));
559 }
560 }
561 }