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