on 6381eb4 help sign in
unity8/qml/Shell.qml
761 lines 27 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 AccountsService 0.1
19import GSettings 1.0
20import Unity.Application 0.1
21import Ubuntu.Components 0.1
22import Ubuntu.Gestures 0.1
23import Unity.Launcher 0.1
24import LightDM 0.1 as LightDM
25import Powerd 0.1
26import SessionBroadcast 0.1
27import "Dash"
28import "Greeter"
29import "Launcher"
30import "Panel"
31import "Components"
32import "SideStage"
33import "Notifications"
34import Unity.Notifications 1.0 as NotificationBackend
36FocusScope {
37 id: shell
39 // this is only here to select the width / height of the window if not running fullscreen
40 property bool tablet: false
41 width: tablet ? units.gu(160) : applicationArguments.hasGeometry() ? applicationArguments.width() : units.gu(40)
42 height: tablet ? units.gu(100) : applicationArguments.hasGeometry() ? applicationArguments.height() : units.gu(71)
44 property real edgeSize: units.gu(2)
45 property url defaultBackground: Qt.resolvedUrl(shell.width >= units.gu(60) ? "graphics/tablet_background.jpg" : "graphics/phone_background.jpg")
46 property url background
47 readonly property real panelHeight: panel.panelHeight
49 property bool dashShown: dash.shown
50 property bool stageScreenshotsReady: {
51 if (sideStage.shown) {
52 if (mainStage.applications.count > 0) {
53 return mainStage.usingScreenshots && sideStage.usingScreenshots;
54 } else {
55 return sideStage.usingScreenshots;
56 }
57 } else {
58 return mainStage.usingScreenshots;
59 }
60 }
62 property var applicationManager: ApplicationManagerWrapper {}
64 Binding {
65 target: LauncherModel
66 property: "applicationManager"
67 value: ApplicationManager
68 }
70 Component.onCompleted: {
71 Theme.name = "Ubuntu.Components.Themes.SuruGradient"
73 applicationManager.sideStageEnabled = Qt.binding(function() { return sideStage.enabled })
75 // FIXME: if application focused before shell starts, shell draws on top of it only.
76 // We should detect already running applications on shell start and bring them to the front.
77 applicationManager.unfocusCurrentApplication();
78 }
80 readonly property bool applicationFocused: !!applicationManager.mainStageFocusedApplication
81 || !!applicationManager.sideStageFocusedApplication
82 // Used for autopilot testing.
83 readonly property string currentFocusedAppId: ApplicationManager.focusedApplicationId
85 readonly property bool fullscreenMode: {
86 if (greeter.shown || lockscreen.shown) {
87 return false;
88 } else if (mainStage.usingScreenshots) { // Window Manager animating so want to re-evaluate fullscreen mode
89 return mainStage.switchingFromFullscreenToFullscreen;
90 } else if (applicationManager.mainStageFocusedApplication) {
91 return applicationManager.mainStageFocusedApplication.fullscreen;
92 } else {
93 return false;
94 }
95 }
97 function activateApplication(appId, argument) {
98 if (applicationManager) {
99 // For newly started applications, as it takes them time to draw their first frame
100 // we add a delay before we hide the animation screenshots to compensate.
101 var addDelay = !applicationManager.getApplicationFromDesktopFile(appId);
103 var application;
104 application = applicationManager.activateApplication(appId, argument);
105 if (application == null) {
106 return;
107 }
108 if (application.stage == ApplicationInfo.MainStage || !sideStage.enabled) {
109 mainStage.activateApplication(appId, addDelay);
110 } else {
111 sideStage.activateApplication(appId, addDelay);
112 }
113 stages.show();
114 }
115 }
117 GSettings {
118 id: backgroundSettings
119 schema.id: "org.gnome.desktop.background"
120 }
121 property url gSettingsPicture: backgroundSettings.pictureUri != undefined && backgroundSettings.pictureUri.length > 0 ? backgroundSettings.pictureUri : shell.defaultBackground
122 onGSettingsPictureChanged: {
123 shell.background = gSettingsPicture
124 }
126 // This is a dummy image that is needed to determine if the picture url
127 // in backgroundSettings points to a valid picture file.
128 // We can't do this with the real background image because setting a
129 // new source in onStatusChanged triggers a binding loop detection
130 // inside Image, which causes it not to render even though a valid source
131 // would be set. We don't mind about this image staying black and just
132 // use it for verification to populate the source for the real
133 // background image.
134 Image {
135 source: shell.background
136 height: 0
137 width: 0
138 sourceSize.height: 0
139 sourceSize.width: 0
140 onStatusChanged: {
141 if (status == Image.Error && source != shell.defaultBackground) {
142 shell.background = defaultBackground
143 }
144 }
145 }
147 VolumeControl {
148 id: volumeControl
149 }
151 Keys.onVolumeUpPressed: volumeControl.volumeUp()
152 Keys.onVolumeDownPressed: volumeControl.volumeDown()
154 Item {
155 id: underlay
156 objectName: "underlay"
157 anchors.fill: parent
159 // Whether the underlay is fully covered by opaque UI elements.
160 property bool fullyCovered: panel.indicators.fullyOpened && shell.width <= panel.indicatorsMenuWidth
162 readonly property bool applicationRunning: ((mainStage.applications && mainStage.applications.count > 0)
163 || (sideStage.applications && sideStage.applications.count > 0))
165 // Whether the user should see the topmost application surface (if there's one at all).
166 readonly property bool applicationSurfaceShouldBeSeen: applicationRunning && !stages.fullyHidden
167 && !mainStage.usingScreenshots // but want sideStage animating over app surface
171 // NB! Application surfaces are stacked behing the shell one. So they can only be seen by the user
172 // through the translucent parts of the shell surface.
173 visible: !fullyCovered && !applicationSurfaceShouldBeSeen
175 CrossFadeImage {
176 id: backgroundImage
177 objectName: "backgroundImage"
179 anchors.fill: parent
180 source: shell.background
181 fillMode: Image.PreserveAspectCrop
182 }
184 Rectangle {
185 anchors.fill: parent
186 color: "black"
187 opacity: dash.disappearingAnimationProgress
188 }
190 Dash {
191 id: dash
192 objectName: "dash"
194 available: !greeter.shown && !lockscreen.shown
195 hides: [stages, launcher, panel.indicators]
196 shown: disappearingAnimationProgress !== 1.0
197 enabled: disappearingAnimationProgress === 0.0 && edgeDemo.dashEnabled
198 // FIXME: unfocus all applications when going back to the dash
199 onEnabledChanged: {
200 if (enabled) {
201 shell.applicationManager.unfocusCurrentApplication()
202 }
203 }
205 anchors {
206 fill: parent
207 topMargin: panel.panelHeight
208 }
210 contentScale: 1.0 - 0.2 * disappearingAnimationProgress
211 opacity: 1.0 - disappearingAnimationProgress
212 property real disappearingAnimationProgress: {
213 if (greeter.shown) {
214 return greeter.showProgress;
215 } else {
216 return stagesOuterContainer.showProgress;
217 }
218 }
220 // FIXME: only necessary because stagesOuterContainer.showProgress and
221 // greeterRevealer.animatedProgress are not animated
222 Behavior on disappearingAnimationProgress { SmoothedAnimation { velocity: 5 }}
223 }
224 }
226 Item {
227 id: stagesOuterContainer
229 width: parent.width
230 height: parent.height
231 x: launcher.progress
232 Behavior on x {SmoothedAnimation{velocity: 600}}
234 property real showProgress:
235 MathUtils.clamp(1 - (x + stages.x) / shell.width, 0, 1)
237 Showable {
238 id: stages
239 objectName: "stages"
241 x: width
243 property bool fullyShown: shown && x == 0 && parent.x == 0
244 property bool fullyHidden: !shown && x == width
245 available: !greeter.shown
246 hides: [panel.indicators]
247 shown: false
248 opacity: 1.0
249 showAnimation: StandardAnimation { property: "x"; duration: 350; to: 0; easing.type: Easing.OutCubic }
250 hideAnimation: StandardAnimation { property: "x"; duration: 350; to: width; easing.type: Easing.OutCubic }
252 width: parent.width
253 height: parent.height
255 // close the stages when no focused application remains
256 Connections {
257 target: shell.applicationManager
258 onMainStageFocusedApplicationChanged: stages.closeIfNoApplications()
259 onSideStageFocusedApplicationChanged: stages.closeIfNoApplications()
260 ignoreUnknownSignals: true
261 }
263 function closeIfNoApplications() {
264 if (!shell.applicationManager.mainStageFocusedApplication
265 && !shell.applicationManager.sideStageFocusedApplication
266 && shell.applicationManager.mainStageApplications.count == 0
267 && shell.applicationManager.sideStageApplications.count == 0) {
268 stages.hide();
269 }
270 }
272 // show the stages when an application gets the focus
273 Connections {
274 target: shell.applicationManager
275 onMainStageFocusedApplicationChanged: {
276 if (shell.applicationManager.mainStageFocusedApplication) {
277 mainStage.show();
278 stages.show();
279 }
280 }
281 onSideStageFocusedApplicationChanged: {
282 if (shell.applicationManager.sideStageFocusedApplication) {
283 sideStage.show();
284 stages.show();
285 }
286 }
287 ignoreUnknownSignals: true
288 }
290 Stage {
291 id: mainStage
293 anchors.fill: parent
294 fullyShown: stages.fullyShown
295 fullyHidden: stages.fullyHidden
296 shouldUseScreenshots: !fullyShown
297 rightEdgeEnabled: !sideStage.enabled
299 applicationManager: shell.applicationManager
300 rightEdgeDraggingAreaWidth: shell.edgeSize
301 normalApplicationY: shell.panelHeight
303 shown: true
304 function show() {
305 stages.show();
306 }
307 function hide() {
308 }
310 // FIXME: workaround the fact that focusing a main stage application
311 // raises its surface on top of all other surfaces including the ones
312 // that belong to side stage applications.
313 onFocusedApplicationChanged: {
314 if (focusedApplication && sideStage.focusedApplication && sideStage.fullyShown) {
315 shell.applicationManager.focusApplication(sideStage.focusedApplication);
316 }
317 }
318 }
320 SideStage {
321 id: sideStage
323 applicationManager: shell.applicationManager
324 rightEdgeDraggingAreaWidth: shell.edgeSize
325 normalApplicationY: shell.panelHeight
327 onShownChanged: {
328 if (!shown && mainStage.applications.count == 0) {
329 stages.hide();
330 }
331 }
332 // FIXME: when hiding the side stage, refocus the main stage
333 // application so that it goes in front of the side stage
334 // application and hides it
335 onFullyShownChanged: {
336 if (!fullyShown && stages.fullyShown && sideStage.focusedApplication != null) {
337 shell.applicationManager.focusApplication(mainStage.focusedApplication);
338 }
339 }
341 enabled: shell.width >= units.gu(100)
342 visible: enabled
343 fullyShown: stages.fullyShown && shown
344 && sideStage[sideStageRevealer.boundProperty] == sideStageRevealer.openedValue
345 shouldUseScreenshots: !fullyShown || mainStage.usingScreenshots || sideStageRevealer.pressed
347 available: !greeter.shown && !lockscreen.shown && enabled
348 hides: [launcher, panel.indicators]
349 shown: false
350 showAnimation: StandardAnimation { property: "x"; duration: 350; to: sideStageRevealer.openedValue; easing.type: Easing.OutQuint }
351 hideAnimation: StandardAnimation { property: "x"; duration: 350; to: sideStageRevealer.closedValue; easing.type: Easing.OutQuint }
353 width: units.gu(40)
354 height: stages.height
355 handleExpanded: sideStageRevealer.pressed
356 }
358 Revealer {
359 id: sideStageRevealer
361 enabled: mainStage.applications.count > 0 && sideStage.applications.count > 0
362 && sideStage.available
363 direction: Qt.RightToLeft
364 openedValue: parent.width - sideStage.width
365 hintDisplacement: units.gu(3)
366 /* The size of the sidestage handle needs to be bigger than the
367 typical size used for edge detection otherwise it is really
368 hard to grab.
369 */
370 handleSize: sideStage.shown ? units.gu(4) : shell.edgeSize
371 closedValue: parent.width + sideStage.handleSizeCollapsed
372 target: sideStage
373 x: parent.width - width
374 width: sideStage.width + handleSize * 0.7
375 height: sideStage.height
376 orientation: Qt.Horizontal
377 }
379 DragHandle {
380 id: stagesDragHandle
382 anchors.top: parent.top
383 anchors.bottom: parent.bottom
384 anchors.right: parent.left
386 width: shell.edgeSize
387 direction: Direction.Leftwards
388 enabled: greeter.showProgress == 0 && edgeDemo.dashEnabled
389 property bool haveApps: mainStage.applications.count > 0 || sideStage.applications.count > 0
391 maxTotalDragDistance: haveApps ? parent.width : parent.width * 0.7
392 // Make autocompletion impossible when !haveApps
393 edgeDragEvaluator.minDragDistance: haveApps ? maxTotalDragDistance * 0.1 : Number.MAX_VALUE
394 }
395 }
396 }
398 Lockscreen {
399 id: lockscreen
400 objectName: "lockscreen"
402 readonly property int backgroundTopMargin: -panel.panelHeight
404 hides: [launcher, panel.indicators]
405 shown: false
406 enabled: true
407 showAnimation: StandardAnimation { property: "opacity"; to: 1 }
408 hideAnimation: StandardAnimation { property: "opacity"; to: 0 }
409 y: panel.panelHeight
410 x: required ? 0 : - width
411 width: parent.width
412 height: parent.height - panel.panelHeight
413 background: shell.background
414 pinLength: 4
416 onEntered: LightDM.Greeter.respond(passphrase);
417 onCancel: greeter.show()
419 Component.onCompleted: {
420 if (LightDM.Users.count == 1) {
421 LightDM.Greeter.authenticate(LightDM.Users.data(0, LightDM.UserRoles.NameRole))
422 }
423 }
424 }
426 Connections {
427 target: LightDM.Greeter
429 onShowPrompt: {
430 if (LightDM.Users.count == 1) {
431 // TODO: There's no better way for now to determine if its a PIN or a passphrase.
432 if (text == "PIN") {
433 lockscreen.alphaNumeric = false
434 } else {
435 lockscreen.alphaNumeric = true
436 }
437 lockscreen.placeholderText = i18n.tr("Please enter %1").arg(text);
438 lockscreen.show();
439 }
440 }
442 onAuthenticationComplete: {
443 if (LightDM.Greeter.promptless) {
444 return;
445 }
446 if (LightDM.Greeter.authenticated) {
447 lockscreen.hide();
448 } else {
449 lockscreen.clear(true);
450 }
451 }
452 }
454 Greeter {
455 id: greeter
456 objectName: "greeter"
458 available: true
459 hides: [launcher, panel.indicators]
460 shown: true
462 defaultBackground: shell.background
464 y: panel.panelHeight
465 width: parent.width
466 height: parent.height - panel.panelHeight
468 dragHandleWidth: shell.edgeSize
470 property var previousMainApp: null
471 property var previousSideApp: null
473 function removeApplicationFocus() {
474 greeter.previousMainApp = applicationManager.mainStageFocusedApplication;
475 greeter.previousSideApp = applicationManager.sideStageFocusedApplication;
476 applicationManager.unfocusCurrentApplication();
477 }
479 function restoreApplicationFocus() {
480 if (greeter.previousMainApp) {
481 applicationManager.focusApplication(greeter.previousMainApp);
482 greeter.previousMainApp = null;
483 }
484 if (greeter.previousSideApp) {
485 applicationManager.focusApplication(greeter.previousSideApp);
486 greeter.previousSideApp = null;
487 }
488 }
490 onShownChanged: {
491 if (shown) {
492 lockscreen.reset();
493 // If there is only one user, we start authenticating with that one here.
494 // If there are more users, the Greeter will handle that
495 if (LightDM.Users.count == 1) {
496 LightDM.Greeter.authenticate(LightDM.Users.data(0, LightDM.UserRoles.NameRole));
497 }
498 greeter.forceActiveFocus();
499 removeApplicationFocus();
500 } else {
501 restoreApplicationFocus();
502 }
503 }
505 onUnlocked: greeter.hide()
506 onSelected: {
507 // Update launcher items for new user
508 var user = LightDM.Users.data(uid, LightDM.UserRoles.NameRole);
509 AccountsService.user = user;
510 LauncherModel.setUser(user);
511 }
513 onLeftTeaserPressedChanged: {
514 if (leftTeaserPressed) {
515 launcher.tease();
516 }
517 }
519 Connections {
520 target: applicationManager
521 ignoreUnknownSignals: true
522 // If any app is focused when greeter is open, it's due to a user action
523 // like a snap decision (say, an incoming call).
524 // TODO: these should be protected to only unlock for certain applications / certain usecases
525 // potentially only in connection with a notification.
526 onMainStageFocusedApplicationChanged: {
527 if (greeter.shown && applicationManager.mainStageFocusedApplication) {
528 greeter.previousMainApp = null // make way for new focused app
529 greeter.previousSideApp = null
530 greeter.hide()
531 }
532 }
533 onSideStageFocusedApplicationChanged: {
534 if (greeter.shown && applicationManager.sideStageFocusedApplication) {
535 greeter.previousMainApp = null // make way for new focused app
536 greeter.previousSideApp = null
537 greeter.hide()
538 }
539 }
540 }
541 }
543 InputFilterArea {
544 anchors.fill: parent
545 blockInput: !applicationFocused || greeter.shown || lockscreen.shown || launcher.shown
546 || panel.indicators.shown
547 }
549 Connections {
550 id: powerConnection
551 target: Powerd
553 onDisplayPowerStateChange: {
554 // We ignore any display-off signals when the proximity sensor
555 // is active. This usually indicates something like a phone call.
556 if (status == Powerd.Off && (flags & Powerd.UseProximity) == 0) {
557 greeter.showNow();
558 }
560 // No reason to chew demo CPU when user isn't watching
561 if (status == Powerd.Off) {
562 edgeDemo.paused = true;
563 } else if (status == Powerd.On) {
564 edgeDemo.paused = false;
565 }
566 }
567 }
569 function showHome() {
570 var animate = !greeter.shown && !stages.shown
571 greeter.hide()
572 dash.setCurrentScope("home.scope", animate, false)
573 stages.hide()
574 }
576 function hideIndicatorMenu(delay) {
577 panel.hideIndicatorMenu(delay);
578 }
580 Item {
581 id: overlay
583 anchors.fill: parent
585 Panel {
586 id: panel
587 anchors.fill: parent //because this draws indicator menus
588 indicatorsMenuWidth: parent.width > units.gu(60) ? units.gu(40) : parent.width
589 indicators {
590 hides: [launcher]
591 available: edgeDemo.panelEnabled
592 contentEnabled: edgeDemo.panelContentEnabled
593 }
594 fullscreenMode: shell.fullscreenMode
595 searchVisible: !greeter.shown && !lockscreen.shown && dash.shown && dash.searchable
597 InputFilterArea {
598 anchors {
599 top: parent.top
600 left: parent.left
601 right: parent.right
602 }
603 height: (panel.fullscreenMode) ? shell.edgeSize : panel.panelHeight
604 blockInput: true
605 }
606 }
608 InputFilterArea {
609 blockInput: launcher.shown
610 anchors {
611 top: parent.top
612 bottom: parent.bottom
613 left: parent.left
614 }
615 width: launcher.width
616 }
618 Launcher {
619 id: launcher
621 readonly property bool dashSwipe: progress > 0
623 anchors.top: parent.top
624 anchors.bottom: parent.bottom
625 width: parent.width
626 dragAreaWidth: shell.edgeSize
627 available: (!greeter.shown || greeter.narrowMode) && edgeDemo.launcherEnabled
629 onShowDashHome: {
630 if (edgeDemo.running)
631 return;
633 showHome()
634 }
635 onDash: {
636 if (stages.shown) {
637 stages.hide();
638 launcher.hide();
639 }
640 }
641 onDashSwipeChanged: if (dashSwipe && stages.shown) dash.setCurrentScope("applications.scope", false, true)
642 onLauncherApplicationSelected:{
643 if (edgeDemo.running)
644 return;
646 greeter.hide()
647 shell.activateApplication(appId)
648 }
649 onShownChanged: {
650 if (shown) {
651 panel.indicators.hide()
652 }
653 }
654 }
656 Notifications {
657 id: notifications
659 model: NotificationBackend.Model
660 margin: units.gu(1)
662 anchors {
663 top: parent.top
664 right: parent.right
665 bottom: parent.bottom
666 topMargin: panel.panelHeight
667 }
668 states: [
669 State {
670 name: "narrow"
671 when: overlay.width <= units.gu(60)
672 AnchorChanges { target: notifications; anchors.left: parent.left }
673 },
674 State {
675 name: "wide"
676 when: overlay.width > units.gu(60)
677 AnchorChanges { target: notifications; anchors.left: undefined }
678 PropertyChanges { target: notifications; width: units.gu(38) }
679 }
680 ]
682 InputFilterArea {
683 anchors { left: parent.left; right: parent.right }
684 height: parent.contentHeight
685 blockInput: height > 0
686 }
687 }
688 }
690 focus: true
691 onFocusChanged: if (!focus) forceActiveFocus();
693 InputFilterArea {
694 anchors {
695 top: parent.top
696 bottom: parent.bottom
697 left: parent.left
698 }
699 width: shell.edgeSize
700 blockInput: true
701 }
703 InputFilterArea {
704 anchors {
705 top: parent.top
706 bottom: parent.bottom
707 right: parent.right
708 }
709 width: shell.edgeSize
710 blockInput: true
711 }
713 Binding {
714 target: i18n
715 property: "domain"
716 value: "unity8"
717 }
719 OSKController {
720 anchors.topMargin: panel.panelHeight
721 anchors.fill: parent // as needs to know the geometry of the shell
722 }
724 //FIXME: This should be handled in the input stack, keyboard shouldnt propagate
725 MouseArea {
726 anchors.bottom: parent.bottom
727 anchors.left: parent.left
728 anchors.right: parent.right
729 height: shell.applicationManager ? shell.applicationManager.keyboardHeight : 0
731 enabled: shell.applicationManager && shell.applicationManager.keyboardVisible
732 }
734 Label {
735 anchors.centerIn: parent
736 visible: applicationManager.fake
737 text: "EARLY ALPHA\nNOT READY FOR USE"
738 color: "lightgrey"
739 opacity: 0.2
740 font.weight: Font.Black
741 horizontalAlignment: Text.AlignHCenter
742 verticalAlignment: Text.AlignVCenter
743 fontSizeMode: Text.Fit
744 rotation: -45
745 scale: Math.min(parent.width, parent.height) / width
746 }
748 EdgeDemo {
749 id: edgeDemo
750 greeter: greeter
751 launcher: launcher
752 dash: dash
753 indicators: panel.indicators
754 underlay: underlay
755 }
757 Connections {
758 target: SessionBroadcast
759 onShowHome: showHome()
760 }