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