on 92f2c7d help sign in
unity8/qml/Shell.qml
727 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 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 setFakeActiveForApp(app) {
91 if (shell.locked) {
92 greeter.fakeActiveForApp = 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 (appId === "dialer-app") {
173 // Always let the dialer-app through. Either user asked
174 // for an emergency call or accepted an incoming call.
175 setFakeActiveForApp(appId)
176 } else if (greeter.fakeActiveForApp !== "" && greeter.fakeActiveForApp !== appId) {
177 lockscreen.show();
178 }
179 greeter.hide();
180 }
182 onFocusedApplicationIdChanged: {
183 if (greeter.fakeActiveForApp !== "" && greeter.fakeActiveForApp !== ApplicationManager.focusedApplicationId) {
184 lockscreen.show();
185 }
186 panel.indicators.hide();
187 }
189 onApplicationAdded: {
190 if (greeter.shown && appId != "unity8-dash") {
191 greeter.hide();
192 }
193 if (appId === "dialer-app") {
194 // Always let the dialer-app through. Either user asked
195 // for an emergency call or accepted an incoming call.
196 setFakeActiveForApp(appId)
197 }
198 launcher.hide();
199 }
200 }
202 Loader {
203 id: applicationsDisplayLoader
204 anchors.fill: parent
206 source: shell.sideStageEnabled ? "Stages/TabletStage.qml" : "Stages/PhoneStage.qml"
208 Binding {
209 target: applicationsDisplayLoader.item
210 property: "objectName"
211 value: "stage"
212 }
213 Binding {
214 target: applicationsDisplayLoader.item
215 property: "dragAreaWidth"
216 value: shell.edgeSize
217 }
218 Binding {
219 target: applicationsDisplayLoader.item
220 property: "maximizedAppTopMargin"
221 // Not just using panel.panelHeight as that changes depending on the focused app.
222 value: panel.indicators.panelHeight
223 }
224 Binding {
225 target: applicationsDisplayLoader.item
226 property: "interactive"
227 value: edgeDemo.stagesEnabled && !greeter.shown && !lockscreen.shown && panel.indicators.fullyClosed && launcher.progress == 0
228 }
229 Binding {
230 target: applicationsDisplayLoader.item
231 property: "spreadEnabled"
232 value: edgeDemo.stagesEnabled && greeter.fakeActiveForApp === "" // to support emergency dialer hack
233 }
234 Binding {
235 target: applicationsDisplayLoader.item
236 property: "inverseProgress"
237 value: launcher.progress
238 }
239 Binding {
240 target: applicationsDisplayLoader.item
241 property: "orientation"
242 value: shell.orientation
243 }
244 }
245 }
247 InputMethod {
248 id: inputMethod
249 objectName: "inputMethod"
250 anchors { fill: parent; topMargin: panel.panelHeight }
251 z: notifications.useModal || panel.indicators.shown ? overlay.z + 1 : overlay.z - 1
252 }
254 Connections {
255 target: SurfaceManager
256 onSurfaceCreated: {
257 if (surface.type == MirSurfaceItem.InputMethod) {
258 inputMethod.surface = surface;
259 }
260 }
262 onSurfaceDestroyed: {
263 if (inputMethod.surface == surface) {
264 inputMethod.surface = null;
265 surface.parent = null;
266 }
267 if (!surface.parent) {
268 // there's no one displaying it. delete it right away
269 surface.release();
270 }
271 }
272 }
273 Connections {
274 target: SessionManager
275 onSessionStopping: {
276 if (!session.parentSession && !session.application) {
277 // nothing is using it. delete it right away
278 session.release();
279 }
280 }
281 }
283 Lockscreen {
284 id: lockscreen
285 objectName: "lockscreen"
287 hides: [launcher, panel.indicators]
288 shown: false
289 enabled: true
290 showAnimation: StandardAnimation { property: "opacity"; to: 1 }
291 hideAnimation: StandardAnimation { property: "opacity"; to: 0 }
292 y: panel.panelHeight
293 visible: required
294 width: parent.width
295 height: parent.height - panel.panelHeight
296 background: shell.background
297 alphaNumeric: AccountsService.passwordDisplayHint === AccountsService.Keyboard
298 minPinLength: 4
299 maxPinLength: 4
301 // FIXME: We *should* show emergency dialer if there is a SIM present,
302 // regardless of whether the side stage is enabled. But right now,
303 // the assumption is that narrow screens are phones which have SIMs
304 // and wider screens are tablets which don't. When we do allow this
305 // on devices with a side stage and a SIM, work should be done to
306 // ensure that the main stage is disabled while the dialer is present
307 // in the side stage.
308 showEmergencyCallButton: !shell.sideStageEnabled
310 onEntered: LightDM.Greeter.respond(passphrase);
311 onCancel: greeter.show()
312 onEmergencyCall: shell.activateApplication("dialer-app") // will automatically enter fake-active mode
314 onShownChanged: if (shown) greeter.fakeActiveForApp = ""
316 Timer {
317 id: forcedDelayTimer
318 interval: 1000 * 60
319 onTriggered: {
320 if (lockscreen.delayMinutes > 0) {
321 lockscreen.delayMinutes -= 1
322 if (lockscreen.delayMinutes > 0) {
323 start() // go again
324 }
325 }
326 }
327 }
329 Component.onCompleted: {
330 if (greeter.narrowMode) {
331 LightDM.Greeter.authenticate(LightDM.Users.data(0, LightDM.UserRoles.NameRole))
332 }
333 }
334 }
336 Connections {
337 target: LightDM.Greeter
339 onShowGreeter: greeter.show()
340 onHideGreeter: greeter.login()
342 onShowPrompt: {
343 if (greeter.narrowMode) {
344 if (isDefaultPrompt) {
345 if (lockscreen.alphaNumeric) {
346 lockscreen.infoText = i18n.tr("Enter passphrase")
347 lockscreen.errorText = i18n.tr("Sorry, incorrect passphrase") + "\n" +
348 i18n.tr("Please re-enter")
349 } else {
350 lockscreen.infoText = i18n.tr("Enter passcode")
351 lockscreen.errorText = i18n.tr("Sorry, incorrect passcode")
352 }
353 } else {
354 lockscreen.infoText = i18n.tr("Enter %1").arg(text.toLowerCase())
355 lockscreen.errorText = i18n.tr("Sorry, incorrect %1").arg(text.toLowerCase())
356 }
358 lockscreen.show();
359 }
360 }
362 onPromptlessChanged: {
363 if (LightDM.Greeter.promptless && LightDM.Greeter.authenticated) {
364 lockscreen.hide()
365 } else {
366 lockscreen.reset();
367 lockscreen.show();
368 }
369 }
371 onAuthenticationComplete: {
372 if (LightDM.Greeter.authenticated) {
373 AccountsService.failedLogins = 0
374 }
375 // Else only penalize user for a failed login if they actually were
376 // prompted for a password. We do this below after the promptless
377 // early exit.
379 if (LightDM.Greeter.promptless) {
380 return;
381 }
383 if (LightDM.Greeter.authenticated) {
384 greeter.login();
385 } else {
386 AccountsService.failedLogins++
387 if (maxFailedLogins >= 2) { // require at least a warning
388 if (AccountsService.failedLogins === maxFailedLogins - 1) {
389 var title = lockscreen.alphaNumeric ?
390 i18n.tr("Sorry, incorrect passphrase.") :
391 i18n.tr("Sorry, incorrect passcode.")
392 var text = i18n.tr("This will be your last attempt.") + " " +
393 (lockscreen.alphaNumeric ?
394 i18n.tr("If passphrase is entered incorrectly, your phone will conduct a factory reset and all personal data will be deleted.") :
395 i18n.tr("If passcode is entered incorrectly, your phone will conduct a factory reset and all personal data will be deleted."))
396 lockscreen.showInfoPopup(title, text)
397 } else if (AccountsService.failedLogins >= maxFailedLogins) {
398 SystemImage.factoryReset() // Ouch!
399 }
400 }
401 if (failedLoginsDelayAttempts > 0 && AccountsService.failedLogins % failedLoginsDelayAttempts == 0) {
402 lockscreen.delayMinutes = failedLoginsDelayMinutes
403 forcedDelayTimer.start()
404 }
406 lockscreen.clear(true);
407 if (greeter.narrowMode) {
408 LightDM.Greeter.authenticate(LightDM.Users.data(0, LightDM.UserRoles.NameRole))
409 }
410 }
411 }
412 }
414 Binding {
415 target: LightDM.Greeter
416 property: "active"
417 value: greeter.shown || lockscreen.shown || greeter.fakeActiveForApp != ""
418 }
420 Rectangle {
421 anchors.fill: parent
422 color: "black"
423 opacity: greeterWrapper.showProgress * 0.8
424 }
426 Item {
427 // Just a tiny wrapper to adjust greeter's x without messing with its own dragging
428 id: greeterWrapper
429 x: launcher.progress
430 y: panel.panelHeight
431 width: parent.width
432 height: parent.height - panel.panelHeight
434 Behavior on x {
435 enabled: !launcher.dashSwipe
436 StandardAnimation {}
437 }
439 property bool fullyShown: showProgress === 1.0
440 onFullyShownChanged: {
441 // Wait until the greeter is completely covering lockscreen before resetting it.
442 if (fullyShown && !LightDM.Greeter.authenticated) {
443 lockscreen.reset();
444 lockscreen.show();
445 }
446 }
448 readonly property real showProgress: MathUtils.clamp((1 - x/width) + greeter.showProgress - 1, 0, 1)
449 onShowProgressChanged: {
450 if (showProgress === 0) {
451 if (LightDM.Greeter.authenticated) {
452 greeter.login()
453 } else if (greeter.narrowMode) {
454 lockscreen.clear(false) // to reset focus if necessary
455 }
456 }
457 }
459 Greeter {
460 id: greeter
461 objectName: "greeter"
463 signal sessionStarted() // helpful for tests
465 property string fakeActiveForApp: ""
467 available: true
468 hides: [launcher, panel.indicators]
469 shown: true
470 loadContent: required || lockscreen.required // keeps content in memory for quick show()
472 defaultBackground: shell.background
474 width: parent.width
475 height: parent.height
477 dragHandleWidth: shell.edgeSize
479 function login() {
480 enabled = false;
481 if (LightDM.Greeter.startSessionSync()) {
482 sessionStarted();
483 greeter.hide();
484 lockscreen.hide();
485 launcher.hide();
486 }
487 enabled = true;
488 }
490 onShownChanged: {
491 if (shown) {
492 if (greeter.narrowMode) {
493 LightDM.Greeter.authenticate(LightDM.Users.data(0, LightDM.UserRoles.NameRole));
494 }
495 greeter.fakeActiveForApp = "";
496 greeter.forceActiveFocus();
497 }
498 }
500 /* TODO re-enable when the corresponding changes in the service land (LP: #1361074)
501 Component.onCompleted: {
502 Connectivity.unlockAllModems()
503 } */
505 onUnlocked: greeter.hide()
506 onSelected: {
507 // Update launcher items for new user
508 var user = LightDM.Users.data(uid, LightDM.UserRoles.NameRole);
509 AccountsService.user = user;
510 LauncherModel.setUser(user);
511 }
513 onTease: launcher.tease()
515 Binding {
516 target: ApplicationManager
517 property: "suspended"
518 value: greeter.shown && greeterWrapper.showProgress == 1
519 }
520 }
521 }
523 Connections {
524 id: powerConnection
525 target: Powerd
527 onStatusChanged: {
528 if (Powerd.status === Powerd.Off && !callManager.hasCalls && !edgeDemo.running) {
529 greeter.showNow()
530 }
531 }
532 }
534 function showHome() {
535 if (edgeDemo.running) {
536 return
537 }
539 if (LightDM.Greeter.active) {
540 if (!LightDM.Greeter.authenticated) {
541 lockscreen.show()
542 }
543 greeter.hide()
544 }
546 var animate = !LightDM.Greeter.active && !stages.shown
547 dash.setCurrentScope("clickscope", animate, false)
548 ApplicationManager.requestFocusApplication("unity8-dash")
549 }
551 function showDash() {
552 if (greeter.fakeActiveForApp !== "") { // just in case user gets here
553 return
554 }
556 if (greeter.shown) {
557 greeter.hideRight();
558 launcher.fadeOut();
559 }
561 if (ApplicationManager.focusedApplicationId != "unity8-dash") {
562 ApplicationManager.requestFocusApplication("unity8-dash")
563 launcher.fadeOut();
564 }
565 }
567 Item {
568 id: overlay
569 z: 10
571 anchors.fill: parent
573 Panel {
574 id: panel
575 objectName: "panel"
576 anchors.fill: parent //because this draws indicator menus
577 indicators {
578 hides: [launcher]
579 available: edgeDemo.panelEnabled && (!shell.locked || AccountsService.enableIndicatorsWhileLocked) && greeter.fakeActiveForApp === ""
580 contentEnabled: edgeDemo.panelContentEnabled
581 width: parent.width > units.gu(60) ? units.gu(40) : parent.width
582 panelHeight: units.gu(3)
583 }
585 property bool topmostApplicationIsFullscreen:
586 ApplicationManager.focusedApplicationId &&
587 ApplicationManager.findApplication(ApplicationManager.focusedApplicationId).fullscreen
589 fullscreenMode: (topmostApplicationIsFullscreen && !LightDM.Greeter.active && launcher.progress == 0)
590 || greeter.fakeActiveForApp !== ""
591 }
593 Launcher {
594 id: launcher
595 objectName: "launcher"
597 readonly property bool dashSwipe: progress > 0
599 anchors.top: parent.top
600 anchors.bottom: parent.bottom
601 width: parent.width
602 dragAreaWidth: shell.edgeSize
603 available: edgeDemo.launcherEnabled && (!shell.locked || AccountsService.enableLauncherWhileLocked) && greeter.fakeActiveForApp === ""
605 onShowDashHome: showHome()
606 onDash: showDash()
607 onDashSwipeChanged: {
608 if (dashSwipe) {
609 dash.setCurrentScope("clickscope", false, true)
610 }
611 }
612 onLauncherApplicationSelected: {
613 if (greeter.fakeActiveForApp !== "") {
614 lockscreen.show()
615 }
616 if (!edgeDemo.running)
617 shell.activateApplication(appId)
618 }
619 onShownChanged: {
620 if (shown) {
621 panel.indicators.hide()
622 }
623 }
624 }
626 Rectangle {
627 id: modalNotificationBackground
629 visible: notifications.useModal && !greeter.shown && (notifications.state == "narrow")
630 color: "#000000"
631 anchors.fill: parent
632 opacity: 0.9
634 MouseArea {
635 anchors.fill: parent
636 }
637 }
639 Notifications {
640 id: notifications
642 model: NotificationBackend.Model
643 margin: units.gu(1)
645 y: panel.panelHeight
646 width: parent.width
647 height: parent.height - panel.panelHeight
649 states: [
650 State {
651 name: "narrow"
652 when: overlay.width <= units.gu(60)
653 AnchorChanges { target: notifications; anchors.left: parent.left }
654 },
655 State {
656 name: "wide"
657 when: overlay.width > units.gu(60)
658 AnchorChanges { target: notifications; anchors.left: undefined }
659 PropertyChanges { target: notifications; width: units.gu(38) }
660 }
661 ]
662 }
663 }
665 Dialogs {
666 id: dialogs
667 anchors.fill: parent
668 z: overlay.z + 10
669 onPowerOffClicked: {
670 shutdownFadeOutRectangle.enabled = true;
671 shutdownFadeOutRectangle.visible = true;
672 shutdownFadeOut.start();
673 }
674 }
676 Label {
677 id: alphaDisclaimerLabel
678 anchors.centerIn: parent
679 visible: ApplicationManager.fake ? ApplicationManager.fake : false
680 z: dialogs.z + 10
681 text: "EARLY ALPHA\nNOT READY FOR USE"
682 color: "lightgrey"
683 opacity: 0.2
684 font.weight: Font.Black
685 horizontalAlignment: Text.AlignHCenter
686 verticalAlignment: Text.AlignVCenter
687 fontSizeMode: Text.Fit
688 rotation: -45
689 scale: Math.min(parent.width, parent.height) / width
690 }
692 EdgeDemo {
693 id: edgeDemo
694 z: alphaDisclaimerLabel.z + 10
695 paused: Powerd.status === Powerd.Off // Saves power
696 greeter: greeter
697 launcher: launcher
698 indicators: panel.indicators
699 stages: stages
700 }
702 Connections {
703 target: SessionBroadcast
704 onShowHome: showHome()
705 }
707 Rectangle {
708 id: shutdownFadeOutRectangle
709 z: edgeDemo.z + 10
710 enabled: false
711 visible: false
712 color: "black"
713 anchors.fill: parent
714 opacity: 0.0
715 NumberAnimation on opacity {
716 id: shutdownFadeOut
717 from: 0.0
718 to: 1.0
719 onStopped: {
720 if (shutdownFadeOutRectangle.enabled && shutdownFadeOutRectangle.visible) {
721 DBusUnitySessionService.Shutdown();
722 }
723 }
724 }
725 }