on 76cbe21 help sign in
unity8/qml/Shell.qml
772 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 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 setLockedApp(app) {
92 if (shell.locked) {
93 greeter.lockedApp = 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 Binding {
133 target: ApplicationManager
134 property: "forceDashActive"
135 value: launcher.shown || launcher.dashSwipe
136 }
139 WindowKeysFilter {
140 // Handle but do not filter out volume keys
141 Keys.onVolumeUpPressed: { volumeControl.volumeUp(); event.accepted = false; }
142 Keys.onVolumeDownPressed: { volumeControl.volumeDown(); event.accepted = false; }
144 Keys.onPressed: {
145 if (event.key == Qt.Key_PowerOff || event.key == Qt.Key_PowerDown) {
146 dialogs.onPowerKeyPressed();
147 event.accepted = true;
148 } else {
149 event.accepted = false;
150 }
151 }
153 Keys.onReleased: {
154 if (event.key == Qt.Key_PowerOff || event.key == Qt.Key_PowerDown) {
155 dialogs.onPowerKeyReleased();
156 event.accepted = true;
157 } else {
158 event.accepted = false;
159 }
160 }
161 }
163 Item {
164 id: stages
165 objectName: "stages"
166 width: parent.width
167 height: parent.height
168 visible: !ApplicationManager.empty
170 Connections {
171 target: ApplicationManager
172 onFocusRequested: {
173 if (greeter.narrowMode) {
174 if (appId === "dialer-app") {
175 // Always let the dialer-app through. Either user asked
176 // for an emergency call or accepted an incoming call.
177 setLockedApp(appId)
178 } else if (greeter.hasLockedApp && greeter.lockedApp !== appId) {
179 greeter.startUnlock()
180 }
181 greeter.hide();
182 } else {
183 if (LightDM.Greeter.active) {
184 greeter.startUnlock()
185 }
186 }
187 }
189 onFocusedApplicationIdChanged: {
190 if (greeter.hasLockedApp && greeter.lockedApp !== ApplicationManager.focusedApplicationId) {
191 greeter.startUnlock()
192 }
193 panel.indicators.hide();
194 }
196 onApplicationAdded: {
197 if (greeter.shown && appId != "unity8-dash") {
198 greeter.startUnlock()
199 }
200 if (greeter.narrowMode && appId === "dialer-app") {
201 // Always let the dialer-app through. Either user asked
202 // for an emergency call or accepted an incoming call.
203 setLockedApp(appId)
204 }
205 launcher.hide();
206 }
207 }
209 Loader {
210 id: applicationsDisplayLoader
211 objectName: "applicationsDisplayLoader"
212 anchors.fill: parent
214 // When we have a locked app, we only want to show that one app.
215 // FIXME: do this in a less traumatic way. We currently only allow
216 // locked apps in phone mode (see FIXME in Lockscreen component in
217 // this same file). When that changes, we need to do something
218 // nicer here. But this code is currently just to prevent a
219 // theoretical attack where user enters lockedApp mode, then makes
220 // the screen larger (maybe connects to monitor) and tries to enter
221 // tablet mode.
222 property bool tabletMode: shell.sideStageEnabled && !greeter.hasLockedApp
223 source: tabletMode ? "Stages/TabletStage.qml" : "Stages/PhoneStage.qml"
225 Binding {
226 target: applicationsDisplayLoader.item
227 property: "objectName"
228 value: "stage"
229 }
230 Binding {
231 target: applicationsDisplayLoader.item
232 property: "dragAreaWidth"
233 value: shell.edgeSize
234 }
235 Binding {
236 target: applicationsDisplayLoader.item
237 property: "maximizedAppTopMargin"
238 // Not just using panel.panelHeight as that changes depending on the focused app.
239 value: panel.indicators.minimizedPanelHeight + units.dp(2) // dp(2) for orange line
240 }
241 Binding {
242 target: applicationsDisplayLoader.item
243 property: "interactive"
244 value: edgeDemo.stagesEnabled && !greeter.shown && !lockscreen.shown && panel.indicators.fullyClosed && launcher.progress == 0
245 }
246 Binding {
247 target: applicationsDisplayLoader.item
248 property: "spreadEnabled"
249 value: edgeDemo.stagesEnabled && !greeter.hasLockedApp
250 }
251 Binding {
252 target: applicationsDisplayLoader.item
253 property: "inverseProgress"
254 value: launcher.progress
255 }
256 Binding {
257 target: applicationsDisplayLoader.item
258 property: "orientation"
259 value: shell.orientation
260 }
261 }
262 }
264 InputMethod {
265 id: inputMethod
266 objectName: "inputMethod"
267 anchors { fill: parent; topMargin: panel.panelHeight }
268 z: notifications.useModal || panel.indicators.shown ? overlay.z + 1 : overlay.z - 1
269 }
271 Connections {
272 target: SurfaceManager
273 onSurfaceCreated: {
274 if (surface.type == MirSurfaceItem.InputMethod) {
275 inputMethod.surface = surface;
276 }
277 }
279 onSurfaceDestroyed: {
280 if (inputMethod.surface == surface) {
281 inputMethod.surface = null;
282 surface.parent = null;
283 }
284 if (!surface.parent) {
285 // there's no one displaying it. delete it right away
286 surface.release();
287 }
288 }
289 }
290 Connections {
291 target: SessionManager
292 onSessionStopping: {
293 if (!session.parentSession && !session.application) {
294 // nothing is using it. delete it right away
295 session.release();
296 }
297 }
298 }
300 Lockscreen {
301 id: lockscreen
302 objectName: "lockscreen"
304 hides: [launcher, panel.indicators]
305 shown: false
306 enabled: true
307 showAnimation: StandardAnimation { property: "opacity"; to: 1 }
308 hideAnimation: StandardAnimation { property: "opacity"; to: 0 }
309 y: panel.panelHeight
310 visible: required
311 width: parent.width
312 height: parent.height - panel.panelHeight
313 background: shell.background
314 alphaNumeric: AccountsService.passwordDisplayHint === AccountsService.Keyboard
315 minPinLength: 4
316 maxPinLength: 4
318 // FIXME: We *should* show emergency dialer if there is a SIM present,
319 // regardless of whether the side stage is enabled. But right now,
320 // the assumption is that narrow screens are phones which have SIMs
321 // and wider screens are tablets which don't. When we do allow this
322 // on devices with a side stage and a SIM, work should be done to
323 // ensure that the main stage is disabled while the dialer is present
324 // in the side stage. See the FIXME in the stage loader in this file.
325 showEmergencyCallButton: !shell.sideStageEnabled
327 onEntered: LightDM.Greeter.respond(passphrase);
328 onCancel: greeter.show()
329 onEmergencyCall: shell.activateApplication("dialer-app") // will automatically enter locked-app mode
331 onShownChanged: if (shown) greeter.lockedApp = ""
333 Timer {
334 id: forcedDelayTimer
335 interval: 1000 * 60
336 onTriggered: {
337 if (lockscreen.delayMinutes > 0) {
338 lockscreen.delayMinutes -= 1
339 if (lockscreen.delayMinutes > 0) {
340 start() // go again
341 }
342 }
343 }
344 }
346 Component.onCompleted: {
347 if (greeter.narrowMode) {
348 LightDM.Greeter.authenticate(LightDM.Users.data(0, LightDM.UserRoles.NameRole))
349 }
350 }
351 }
353 Connections {
354 target: LightDM.Greeter
356 onShowGreeter: greeter.show()
357 onHideGreeter: greeter.login()
359 onShowPrompt: {
360 if (greeter.narrowMode) {
361 if (isDefaultPrompt) {
362 if (lockscreen.alphaNumeric) {
363 lockscreen.infoText = i18n.tr("Enter passphrase")
364 lockscreen.errorText = i18n.tr("Sorry, incorrect passphrase") + "\n" +
365 i18n.tr("Please re-enter")
366 } else {
367 lockscreen.infoText = i18n.tr("Enter passcode")
368 lockscreen.errorText = i18n.tr("Sorry, incorrect passcode")
369 }
370 } else {
371 lockscreen.infoText = i18n.tr("Enter %1").arg(text.toLowerCase())
372 lockscreen.errorText = i18n.tr("Sorry, incorrect %1").arg(text.toLowerCase())
373 }
375 lockscreen.show();
376 }
377 }
379 onPromptlessChanged: {
380 if (greeter.narrowMode) {
381 if (LightDM.Greeter.promptless && LightDM.Greeter.authenticated) {
382 lockscreen.hide()
383 } else {
384 lockscreen.reset();
385 lockscreen.show();
386 }
387 }
388 }
390 onAuthenticationComplete: {
391 if (LightDM.Greeter.authenticated) {
392 AccountsService.failedLogins = 0
393 }
394 // Else only penalize user for a failed login if they actually were
395 // prompted for a password. We do this below after the promptless
396 // early exit.
398 if (LightDM.Greeter.promptless) {
399 return;
400 }
402 if (LightDM.Greeter.authenticated) {
403 greeter.login();
404 } else {
405 AccountsService.failedLogins++
406 if (maxFailedLogins >= 2) { // require at least a warning
407 if (AccountsService.failedLogins === maxFailedLogins - 1) {
408 var title = lockscreen.alphaNumeric ?
409 i18n.tr("Sorry, incorrect passphrase.") :
410 i18n.tr("Sorry, incorrect passcode.")
411 var text = i18n.tr("This will be your last attempt.") + " " +
412 (lockscreen.alphaNumeric ?
413 i18n.tr("If passphrase is entered incorrectly, your phone will conduct a factory reset and all personal data will be deleted.") :
414 i18n.tr("If passcode is entered incorrectly, your phone will conduct a factory reset and all personal data will be deleted."))
415 lockscreen.showInfoPopup(title, text)
416 } else if (AccountsService.failedLogins >= maxFailedLogins) {
417 SystemImage.factoryReset() // Ouch!
418 }
419 }
420 if (failedLoginsDelayAttempts > 0 && AccountsService.failedLogins % failedLoginsDelayAttempts == 0) {
421 lockscreen.delayMinutes = failedLoginsDelayMinutes
422 forcedDelayTimer.start()
423 }
425 lockscreen.clear(true);
426 if (greeter.narrowMode) {
427 LightDM.Greeter.authenticate(LightDM.Users.data(0, LightDM.UserRoles.NameRole))
428 }
429 }
430 }
431 }
433 Binding {
434 target: LightDM.Greeter
435 property: "active"
436 value: greeter.shown || lockscreen.shown || greeter.hasLockedApp
437 }
439 Rectangle {
440 anchors.fill: parent
441 color: "black"
442 opacity: greeterWrapper.showProgress * 0.8
443 }
445 Item {
446 // Just a tiny wrapper to adjust greeter's x without messing with its own dragging
447 id: greeterWrapper
448 objectName: "greeterWrapper"
449 x: greeter.narrowMode ? launcher.progress : 0
450 y: panel.panelHeight
451 width: parent.width
452 height: parent.height - panel.panelHeight
454 Behavior on x {
455 enabled: !launcher.dashSwipe
456 StandardAnimation {}
457 }
459 property bool fullyShown: showProgress === 1.0
460 onFullyShownChanged: {
461 // Wait until the greeter is completely covering lockscreen before resetting it.
462 if (greeter.narrowMode && fullyShown && !LightDM.Greeter.authenticated) {
463 lockscreen.reset();
464 lockscreen.show();
465 }
466 }
468 readonly property real showProgress: MathUtils.clamp((1 - x/width) + greeter.showProgress - 1, 0, 1)
469 onShowProgressChanged: {
470 if (showProgress === 0) {
471 if (LightDM.Greeter.promptless && LightDM.Greeter.authenticated) {
472 greeter.login()
473 } else if (greeter.narrowMode) {
474 lockscreen.clear(false) // to reset focus if necessary
475 }
476 }
477 }
479 Greeter {
480 id: greeter
481 objectName: "greeter"
483 signal sessionStarted() // helpful for tests
485 property string lockedApp: ""
486 property bool hasLockedApp: lockedApp !== ""
488 available: true
489 hides: [launcher, panel.indicators]
490 shown: true
491 loadContent: required || lockscreen.required // keeps content in memory for quick show()
493 locked: shell.locked
495 defaultBackground: shell.background
497 width: parent.width
498 height: parent.height
500 dragHandleWidth: shell.edgeSize
502 function startUnlock() {
503 if (narrowMode) {
504 if (!LightDM.Greeter.authenticated) {
505 lockscreen.show()
506 }
507 hide()
508 } else {
509 show()
510 tryToUnlock()
511 }
512 }
514 function login() {
515 enabled = false;
516 if (LightDM.Greeter.startSessionSync()) {
517 sessionStarted();
518 greeter.hide();
519 lockscreen.hide();
520 launcher.hide();
521 }
522 enabled = true;
523 }
525 onShownChanged: {
526 if (shown) {
527 if (greeter.narrowMode) {
528 LightDM.Greeter.authenticate(LightDM.Users.data(0, LightDM.UserRoles.NameRole));
529 } else {
530 reset()
531 }
532 greeter.lockedApp = "";
533 greeter.forceActiveFocus();
534 }
535 }
537 /* TODO re-enable when the corresponding changes in the service land (LP: #1361074)
538 Component.onCompleted: {
539 Connectivity.unlockAllModems()
540 } */
542 onUnlocked: greeter.hide()
543 onSelected: {
544 // Update launcher items for new user
545 var user = LightDM.Users.data(uid, LightDM.UserRoles.NameRole);
546 AccountsService.user = user;
547 LauncherModel.setUser(user);
548 }
550 onTease: launcher.tease()
552 Binding {
553 target: ApplicationManager
554 property: "suspended"
555 value: greeter.shown && greeterWrapper.showProgress == 1
556 }
557 }
558 }
560 Connections {
561 id: powerConnection
562 target: Powerd
564 onStatusChanged: {
565 if (Powerd.status === Powerd.Off && !callManager.hasCalls && !edgeDemo.running) {
566 greeter.showNow()
567 }
568 }
569 }
571 function showHome() {
572 if (edgeDemo.running) {
573 return
574 }
576 if (LightDM.Greeter.active) {
577 greeter.startUnlock()
578 }
580 var animate = !LightDM.Greeter.active && !stages.shown
581 dash.setCurrentScope("clickscope", animate, false)
582 ApplicationManager.requestFocusApplication("unity8-dash")
583 }
585 function showDash() {
586 if (greeter.hasLockedApp || // just in case user gets here
587 (!greeter.narrowMode && shell.locked)) {
588 return
589 }
591 if (greeter.shown) {
592 greeter.hideRight();
593 launcher.fadeOut();
594 }
596 if (ApplicationManager.focusedApplicationId != "unity8-dash") {
597 ApplicationManager.requestFocusApplication("unity8-dash")
598 launcher.fadeOut();
599 }
600 }
602 Item {
603 id: overlay
604 z: 10
606 anchors.fill: parent
608 Panel {
609 id: panel
610 objectName: "panel"
611 anchors.fill: parent //because this draws indicator menus
612 indicators {
613 hides: [launcher]
614 available: edgeDemo.panelEnabled && (!shell.locked || AccountsService.enableIndicatorsWhileLocked) && !greeter.hasLockedApp
615 contentEnabled: edgeDemo.panelContentEnabled
616 width: parent.width > units.gu(60) ? units.gu(40) : parent.width
618 minimizedPanelHeight: units.gu(3)
619 expandedPanelHeight: units.gu(7)
621 indicatorsModel: visibleIndicators.model
622 }
624 VisibleIndicators {
625 id: visibleIndicators
626 // TODO: This should be sourced by device type (eg "desktop", "tablet", "phone"...)
627 Component.onCompleted: initialise(indicatorProfile)
628 }
630 property bool topmostApplicationIsFullscreen:
631 ApplicationManager.focusedApplicationId &&
632 ApplicationManager.findApplication(ApplicationManager.focusedApplicationId).fullscreen
634 fullscreenMode: (topmostApplicationIsFullscreen && !LightDM.Greeter.active && launcher.progress == 0)
635 || greeter.hasLockedApp
636 }
638 Launcher {
639 id: launcher
640 objectName: "launcher"
642 readonly property bool dashSwipe: progress > 0
644 anchors.top: parent.top
645 anchors.bottom: parent.bottom
646 width: parent.width
647 dragAreaWidth: shell.edgeSize
648 available: edgeDemo.launcherEnabled && (!shell.locked || AccountsService.enableLauncherWhileLocked) && !greeter.hasLockedApp
650 onShowDashHome: showHome()
651 onDash: showDash()
652 onDashSwipeChanged: {
653 if (dashSwipe) {
654 dash.setCurrentScope("clickscope", false, true)
655 }
656 }
657 onLauncherApplicationSelected: {
658 if (greeter.hasLockedApp) {
659 greeter.startUnlock()
660 }
661 if (!edgeDemo.running)
662 shell.activateApplication(appId)
663 }
664 onShownChanged: {
665 if (shown) {
666 panel.indicators.hide()
667 }
668 }
669 }
671 Rectangle {
672 id: modalNotificationBackground
674 visible: notifications.useModal && !greeter.shown && (notifications.state == "narrow")
675 color: "#000000"
676 anchors.fill: parent
677 opacity: 0.9
679 MouseArea {
680 anchors.fill: parent
681 }
682 }
684 Notifications {
685 id: notifications
687 model: NotificationBackend.Model
688 margin: units.gu(1)
690 y: panel.panelHeight
691 width: parent.width
692 height: parent.height - panel.panelHeight
694 states: [
695 State {
696 name: "narrow"
697 when: overlay.width <= units.gu(60)
698 AnchorChanges { target: notifications; anchors.left: parent.left }
699 },
700 State {
701 name: "wide"
702 when: overlay.width > units.gu(60)
703 AnchorChanges { target: notifications; anchors.left: undefined }
704 PropertyChanges { target: notifications; width: units.gu(38) }
705 }
706 ]
707 }
708 }
710 Dialogs {
711 id: dialogs
712 anchors.fill: parent
713 z: overlay.z + 10
714 onPowerOffClicked: {
715 shutdownFadeOutRectangle.enabled = true;
716 shutdownFadeOutRectangle.visible = true;
717 shutdownFadeOut.start();
718 }
719 }
721 Label {
722 id: alphaDisclaimerLabel
723 anchors.centerIn: parent
724 visible: ApplicationManager.fake ? ApplicationManager.fake : false
725 z: dialogs.z + 10
726 text: "EARLY ALPHA\nNOT READY FOR USE"
727 color: "lightgrey"
728 opacity: 0.2
729 font.weight: Font.Black
730 horizontalAlignment: Text.AlignHCenter
731 verticalAlignment: Text.AlignVCenter
732 fontSizeMode: Text.Fit
733 rotation: -45
734 scale: Math.min(parent.width, parent.height) / width
735 }
737 EdgeDemo {
738 id: edgeDemo
739 z: alphaDisclaimerLabel.z + 10
740 paused: Powerd.status === Powerd.Off // Saves power
741 greeter: greeter
742 launcher: launcher
743 indicators: panel.indicators
744 stages: stages
745 }
747 Connections {
748 target: SessionBroadcast
749 onShowHome: showHome()
750 }
752 Rectangle {
753 id: shutdownFadeOutRectangle
754 z: edgeDemo.z + 10
755 enabled: false
756 visible: false
757 color: "black"
758 anchors.fill: parent
759 opacity: 0.0
760 NumberAnimation on opacity {
761 id: shutdownFadeOut
762 from: 0.0
763 to: 1.0
764 onStopped: {
765 if (shutdownFadeOutRectangle.enabled && shutdownFadeOutRectangle.visible) {
766 DBusUnitySessionService.Shutdown();
767 }
768 }
769 }
770 }