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