on 9f59192 help sign in
unity8/qml/Shell.qml
699 lines 24 kB history raw
1/*
2 * Copyright (C) 2013-2015 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 QtQuick.Window 2.0
19import AccountsService 0.1
20import GSettings 1.0
21import Unity.Application 0.1
22import Ubuntu.Components 0.1
23import Ubuntu.Components.Popups 1.0
24import Ubuntu.Gestures 0.1
25import Ubuntu.Telephony 0.1 as Telephony
26import Unity.Connectivity 0.1
27import Unity.Launcher 0.1
28import Utils 0.1
29import LightDM 0.1 as LightDM
30import Powerd 0.1
31import SessionBroadcast 0.1
32import "Greeter"
33import "Launcher"
34import "Panel"
35import "Components"
36import "Notifications"
37import "Stages"
38import "Tutorial"
39import "Wizard"
40import Unity.Notifications 1.0 as NotificationBackend
41import Unity.Session 0.1
42import Unity.DashCommunicator 0.1
43import Unity.Indicators 0.1 as Indicators
45Item {
46 id: shell
48 // to be set from outside
49 property int orientationAngle: 0
50 property int orientation
51 property int primaryOrientation
52 property int nativeOrientation
53 property real nativeWidth
54 property real nativeHeight
55 property alias indicatorAreaShowProgress: panel.indicatorAreaShowProgress
56 property bool beingResized
57 property string usageScenario: "phone" // supported values: "phone", "tablet" or "desktop"
58 function updateFocusedAppOrientation() {
59 applicationsDisplayLoader.item.updateFocusedAppOrientation();
60 }
61 function updateFocusedAppOrientationAnimated() {
62 applicationsDisplayLoader.item.updateFocusedAppOrientationAnimated();
63 }
65 // to be read from outside
66 readonly property int mainAppWindowOrientationAngle:
67 applicationsDisplayLoader.item ? applicationsDisplayLoader.item.mainAppWindowOrientationAngle : 0
69 readonly property bool orientationChangesEnabled: panel.indicators.fullyClosed
70 && (applicationsDisplayLoader.item && applicationsDisplayLoader.item.orientationChangesEnabled)
71 && !greeter.animating
73 property bool startingUp: true
74 Timer { id: finishStartUpTimer; interval: 500; onTriggered: startingUp = false }
76 property int supportedOrientations: {
77 if (startingUp) {
78 // Ensure we don't rotate during start up
79 return Qt.PrimaryOrientation;
80 } else if (greeter.shown) {
81 return Qt.PrimaryOrientation;
82 } else if (mainApp) {
83 return mainApp.supportedOrientations;
84 } else {
85 // we just don't care
86 return Qt.PortraitOrientation
87 | Qt.LandscapeOrientation
88 | Qt.InvertedPortraitOrientation
89 | Qt.InvertedLandscapeOrientation;
90 }
91 }
93 // For autopilot consumption
94 readonly property string focusedApplicationId: ApplicationManager.focusedApplicationId
96 // internal props from here onwards
97 readonly property var mainApp:
98 applicationsDisplayLoader.item ? applicationsDisplayLoader.item.mainApp : null
100 // Disable everything while greeter is waiting, so that the user can't swipe
101 // the greeter or launcher until we know whether the session is locked.
102 enabled: !greeter.waiting
104 property real edgeSize: units.gu(2)
105 property url defaultBackground: Qt.resolvedUrl(shell.width >= units.gu(60) ? "graphics/tablet_background.jpg" : "graphics/phone_background.jpg")
106 property url background: asImageTester.status == Image.Ready ? asImageTester.source
107 : gsImageTester.status == Image.Ready ? gsImageTester.source : defaultBackground
109 // This is _only_ used to expose the property to autopilot tests
110 readonly property string testShellMode: shellMode
112 function activateApplication(appId) {
113 if (ApplicationManager.findApplication(appId)) {
114 ApplicationManager.requestFocusApplication(appId);
115 } else {
116 var execFlags = shell.usageScenario === "phone" ? ApplicationManager.ForceMainStage
117 : ApplicationManager.NoFlag;
118 ApplicationManager.startApplication(appId, execFlags);
119 }
120 }
122 function startLockedApp(app) {
123 if (greeter.locked) {
124 greeter.lockedApp = app;
125 }
126 shell.activateApplication(app);
127 }
129 // This is a dummy image to detect if the custom AS set wallpaper loads successfully.
130 Image {
131 id: asImageTester
132 source: AccountsService.backgroundFile != undefined && AccountsService.backgroundFile.length > 0 ? AccountsService.backgroundFile : ""
133 height: 0
134 width: 0
135 sourceSize.height: 0
136 sourceSize.width: 0
137 }
139 GSettings {
140 id: backgroundSettings
141 objectName: "backgroundSettings"
142 schema.id: "org.gnome.desktop.background"
143 }
145 // This is a dummy image to detect if the custom GSettings set wallpaper loads successfully.
146 Image {
147 id: gsImageTester
148 source: backgroundSettings.pictureUri != undefined && backgroundSettings.pictureUri.length > 0 ? backgroundSettings.pictureUri : ""
149 height: 0
150 width: 0
151 sourceSize.height: 0
152 sourceSize.width: 0
153 }
155 Binding {
156 target: LauncherModel
157 property: "applicationManager"
158 value: ApplicationManager
159 }
161 Component.onCompleted: {
162 Theme.name = "Ubuntu.Components.Themes.SuruGradient"
163 if (ApplicationManager.count > 0) {
164 ApplicationManager.focusApplication(ApplicationManager.get(0).appId);
165 }
166 finishStartUpTimer.start();
167 }
169 VolumeControl {
170 id: volumeControl
171 }
173 DashCommunicator {
174 id: dash
175 objectName: "dashCommunicator"
176 }
178 PhysicalKeysMapper {
179 id: physicalKeysMapper
181 onPowerKeyLongPressed: dialogs.showPowerDialog()
182 onVolumeDownTriggered: volumeControl.volumeDown();
183 onVolumeUpTriggered: volumeControl.volumeUp();
184 onScreenshotTriggered: screenGrabber.capture();
185 }
187 ScreenGrabber {
188 id: screenGrabber
189 z: dialogs.z + 10
190 enabled: Powerd.status === Powerd.On
191 }
193 Binding {
194 target: ApplicationManager
195 property: "forceDashActive"
196 value: launcher.shown || launcher.dashSwipe
197 }
199 WindowKeysFilter {
200 Keys.onPressed: physicalKeysMapper.onKeyPressed(event);
201 Keys.onReleased: physicalKeysMapper.onKeyReleased(event);
202 }
204 HomeKeyWatcher {
205 onActivated: { launcher.fadeOut(); shell.showHome(); }
206 }
208 Item {
209 id: stages
210 objectName: "stages"
211 width: parent.width
212 height: parent.height
214 Connections {
215 target: ApplicationManager
217 // This signal is also fired when we try to focus the current app
218 // again. We rely on this!
219 onFocusedApplicationIdChanged: {
220 var appId = ApplicationManager.focusedApplicationId;
222 if (tutorial.running && appId != "" && appId != "unity8-dash") {
223 // If this happens on first boot, we may be in edge
224 // tutorial or wizard while receiving a call. But a call
225 // is more important than wizard so just bail out of those.
226 tutorial.finish();
227 wizard.hide();
228 }
230 if (appId === "dialer-app" && callManager.hasCalls && greeter.locked) {
231 // If we are in the middle of a call, make dialer lockedApp and show it.
232 // This can happen if user backs out of dialer back to greeter, then
233 // launches dialer again.
234 greeter.lockedApp = appId;
235 }
236 greeter.notifyAppFocused(appId);
238 panel.indicators.hide();
239 }
241 onApplicationAdded: {
242 launcher.hide();
243 }
244 }
246 Loader {
247 id: applicationsDisplayLoader
248 objectName: "applicationsDisplayLoader"
249 anchors.fill: parent
251 // When we have a locked app, we only want to show that one app.
252 // FIXME: do this in a less traumatic way. We currently only allow
253 // locked apps in phone mode (see FIXME in Lockscreen component in
254 // this same file). When that changes, we need to do something
255 // nicer here. But this code is currently just to prevent a
256 // theoretical attack where user enters lockedApp mode, then makes
257 // the screen larger (maybe connects to monitor) and tries to enter
258 // tablet mode.
260 property string usageScenario: shell.usageScenario === "phone" || greeter.hasLockedApp
261 ? "phone"
262 : shell.usageScenario
263 source: {
264 if (applicationsDisplayLoader.usageScenario === "phone") {
265 return "Stages/PhoneStage.qml";
266 } else if (applicationsDisplayLoader.usageScenario === "tablet") {
267 return "Stages/TabletStage.qml";
268 } else {
269 return "Stages/DesktopStage.qml";
270 }
271 }
273 property bool interactive: tutorial.spreadEnabled
274 && !greeter.shown
275 && panel.indicators.fullyClosed
276 && launcher.progress == 0
277 && !notifications.useModal
279 onInteractiveChanged: { if (interactive) { focus = true; } }
281 Binding {
282 target: applicationsDisplayLoader.item
283 property: "objectName"
284 value: "stage"
285 }
286 Binding {
287 target: applicationsDisplayLoader.item
288 property: "dragAreaWidth"
289 value: shell.edgeSize
290 }
291 Binding {
292 target: applicationsDisplayLoader.item
293 property: "maximizedAppTopMargin"
294 // Not just using panel.panelHeight as that changes depending on the focused app.
295 value: panel.indicators.minimizedPanelHeight + units.dp(2) // dp(2) for orange line
296 }
297 Binding {
298 target: applicationsDisplayLoader.item
299 property: "interactive"
300 value: applicationsDisplayLoader.interactive
301 }
302 Binding {
303 target: applicationsDisplayLoader.item
304 property: "spreadEnabled"
305 value: tutorial.spreadEnabled && !greeter.hasLockedApp
306 }
307 Binding {
308 target: applicationsDisplayLoader.item
309 property: "inverseProgress"
310 value: greeter.locked ? 0 : launcher.progress
311 }
312 Binding {
313 target: applicationsDisplayLoader.item
314 property: "shellOrientationAngle"
315 value: shell.orientationAngle
316 }
317 Binding {
318 target: applicationsDisplayLoader.item
319 property: "shellOrientation"
320 value: shell.orientation
321 }
322 Binding {
323 target: applicationsDisplayLoader.item
324 property: "background"
325 value: shell.background
326 }
327 Binding {
328 target: applicationsDisplayLoader.item
329 property: "shellPrimaryOrientation"
330 value: shell.primaryOrientation
331 }
332 Binding {
333 target: applicationsDisplayLoader.item
334 property: "nativeOrientation"
335 value: shell.nativeOrientation
336 }
337 Binding {
338 target: applicationsDisplayLoader.item
339 property: "nativeWidth"
340 value: shell.nativeWidth
341 }
342 Binding {
343 target: applicationsDisplayLoader.item
344 property: "nativeHeight"
345 value: shell.nativeHeight
346 }
347 Binding {
348 target: applicationsDisplayLoader.item
349 property: "beingResized"
350 value: shell.beingResized
351 }
352 }
354 Tutorial {
355 id: tutorial
356 objectName: "tutorial"
357 anchors.fill: parent
358 active: AccountsService.demoEdges
359 paused: LightDM.Greeter.active
360 launcher: launcher
361 panel: panel
362 edgeSize: shell.edgeSize
364 // EdgeDragAreas don't work with mice. So to avoid trapping the user,
365 // we'll tell the tutorial to avoid using them on the Desktop. The
366 // Desktop doesn't use the same spread design anyway. The tutorial is
367 // all a bit of a placeholder on non-phone form factors right now.
368 // When the design team gives us more guidance, we can do something
369 // more clever here.
370 // TODO: use DeviceConfiguration instead of checking source
371 useEdgeDragArea: applicationsDisplayLoader.source != Qt.resolvedUrl("Stages/DesktopStage.qml")
373 onFinished: {
374 AccountsService.demoEdges = false;
375 active = false; // for immediate response / if AS is having problems
376 }
377 }
378 }
380 InputMethod {
381 id: inputMethod
382 objectName: "inputMethod"
383 anchors { fill: parent; topMargin: panel.panelHeight }
384 z: notifications.useModal || panel.indicators.shown || wizard.active ? overlay.z + 1 : overlay.z - 1
385 }
387 Connections {
388 target: SurfaceManager
389 onSurfaceCreated: {
390 if (surface.type == MirSurfaceItem.InputMethod) {
391 inputMethod.surface = surface;
392 }
393 }
395 onSurfaceDestroyed: {
396 if (inputMethod.surface == surface) {
397 inputMethod.surface = null;
398 surface.parent = null;
399 }
400 if (!surface.parent) {
401 // there's no one displaying it. delete it right away
402 surface.release();
403 }
404 }
405 }
406 Connections {
407 target: SessionManager
408 onSessionStopping: {
409 if (!session.parentSession && !session.application) {
410 // nothing is using it. delete it right away
411 session.release();
412 }
413 }
414 }
416 Greeter {
417 id: greeter
418 objectName: "greeter"
420 hides: [launcher, panel.indicators]
421 tabletMode: shell.usageScenario !== "phone"
422 launcherOffset: launcher.progress
423 forcedUnlock: tutorial.running
424 background: shell.background
426 anchors.fill: parent
427 anchors.topMargin: panel.panelHeight
429 // avoid overlapping with Launcher's edge drag area
430 // FIXME: Fix TouchRegistry & friends and remove this workaround
431 // Issue involves launcher's DDA getting disabled on a long
432 // left-edge drag
433 dragHandleLeftMargin: launcher.available ? launcher.dragAreaWidth + 1 : 0
435 onSessionStarted: {
436 launcher.hide();
437 }
439 onTease: {
440 if (!tutorial.running) {
441 launcher.tease();
442 }
443 }
445 onEmergencyCall: startLockedApp("dialer-app")
447 Timer {
448 // See powerConnection for why this is useful
449 id: showGreeterDelayed
450 interval: 1
451 onTriggered: {
452 greeter.forceShow();
453 }
454 }
456 Binding {
457 target: ApplicationManager
458 property: "suspended"
459 value: greeter.shown
460 }
461 }
463 Connections {
464 id: callConnection
465 target: callManager
467 onHasCallsChanged: {
468 if (greeter.locked && callManager.hasCalls && greeter.lockedApp !== "dialer-app") {
469 // We just received an incoming call while locked. The
470 // indicator will have already launched dialer-app for us, but
471 // there is a race between "hasCalls" changing and the dialer
472 // starting up. So in case we lose that race, we'll start/
473 // focus the dialer ourselves here too. Even if the indicator
474 // didn't launch the dialer for some reason (or maybe a call
475 // started via some other means), if an active call is
476 // happening, we want to be in the dialer.
477 startLockedApp("dialer-app")
478 }
479 }
480 }
482 Connections {
483 id: powerConnection
484 target: Powerd
486 onStatusChanged: {
487 if (Powerd.status === Powerd.Off && reason !== Powerd.Proximity &&
488 !callManager.hasCalls && !tutorial.running) {
489 // We don't want to simply call greeter.showNow() here, because
490 // that will take too long. Qt will delay button event
491 // handling until the greeter is done loading and may think the
492 // user held down the power button the whole time, leading to a
493 // power dialog being shown. Instead, delay showing the
494 // greeter until we've finished handling the event. We could
495 // make the greeter load asynchronously instead, but that
496 // introduces a whole host of timing issues, especially with
497 // its animations. So this is simpler.
498 showGreeterDelayed.start();
499 }
500 }
501 }
503 function showHome() {
504 if (tutorial.running) {
505 return
506 }
508 greeter.notifyAboutToFocusApp("unity8-dash");
510 var animate = !LightDM.Greeter.active && !stages.shown
511 dash.setCurrentScope(0, animate, false)
512 ApplicationManager.requestFocusApplication("unity8-dash")
513 }
515 function showDash() {
516 if (greeter.notifyShowingDashFromDrag()) {
517 launcher.fadeOut();
518 }
520 if (!greeter.locked && ApplicationManager.focusedApplicationId != "unity8-dash") {
521 ApplicationManager.requestFocusApplication("unity8-dash")
522 launcher.fadeOut();
523 }
524 }
526 Item {
527 id: overlay
528 z: 10
530 anchors.fill: parent
532 Panel {
533 id: panel
534 objectName: "panel"
535 anchors.fill: parent //because this draws indicator menus
536 indicators {
537 hides: [launcher]
538 available: tutorial.panelEnabled
539 && (!greeter.locked || AccountsService.enableIndicatorsWhileLocked)
540 && !greeter.hasLockedApp
541 contentEnabled: tutorial.panelContentEnabled
542 width: parent.width > units.gu(60) ? units.gu(40) : parent.width
544 minimizedPanelHeight: units.gu(3)
545 expandedPanelHeight: units.gu(7)
547 indicatorsModel: Indicators.IndicatorsModel {
548 // tablet and phone both use the same profile
549 profile: shell.usageScenario === "desktop" ? "desktop" : "phone"
550 Component.onCompleted: load();
551 }
552 }
554 callHint {
555 greeterShown: greeter.shown
556 }
558 property bool mainAppIsFullscreen: shell.mainApp && shell.mainApp.fullscreen
559 fullscreenMode: (mainAppIsFullscreen && !LightDM.Greeter.active && launcher.progress == 0)
560 || greeter.hasLockedApp
561 }
563 Launcher {
564 id: launcher
565 objectName: "launcher"
567 readonly property bool dashSwipe: progress > 0
569 anchors.top: parent.top
570 anchors.topMargin: inverted ? 0 : panel.panelHeight
571 anchors.bottom: parent.bottom
572 width: parent.width
573 dragAreaWidth: shell.edgeSize
574 available: tutorial.launcherEnabled
575 && (!greeter.locked || AccountsService.enableLauncherWhileLocked)
576 && !greeter.hasLockedApp
577 inverted: shell.usageScenario !== "desktop"
578 shadeBackground: !tutorial.running
580 onShowDashHome: showHome()
581 onDash: showDash()
582 onDashSwipeChanged: {
583 if (dashSwipe) {
584 dash.setCurrentScope(0, false, true)
585 }
586 }
587 onLauncherApplicationSelected: {
588 if (!tutorial.running) {
589 greeter.notifyAboutToFocusApp(appId);
590 shell.activateApplication(appId)
591 }
592 }
593 onShownChanged: {
594 if (shown) {
595 panel.indicators.hide()
596 }
597 }
598 }
600 Wizard {
601 id: wizard
602 objectName: "wizard"
603 anchors.fill: parent
604 background: shell.background
606 function unlockWhenDoneWithWizard() {
607 if (!active) {
608 Connectivity.unlockAllModems();
609 }
610 }
612 Component.onCompleted: unlockWhenDoneWithWizard()
613 onActiveChanged: unlockWhenDoneWithWizard()
614 }
616 Rectangle {
617 id: modalNotificationBackground
619 visible: notifications.useModal
620 color: "#000000"
621 anchors.fill: parent
622 opacity: 0.9
624 MouseArea {
625 anchors.fill: parent
626 }
627 }
629 Notifications {
630 id: notifications
632 model: NotificationBackend.Model
633 margin: units.gu(1)
635 y: topmostIsFullscreen ? 0 : panel.panelHeight
636 height: parent.height - (topmostIsFullscreen ? 0 : panel.panelHeight)
638 states: [
639 State {
640 name: "narrow"
641 when: overlay.width <= units.gu(60)
642 AnchorChanges {
643 target: notifications
644 anchors.left: parent.left
645 anchors.right: parent.right
646 }
647 },
648 State {
649 name: "wide"
650 when: overlay.width > units.gu(60)
651 AnchorChanges {
652 target: notifications
653 anchors.left: undefined
654 anchors.right: parent.right
655 }
656 PropertyChanges { target: notifications; width: units.gu(38) }
657 }
658 ]
659 }
660 }
662 Dialogs {
663 id: dialogs
664 objectName: "dialogs"
665 anchors.fill: parent
666 z: overlay.z + 10
667 onPowerOffClicked: {
668 shutdownFadeOutRectangle.enabled = true;
669 shutdownFadeOutRectangle.visible = true;
670 shutdownFadeOut.start();
671 }
672 }
674 Connections {
675 target: SessionBroadcast
676 onShowHome: showHome()
677 }
679 Rectangle {
680 id: shutdownFadeOutRectangle
681 z: screenGrabber.z + 10
682 enabled: false
683 visible: false
684 color: "black"
685 anchors.fill: parent
686 opacity: 0.0
687 NumberAnimation on opacity {
688 id: shutdownFadeOut
689 from: 0.0
690 to: 1.0
691 onStopped: {
692 if (shutdownFadeOutRectangle.enabled && shutdownFadeOutRectangle.visible) {
693 DBusUnitySessionService.Shutdown();
694 }
695 }
696 }
697 }