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