on 799aff2 help sign in
unity8/Shell.qml
753 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.sideStageFocusedApplication
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 || hud.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 }
593 }
595 Revealer {
596 id: hudRevealer
598 enabled: hud.shown
599 width: hud.width
600 anchors.left: hud.left
601 height: parent.height
602 target: hud.revealerTarget
603 closedValue: height
604 openedValue: 0
605 direction: Qt.RightToLeft
606 orientation: Qt.Vertical
607 handleSize: hud.handleHeight
608 onCloseClicked: target.hide()
609 }
611 Bottombar {
612 theHud: hud
613 anchors.fill: parent
614 enabled: !panel.indicators.shown
615 applicationIsOnForeground: applicationFocused
616 }
618 InputFilterArea {
619 blockInput: launcher.shown
620 anchors {
621 top: parent.top
622 bottom: parent.bottom
623 left: parent.left
624 }
625 width: launcher.width
626 }
628 Launcher {
629 id: launcher
631 anchors.top: parent.top
632 anchors.bottom: parent.bottom
633 width: parent.width
634 dragAreaWidth: shell.edgeSize
635 available: (!greeter.shown || greeter.narrowMode) && edgeDemo.launcherEnabled
636 onDashItemSelected: {
637 greeter.hide()
638 // Animate if moving between application and dash
639 if (!stages.shown) {
640 dash.setCurrentScope("home.scope", true, false)
641 } else {
642 dash.setCurrentScope("home.scope", false, false)
643 }
644 stages.hide();
645 }
646 onDash: {
647 if (stages.shown) {
648 dash.setCurrentScope("applications.scope", true, false)
649 stages.hide();
650 launcher.hide();
651 }
652 }
653 onLauncherApplicationSelected:{
654 greeter.hide()
655 shell.activateApplication(desktopFile)
656 }
657 onShownChanged: {
658 if (shown) {
659 panel.indicators.hide()
660 hud.hide()
661 }
662 }
663 }
665 Notifications {
666 id: notifications
668 model: NotificationBackend.Model
669 anchors {
670 top: parent.top
671 right: parent.right
672 bottom: parent.bottom
673 leftMargin: units.gu(1)
674 rightMargin: units.gu(1)
675 topMargin: panel.panelHeight + units.gu(1)
676 }
677 states: [
678 State {
679 name: "narrow"
680 when: overlay.width <= units.gu(60)
681 AnchorChanges { target: notifications; anchors.left: parent.left }
682 },
683 State {
684 name: "wide"
685 when: overlay.width > units.gu(60)
686 AnchorChanges { target: notifications; anchors.left: undefined }
687 PropertyChanges { target: notifications; width: units.gu(38) }
688 }
689 ]
690 }
691 }
693 focus: true
695 InputFilterArea {
696 anchors {
697 top: parent.top
698 bottom: parent.bottom
699 left: parent.left
700 }
701 width: shell.edgeSize
702 blockInput: true
703 }
705 InputFilterArea {
706 anchors {
707 top: parent.top
708 bottom: parent.bottom
709 right: parent.right
710 }
711 width: shell.edgeSize
712 blockInput: true
713 }
715 Binding {
716 target: i18n
717 property: "domain"
718 value: "unity8"
719 }
721 //FIXME: This should be handled in the input stack, keyboard shouldnt propagate
722 MouseArea {
723 anchors.bottom: parent.bottom
724 anchors.left: parent.left
725 anchors.right: parent.right
726 height: shell.applicationManager ? shell.applicationManager.keyboardHeight : 0
728 enabled: shell.applicationManager && shell.applicationManager.keyboardVisible
729 }
731 Label {
732 anchors.centerIn: parent
733 visible: applicationManager.fake
734 text: "EARLY ALPHA\nNOT READY FOR USE"
735 color: "lightgrey"
736 opacity: 0.2
737 font.weight: Font.Black
738 horizontalAlignment: Text.AlignHCenter
739 verticalAlignment: Text.AlignVCenter
740 fontSizeMode: Text.Fit
741 rotation: -45
742 scale: Math.min(parent.width, parent.height) / width
743 }
745 EdgeDemo {
746 id: edgeDemo
747 greeter: greeter
748 launcher: launcher
749 dash: dash
750 indicators: panel.indicators
751 underlay: underlay
752 }