on 13f6ed1 help sign in
unity8/qml/Shell.qml
734 lines 25 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 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.SystemImage 0.1
26import Ubuntu.Telephony 0.1 as Telephony
27import Unity.Connectivity 0.1
28import Unity.Launcher 0.1
29import Utils 0.1
30import LightDM 0.1 as LightDM
31import Powerd 0.1
32import SessionBroadcast 0.1
33import "Greeter"
34import "Launcher"
35import "Panel"
36import "Components"
37import "Notifications"
38import "Stages"
39import "Panel/Indicators"
40import Unity.Notifications 1.0 as NotificationBackend
41import Unity.Session 0.1
42import Unity.DashCommunicator 0.1
44Item {
45 id: shell
47 // this is only here to select the width / height of the window if not running fullscreen
48 property bool tablet: false
49 width: tablet ? units.gu(160) : applicationArguments.hasGeometry() ? applicationArguments.width() : units.gu(40)
50 height: tablet ? units.gu(100) : applicationArguments.hasGeometry() ? applicationArguments.height() : units.gu(71)
52 property real edgeSize: units.gu(2)
53 property url defaultBackground: Qt.resolvedUrl(shell.width >= units.gu(60) ? "graphics/tablet_background.jpg" : "graphics/phone_background.jpg")
54 property url background
55 readonly property real panelHeight: panel.panelHeight
57 readonly property bool locked: LightDM.Greeter.active && !LightDM.Greeter.authenticated
59 property bool sideStageEnabled: shell.width >= units.gu(100)
60 readonly property string focusedApplicationId: ApplicationManager.focusedApplicationId
62 property int maxFailedLogins: -1 // disabled by default for now, will enable via settings in future
63 property int failedLoginsDelayAttempts: 7 // number of failed logins
64 property int failedLoginsDelayMinutes: 5 // minutes of forced waiting
66 property int orientation
67 readonly property int deviceOrientationAngle: Screen.angleBetween(Screen.primaryOrientation, Screen.orientation)
68 onDeviceOrientationAngleChanged: {
69 if (!OrientationLock.enabled) {
70 orientation = Screen.orientation;
71 }
72 }
73 readonly property bool orientationLockEnabled: OrientationLock.enabled
74 onOrientationLockEnabledChanged: {
75 if (orientationLockEnabled) {
76 OrientationLock.savedOrientation = Screen.orientation;
77 } else {
78 orientation = Screen.orientation;
79 }
80 }
82 function activateApplication(appId) {
83 if (ApplicationManager.findApplication(appId)) {
84 ApplicationManager.requestFocusApplication(appId);
85 } else {
86 var execFlags = shell.sideStageEnabled ? ApplicationManager.NoFlag : ApplicationManager.ForceMainStage;
87 ApplicationManager.startApplication(appId, execFlags);
88 }
89 }
91 function setFakeActiveForApp(app) {
92 if (shell.locked) {
93 greeter.fakeActiveForApp = app
94 lockscreen.hide()
95 }
96 }
98 Binding {
99 target: LauncherModel
100 property: "applicationManager"
101 value: ApplicationManager
102 }
104 Component.onCompleted: {
105 Theme.name = "Ubuntu.Components.Themes.SuruGradient"
106 if (ApplicationManager.count > 0) {
107 ApplicationManager.focusApplication(ApplicationManager.get(0).appId);
108 }
109 if (orientationLockEnabled) {
110 orientation = OrientationLock.savedOrientation;
111 }
112 }
114 GSettings {
115 id: backgroundSettings
116 schema.id: "org.gnome.desktop.background"
117 }
118 property url gSettingsPicture: backgroundSettings.pictureUri != undefined && backgroundSettings.pictureUri.length > 0 ? backgroundSettings.pictureUri : shell.defaultBackground
119 onGSettingsPictureChanged: {
120 shell.background = gSettingsPicture
121 }
123 VolumeControl {
124 id: volumeControl
125 }
127 DashCommunicator {
128 id: dash
129 objectName: "dashCommunicator"
130 }
132 WindowKeysFilter {
133 // Handle but do not filter out volume keys
134 Keys.onVolumeUpPressed: { volumeControl.volumeUp(); event.accepted = false; }
135 Keys.onVolumeDownPressed: { volumeControl.volumeDown(); event.accepted = false; }
137 Keys.onPressed: {
138 if (event.key == Qt.Key_PowerOff || event.key == Qt.Key_PowerDown) {
139 dialogs.onPowerKeyPressed();
140 event.accepted = true;
141 } else {
142 event.accepted = false;
143 }
144 }
146 Keys.onReleased: {
147 if (event.key == Qt.Key_PowerOff || event.key == Qt.Key_PowerDown) {
148 dialogs.onPowerKeyReleased();
149 event.accepted = true;
150 } else {
151 event.accepted = false;
152 }
153 }
154 }
156 Item {
157 id: stages
158 objectName: "stages"
159 width: parent.width
160 height: parent.height
161 visible: !ApplicationManager.empty
163 Connections {
164 target: ApplicationManager
165 onFocusRequested: {
166 if (appId === "dialer-app") {
167 // Always let the dialer-app through. Either user asked
168 // for an emergency call or accepted an incoming call.
169 setFakeActiveForApp(appId)
170 } else if (greeter.fakeActiveForApp !== "" && greeter.fakeActiveForApp !== appId) {
171 lockscreen.show();
172 }
173 greeter.hide();
174 launcher.hide();
175 }
177 onFocusedApplicationIdChanged: {
178 if (greeter.fakeActiveForApp !== "" && greeter.fakeActiveForApp !== ApplicationManager.focusedApplicationId) {
179 lockscreen.show();
180 }
181 panel.indicators.hide();
182 }
184 onApplicationAdded: {
185 if (greeter.shown && appId != "unity8-dash") {
186 greeter.hide();
187 }
188 if (appId === "dialer-app") {
189 // Always let the dialer-app through. Either user asked
190 // for an emergency call or accepted an incoming call.
191 setFakeActiveForApp(appId)
192 }
193 launcher.hide();
194 }
195 }
197 Loader {
198 id: applicationsDisplayLoader
199 anchors.fill: parent
201 source: shell.sideStageEnabled ? "Stages/TabletStage.qml" : "Stages/PhoneStage.qml"
203 Binding {
204 target: applicationsDisplayLoader.item
205 property: "objectName"
206 value: "stage"
207 }
208 Binding {
209 target: applicationsDisplayLoader.item
210 property: "dragAreaWidth"
211 value: shell.edgeSize
212 }
213 Binding {
214 target: applicationsDisplayLoader.item
215 property: "maximizedAppTopMargin"
216 // Not just using panel.panelHeight as that changes depending on the focused app.
217 value: panel.indicators.minimizedPanelHeight + units.dp(2) // dp(2) for orange line
218 }
219 Binding {
220 target: applicationsDisplayLoader.item
221 property: "interactive"
222 value: edgeDemo.stagesEnabled && !greeter.shown && !lockscreen.shown && panel.indicators.fullyClosed && launcher.progress == 0
223 }
224 Binding {
225 target: applicationsDisplayLoader.item
226 property: "spreadEnabled"
227 value: edgeDemo.stagesEnabled && greeter.fakeActiveForApp === "" // to support emergency dialer hack
228 }
229 Binding {
230 target: applicationsDisplayLoader.item
231 property: "inverseProgress"
232 value: launcher.progress
233 }
234 Binding {
235 target: applicationsDisplayLoader.item
236 property: "orientation"
237 value: shell.orientation
238 }
239 }
240 }
242 InputMethod {
243 id: inputMethod
244 objectName: "inputMethod"
245 anchors { fill: parent; topMargin: panel.panelHeight }
246 z: notifications.useModal || panel.indicators.shown ? overlay.z + 1 : overlay.z - 1
247 }
249 Connections {
250 target: SurfaceManager
251 onSurfaceCreated: {
252 if (surface.type == MirSurfaceItem.InputMethod) {
253 inputMethod.surface = surface;
254 }
255 }
257 onSurfaceDestroyed: {
258 if (inputMethod.surface == surface) {
259 inputMethod.surface = null;
260 surface.parent = null;
261 }
262 if (!surface.parent) {
263 // there's no one displaying it. delete it right away
264 surface.release();
265 }
266 }
267 }
268 Connections {
269 target: SessionManager
270 onSessionStopping: {
271 if (!session.parentSession && !session.application) {
272 // nothing is using it. delete it right away
273 session.release();
274 }
275 }
276 }
278 Lockscreen {
279 id: lockscreen
280 objectName: "lockscreen"
282 hides: [launcher, panel.indicators]
283 shown: false
284 enabled: true
285 showAnimation: StandardAnimation { property: "opacity"; to: 1 }
286 hideAnimation: StandardAnimation { property: "opacity"; to: 0 }
287 y: panel.panelHeight
288 visible: required
289 width: parent.width
290 height: parent.height - panel.panelHeight
291 background: shell.background
292 alphaNumeric: AccountsService.passwordDisplayHint === AccountsService.Keyboard
293 minPinLength: 4
294 maxPinLength: 4
296 // FIXME: We *should* show emergency dialer if there is a SIM present,
297 // regardless of whether the side stage is enabled. But right now,
298 // the assumption is that narrow screens are phones which have SIMs
299 // and wider screens are tablets which don't. When we do allow this
300 // on devices with a side stage and a SIM, work should be done to
301 // ensure that the main stage is disabled while the dialer is present
302 // in the side stage.
303 showEmergencyCallButton: !shell.sideStageEnabled
305 onEntered: LightDM.Greeter.respond(passphrase);
306 onCancel: greeter.show()
307 onEmergencyCall: shell.activateApplication("dialer-app") // will automatically enter fake-active mode
309 onShownChanged: if (shown) greeter.fakeActiveForApp = ""
311 Timer {
312 id: forcedDelayTimer
313 interval: 1000 * 60
314 onTriggered: {
315 if (lockscreen.delayMinutes > 0) {
316 lockscreen.delayMinutes -= 1
317 if (lockscreen.delayMinutes > 0) {
318 start() // go again
319 }
320 }
321 }
322 }
324 Component.onCompleted: {
325 if (greeter.narrowMode) {
326 LightDM.Greeter.authenticate(LightDM.Users.data(0, LightDM.UserRoles.NameRole))
327 }
328 }
329 }
331 Connections {
332 target: LightDM.Greeter
334 onShowGreeter: greeter.show()
335 onHideGreeter: greeter.login()
337 onShowPrompt: {
338 if (greeter.narrowMode) {
339 if (isDefaultPrompt) {
340 if (lockscreen.alphaNumeric) {
341 lockscreen.infoText = i18n.tr("Enter passphrase")
342 lockscreen.errorText = i18n.tr("Sorry, incorrect passphrase") + "\n" +
343 i18n.tr("Please re-enter")
344 } else {
345 lockscreen.infoText = i18n.tr("Enter passcode")
346 lockscreen.errorText = i18n.tr("Sorry, incorrect passcode")
347 }
348 } else {
349 lockscreen.infoText = i18n.tr("Enter %1").arg(text.toLowerCase())
350 lockscreen.errorText = i18n.tr("Sorry, incorrect %1").arg(text.toLowerCase())
351 }
353 lockscreen.show();
354 }
355 }
357 onPromptlessChanged: {
358 if (LightDM.Greeter.promptless && LightDM.Greeter.authenticated) {
359 lockscreen.hide()
360 } else {
361 lockscreen.reset();
362 lockscreen.show();
363 }
364 }
366 onAuthenticationComplete: {
367 if (LightDM.Greeter.authenticated) {
368 AccountsService.failedLogins = 0
369 }
370 // Else only penalize user for a failed login if they actually were
371 // prompted for a password. We do this below after the promptless
372 // early exit.
374 if (LightDM.Greeter.promptless) {
375 return;
376 }
378 if (LightDM.Greeter.authenticated) {
379 greeter.login();
380 } else {
381 AccountsService.failedLogins++
382 if (maxFailedLogins >= 2) { // require at least a warning
383 if (AccountsService.failedLogins === maxFailedLogins - 1) {
384 var title = lockscreen.alphaNumeric ?
385 i18n.tr("Sorry, incorrect passphrase.") :
386 i18n.tr("Sorry, incorrect passcode.")
387 var text = i18n.tr("This will be your last attempt.") + " " +
388 (lockscreen.alphaNumeric ?
389 i18n.tr("If passphrase is entered incorrectly, your phone will conduct a factory reset and all personal data will be deleted.") :
390 i18n.tr("If passcode is entered incorrectly, your phone will conduct a factory reset and all personal data will be deleted."))
391 lockscreen.showInfoPopup(title, text)
392 } else if (AccountsService.failedLogins >= maxFailedLogins) {
393 SystemImage.factoryReset() // Ouch!
394 }
395 }
396 if (failedLoginsDelayAttempts > 0 && AccountsService.failedLogins % failedLoginsDelayAttempts == 0) {
397 lockscreen.delayMinutes = failedLoginsDelayMinutes
398 forcedDelayTimer.start()
399 }
401 lockscreen.clear(true);
402 if (greeter.narrowMode) {
403 LightDM.Greeter.authenticate(LightDM.Users.data(0, LightDM.UserRoles.NameRole))
404 }
405 }
406 }
407 }
409 Binding {
410 target: LightDM.Greeter
411 property: "active"
412 value: greeter.shown || lockscreen.shown || greeter.fakeActiveForApp != ""
413 }
415 Rectangle {
416 anchors.fill: parent
417 color: "black"
418 opacity: greeterWrapper.showProgress * 0.8
419 }
421 Item {
422 // Just a tiny wrapper to adjust greeter's x without messing with its own dragging
423 id: greeterWrapper
424 x: launcher.progress
425 y: panel.panelHeight
426 width: parent.width
427 height: parent.height - panel.panelHeight
429 Behavior on x {
430 enabled: !launcher.dashSwipe
431 StandardAnimation {}
432 }
434 property bool fullyShown: showProgress === 1.0
435 onFullyShownChanged: {
436 // Wait until the greeter is completely covering lockscreen before resetting it.
437 if (fullyShown && !LightDM.Greeter.authenticated) {
438 lockscreen.reset();
439 lockscreen.show();
440 }
441 }
443 readonly property real showProgress: MathUtils.clamp((1 - x/width) + greeter.showProgress - 1, 0, 1)
444 onShowProgressChanged: {
445 if (showProgress === 0) {
446 if (LightDM.Greeter.authenticated) {
447 greeter.login()
448 } else if (greeter.narrowMode) {
449 lockscreen.clear(false) // to reset focus if necessary
450 }
451 }
452 }
454 Greeter {
455 id: greeter
456 objectName: "greeter"
458 signal sessionStarted() // helpful for tests
460 property string fakeActiveForApp: ""
462 available: true
463 hides: [launcher, panel.indicators]
464 shown: true
465 loadContent: required || lockscreen.required // keeps content in memory for quick show()
467 defaultBackground: shell.background
469 width: parent.width
470 height: parent.height
472 dragHandleWidth: shell.edgeSize
474 function login() {
475 enabled = false;
476 if (LightDM.Greeter.startSessionSync()) {
477 sessionStarted();
478 greeter.hide();
479 lockscreen.hide();
480 launcher.hide();
481 }
482 enabled = true;
483 }
485 onShownChanged: {
486 if (shown) {
487 if (greeter.narrowMode) {
488 LightDM.Greeter.authenticate(LightDM.Users.data(0, LightDM.UserRoles.NameRole));
489 }
490 greeter.fakeActiveForApp = "";
491 greeter.forceActiveFocus();
492 }
493 }
495 /* TODO re-enable when the corresponding changes in the service land (LP: #1361074)
496 Component.onCompleted: {
497 Connectivity.unlockAllModems()
498 } */
500 onUnlocked: greeter.hide()
501 onSelected: {
502 // Update launcher items for new user
503 var user = LightDM.Users.data(uid, LightDM.UserRoles.NameRole);
504 AccountsService.user = user;
505 LauncherModel.setUser(user);
506 }
508 onTease: launcher.tease()
510 Binding {
511 target: ApplicationManager
512 property: "suspended"
513 value: greeter.shown && greeterWrapper.showProgress == 1
514 }
515 }
516 }
518 Connections {
519 id: powerConnection
520 target: Powerd
522 onStatusChanged: {
523 if (Powerd.status === Powerd.Off && !callManager.hasCalls && !edgeDemo.running) {
524 greeter.showNow()
525 }
526 }
527 }
529 function showHome() {
530 if (edgeDemo.running) {
531 return
532 }
534 if (LightDM.Greeter.active) {
535 if (!LightDM.Greeter.authenticated) {
536 lockscreen.show()
537 }
538 greeter.hide()
539 }
541 var animate = !LightDM.Greeter.active && !stages.shown
542 dash.setCurrentScope("clickscope", animate, false)
543 ApplicationManager.requestFocusApplication("unity8-dash")
544 }
546 function showDash() {
547 if (greeter.fakeActiveForApp !== "") { // just in case user gets here
548 return
549 }
551 if (greeter.shown) {
552 greeter.hideRight();
553 launcher.fadeOut();
554 }
556 if (ApplicationManager.focusedApplicationId != "unity8-dash") {
557 ApplicationManager.requestFocusApplication("unity8-dash")
558 launcher.fadeOut();
559 }
560 }
562 Item {
563 id: overlay
564 z: 10
566 anchors.fill: parent
568 Panel {
569 id: panel
570 objectName: "panel"
571 anchors.fill: parent //because this draws indicator menus
572 indicators {
573 hides: [launcher]
574 available: edgeDemo.panelEnabled && (!shell.locked || AccountsService.enableIndicatorsWhileLocked) && greeter.fakeActiveForApp === ""
575 contentEnabled: edgeDemo.panelContentEnabled
576 width: parent.width > units.gu(60) ? units.gu(40) : parent.width
578 minimizedPanelHeight: units.gu(3)
579 expandedPanelHeight: units.gu(7)
581 indicatorsModel: visibleIndicators.model
582 }
584 VisibleIndicators {
585 id: visibleIndicators
586 }
588 property bool topmostApplicationIsFullscreen:
589 ApplicationManager.focusedApplicationId &&
590 ApplicationManager.findApplication(ApplicationManager.focusedApplicationId).fullscreen
592 fullscreenMode: (topmostApplicationIsFullscreen && !LightDM.Greeter.active && launcher.progress == 0)
593 || greeter.fakeActiveForApp !== ""
594 }
596 Launcher {
597 id: launcher
598 objectName: "launcher"
600 readonly property bool dashSwipe: progress > 0
602 anchors.top: parent.top
603 anchors.bottom: parent.bottom
604 width: parent.width
605 dragAreaWidth: shell.edgeSize
606 available: edgeDemo.launcherEnabled && (!shell.locked || AccountsService.enableLauncherWhileLocked) && greeter.fakeActiveForApp === ""
608 onShowDashHome: showHome()
609 onDash: {
610 if (ApplicationManager.focusedApplicationId != "unity8-dash") {
611 showDash()
612 }
613 }
614 onDashSwipeChanged: {
615 if (dashSwipe) {
616 dash.setCurrentScope("clickscope", false, true)
617 }
618 }
619 onLauncherApplicationSelected: {
620 if (greeter.fakeActiveForApp !== "") {
621 lockscreen.show()
622 }
623 if (!edgeDemo.running)
624 shell.activateApplication(appId)
625 }
626 onShownChanged: {
627 if (shown) {
628 panel.indicators.hide()
629 }
630 }
631 }
633 Rectangle {
634 id: modalNotificationBackground
636 visible: notifications.useModal && !greeter.shown && (notifications.state == "narrow")
637 color: "#000000"
638 anchors.fill: parent
639 opacity: 0.9
641 MouseArea {
642 anchors.fill: parent
643 }
644 }
646 Notifications {
647 id: notifications
649 model: NotificationBackend.Model
650 margin: units.gu(1)
652 y: panel.panelHeight
653 width: parent.width
654 height: parent.height - panel.panelHeight
656 states: [
657 State {
658 name: "narrow"
659 when: overlay.width <= units.gu(60)
660 AnchorChanges { target: notifications; anchors.left: parent.left }
661 },
662 State {
663 name: "wide"
664 when: overlay.width > units.gu(60)
665 AnchorChanges { target: notifications; anchors.left: undefined }
666 PropertyChanges { target: notifications; width: units.gu(38) }
667 }
668 ]
669 }
670 }
672 Dialogs {
673 id: dialogs
674 anchors.fill: parent
675 z: overlay.z + 10
676 onPowerOffClicked: {
677 shutdownFadeOutRectangle.enabled = true;
678 shutdownFadeOutRectangle.visible = true;
679 shutdownFadeOut.start();
680 }
681 }
683 Label {
684 id: alphaDisclaimerLabel
685 anchors.centerIn: parent
686 visible: ApplicationManager.fake ? ApplicationManager.fake : false
687 z: dialogs.z + 10
688 text: "EARLY ALPHA\nNOT READY FOR USE"
689 color: "lightgrey"
690 opacity: 0.2
691 font.weight: Font.Black
692 horizontalAlignment: Text.AlignHCenter
693 verticalAlignment: Text.AlignVCenter
694 fontSizeMode: Text.Fit
695 rotation: -45
696 scale: Math.min(parent.width, parent.height) / width
697 }
699 EdgeDemo {
700 id: edgeDemo
701 z: alphaDisclaimerLabel.z + 10
702 paused: Powerd.status === Powerd.Off // Saves power
703 greeter: greeter
704 launcher: launcher
705 indicators: panel.indicators
706 stages: stages
707 }
709 Connections {
710 target: SessionBroadcast
711 onShowHome: showHome()
712 }
714 Rectangle {
715 id: shutdownFadeOutRectangle
716 z: edgeDemo.z + 10
717 enabled: false
718 visible: false
719 color: "black"
720 anchors.fill: parent
721 opacity: 0.0
722 NumberAnimation on opacity {
723 id: shutdownFadeOut
724 from: 0.0
725 to: 1.0
726 onStopped: {
727 if (shutdownFadeOutRectangle.enabled && shutdownFadeOutRectangle.visible) {
728 DBusUnitySessionService.Shutdown();
729 }
730 }
731 }
732 }