on a498661 help sign in
unity8/qml/Shell.qml
701 lines 24 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 failedLoginsDelaySeconds: 5 * 60 // seconds 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 readonly property int backgroundTopMargin: -panel.panelHeight
283 hides: [launcher, panel.indicators]
284 shown: false
285 enabled: true
286 showAnimation: StandardAnimation { property: "opacity"; to: 1 }
287 hideAnimation: StandardAnimation { property: "opacity"; to: 0 }
288 y: panel.panelHeight
289 visible: required
290 width: parent.width
291 height: parent.height - panel.panelHeight
292 background: shell.background
293 alphaNumeric: AccountsService.passwordDisplayHint === AccountsService.Keyboard
294 minPinLength: 4
295 maxPinLength: 4
297 // FIXME: We *should* show emergency dialer if there is a SIM present,
298 // regardless of whether the side stage is enabled. But right now,
299 // the assumption is that narrow screens are phones which have SIMs
300 // and wider screens are tablets which don't. When we do allow this
301 // on devices with a side stage and a SIM, work should be done to
302 // ensure that the main stage is disabled while the dialer is present
303 // in the side stage.
304 showEmergencyCallButton: !shell.sideStageEnabled
306 onEntered: LightDM.Greeter.respond(passphrase);
307 onCancel: greeter.show()
308 onEmergencyCall: shell.activateApplication("dialer-app") // will automatically enter fake-active mode
310 onShownChanged: if (shown) greeter.fakeActiveForApp = ""
312 Component.onCompleted: {
313 if (greeter.narrowMode) {
314 LightDM.Greeter.authenticate(LightDM.Users.data(0, LightDM.UserRoles.NameRole))
315 }
316 }
317 }
319 Connections {
320 target: LightDM.Greeter
322 onShowGreeter: greeter.show()
323 onHideGreeter: greeter.login()
325 onShowPrompt: {
326 if (greeter.narrowMode) {
327 if (isDefaultPrompt) {
328 if (lockscreen.alphaNumeric) {
329 lockscreen.infoText = i18n.tr("Enter your passphrase")
330 lockscreen.errorText = i18n.tr("Sorry, incorrect passphrase")
331 } else {
332 lockscreen.infoText = i18n.tr("Enter your passcode")
333 lockscreen.errorText = i18n.tr("Sorry, incorrect passcode")
334 }
335 } else {
336 lockscreen.infoText = i18n.tr("Enter your %1").arg(text.toLowerCase())
337 lockscreen.errorText = i18n.tr("Sorry, incorrect %1").arg(text.toLowerCase())
338 }
340 lockscreen.show();
341 }
342 }
344 onPromptlessChanged: {
345 if (LightDM.Greeter.promptless && LightDM.Greeter.authenticated) {
346 lockscreen.hide()
347 } else {
348 lockscreen.reset();
349 lockscreen.show();
350 }
351 }
353 onAuthenticationComplete: {
354 if (LightDM.Greeter.authenticated) {
355 AccountsService.failedLogins = 0
356 }
357 // Else only penalize user for a failed login if they actually were
358 // prompted for a password. We do this below after the promptless
359 // early exit.
361 if (LightDM.Greeter.promptless) {
362 return;
363 }
365 if (LightDM.Greeter.authenticated) {
366 greeter.login();
367 } else {
368 AccountsService.failedLogins++
369 if (maxFailedLogins >= 2) { // require at least a warning
370 if (AccountsService.failedLogins === maxFailedLogins - 1) {
371 var title = lockscreen.alphaNumeric ?
372 i18n.tr("Sorry, incorrect passphrase.") :
373 i18n.tr("Sorry, incorrect passcode.")
374 var text = i18n.tr("This will be your last attempt.") + " " +
375 (lockscreen.alphaNumeric ?
376 i18n.tr("If passphrase is entered incorrectly, your phone will conduct a factory reset and all personal data will be deleted.") :
377 i18n.tr("If passcode is entered incorrectly, your phone will conduct a factory reset and all personal data will be deleted."))
378 lockscreen.showInfoPopup(title, text)
379 } else if (AccountsService.failedLogins >= maxFailedLogins) {
380 SystemImage.factoryReset() // Ouch!
381 }
382 }
383 if (failedLoginsDelayAttempts > 0 && AccountsService.failedLogins % failedLoginsDelayAttempts == 0) {
384 lockscreen.forceDelay(failedLoginsDelaySeconds * 1000)
385 }
387 lockscreen.clear(true);
388 if (greeter.narrowMode) {
389 LightDM.Greeter.authenticate(LightDM.Users.data(0, LightDM.UserRoles.NameRole))
390 }
391 }
392 }
393 }
395 Binding {
396 target: LightDM.Greeter
397 property: "active"
398 value: greeter.shown || lockscreen.shown || greeter.fakeActiveForApp != ""
399 }
401 Rectangle {
402 anchors.fill: parent
403 color: "black"
404 opacity: greeterWrapper.showProgress * 0.8
405 }
407 Item {
408 // Just a tiny wrapper to adjust greeter's x without messing with its own dragging
409 id: greeterWrapper
410 x: launcher.progress
411 y: panel.panelHeight
412 width: parent.width
413 height: parent.height - panel.panelHeight
415 Behavior on x {
416 enabled: !launcher.dashSwipe
417 StandardAnimation {}
418 }
420 property bool fullyShown: showProgress === 1.0
421 onFullyShownChanged: {
422 // Wait until the greeter is completely covering lockscreen before resetting it.
423 if (fullyShown && !LightDM.Greeter.authenticated) {
424 lockscreen.reset();
425 lockscreen.show();
426 }
427 }
429 readonly property real showProgress: MathUtils.clamp((1 - x/width) + greeter.showProgress - 1, 0, 1)
430 onShowProgressChanged: if (LightDM.Greeter.authenticated && showProgress === 0) greeter.login()
432 Greeter {
433 id: greeter
434 objectName: "greeter"
436 signal sessionStarted() // helpful for tests
438 property string fakeActiveForApp: ""
440 available: true
441 hides: [launcher, panel.indicators]
442 shown: true
443 loadContent: required || lockscreen.required // keeps content in memory for quick show()
445 defaultBackground: shell.background
447 width: parent.width
448 height: parent.height
450 dragHandleWidth: shell.edgeSize
452 function login() {
453 enabled = false;
454 if (LightDM.Greeter.startSessionSync()) {
455 sessionStarted();
456 greeter.hide();
457 lockscreen.hide();
458 launcher.hide();
459 }
460 enabled = true;
461 }
463 onShownChanged: {
464 if (shown) {
465 if (greeter.narrowMode) {
466 LightDM.Greeter.authenticate(LightDM.Users.data(0, LightDM.UserRoles.NameRole));
467 }
468 greeter.fakeActiveForApp = "";
469 greeter.forceActiveFocus();
470 }
471 }
473 /* TODO re-enable when the corresponding changes in the service land (LP: #1361074)
474 Component.onCompleted: {
475 Connectivity.unlockAllModems()
476 } */
478 onUnlocked: greeter.hide()
479 onSelected: {
480 // Update launcher items for new user
481 var user = LightDM.Users.data(uid, LightDM.UserRoles.NameRole);
482 AccountsService.user = user;
483 LauncherModel.setUser(user);
484 }
486 onTease: launcher.tease()
488 Binding {
489 target: ApplicationManager
490 property: "suspended"
491 value: greeter.shown && greeterWrapper.showProgress == 1
492 }
493 }
494 }
496 Connections {
497 id: powerConnection
498 target: Powerd
500 onStatusChanged: {
501 if (Powerd.status === Powerd.Off && !callManager.hasCalls && !edgeDemo.running) {
502 greeter.showNow()
503 }
504 }
505 }
507 function showHome() {
508 if (edgeDemo.running) {
509 return
510 }
512 if (LightDM.Greeter.active) {
513 if (!LightDM.Greeter.authenticated) {
514 lockscreen.show()
515 }
516 greeter.hide()
517 }
519 var animate = !LightDM.Greeter.active && !stages.shown
520 dash.setCurrentScope("clickscope", animate, false)
521 ApplicationManager.requestFocusApplication("unity8-dash")
522 }
524 function showDash() {
525 if (shell.locked) {
526 return;
527 }
528 if (greeter.shown) {
529 greeter.hideRight();
530 launcher.fadeOut();
531 }
533 ApplicationManager.requestFocusApplication("unity8-dash")
534 launcher.fadeOut();
535 }
537 Item {
538 id: overlay
539 z: 10
541 anchors.fill: parent
543 Panel {
544 id: panel
545 objectName: "panel"
546 anchors.fill: parent //because this draws indicator menus
547 indicators {
548 hides: [launcher]
549 available: edgeDemo.panelEnabled && (!shell.locked || AccountsService.enableIndicatorsWhileLocked) && greeter.fakeActiveForApp === ""
550 contentEnabled: edgeDemo.panelContentEnabled
551 width: parent.width > units.gu(60) ? units.gu(40) : parent.width
552 panelHeight: units.gu(3)
553 }
555 property bool topmostApplicationIsFullscreen:
556 ApplicationManager.focusedApplicationId &&
557 ApplicationManager.findApplication(ApplicationManager.focusedApplicationId).fullscreen
559 fullscreenMode: (topmostApplicationIsFullscreen && !LightDM.Greeter.active && launcher.progress == 0)
560 || greeter.fakeActiveForApp !== ""
561 }
563 Launcher {
564 id: launcher
565 objectName: "launcher"
567 readonly property bool dashSwipe: progress > 0
569 anchors.top: parent.top
570 anchors.bottom: parent.bottom
571 width: parent.width
572 dragAreaWidth: shell.edgeSize
573 available: edgeDemo.launcherEnabled && (!shell.locked || AccountsService.enableLauncherWhileLocked) && greeter.fakeActiveForApp === ""
575 onShowDashHome: showHome()
576 onDash: {
577 if (ApplicationManager.focusedApplicationId != "unity8-dash") {
578 showDash()
579 }
580 }
581 onDashSwipeChanged: {
582 if (dashSwipe) {
583 dash.setCurrentScope("clickscope", false, true)
584 }
585 }
586 onLauncherApplicationSelected: {
587 if (greeter.fakeActiveForApp !== "") {
588 lockscreen.show()
589 }
590 if (!edgeDemo.running)
591 shell.activateApplication(appId)
592 }
593 onShownChanged: {
594 if (shown) {
595 panel.indicators.hide()
596 }
597 }
598 }
600 Rectangle {
601 id: modalNotificationBackground
603 visible: notifications.useModal && !greeter.shown && (notifications.state == "narrow")
604 color: "#000000"
605 anchors.fill: parent
606 opacity: 0.9
608 MouseArea {
609 anchors.fill: parent
610 }
611 }
613 Notifications {
614 id: notifications
616 model: NotificationBackend.Model
617 margin: units.gu(1)
619 y: panel.panelHeight
620 width: parent.width
621 height: parent.height - panel.panelHeight
623 states: [
624 State {
625 name: "narrow"
626 when: overlay.width <= units.gu(60)
627 AnchorChanges { target: notifications; anchors.left: parent.left }
628 },
629 State {
630 name: "wide"
631 when: overlay.width > units.gu(60)
632 AnchorChanges { target: notifications; anchors.left: undefined }
633 PropertyChanges { target: notifications; width: units.gu(38) }
634 }
635 ]
636 }
637 }
639 Dialogs {
640 id: dialogs
641 anchors.fill: parent
642 z: overlay.z + 10
643 onPowerOffClicked: {
644 shutdownFadeOutRectangle.enabled = true;
645 shutdownFadeOutRectangle.visible = true;
646 shutdownFadeOut.start();
647 }
648 }
650 Label {
651 id: alphaDisclaimerLabel
652 anchors.centerIn: parent
653 visible: ApplicationManager.fake ? ApplicationManager.fake : false
654 z: dialogs.z + 10
655 text: "EARLY ALPHA\nNOT READY FOR USE"
656 color: "lightgrey"
657 opacity: 0.2
658 font.weight: Font.Black
659 horizontalAlignment: Text.AlignHCenter
660 verticalAlignment: Text.AlignVCenter
661 fontSizeMode: Text.Fit
662 rotation: -45
663 scale: Math.min(parent.width, parent.height) / width
664 }
666 EdgeDemo {
667 id: edgeDemo
668 z: alphaDisclaimerLabel.z + 10
669 paused: Powerd.status === Powerd.Off // Saves power
670 greeter: greeter
671 launcher: launcher
672 indicators: panel.indicators
673 stages: stages
674 }
676 Connections {
677 target: SessionBroadcast
678 onShowHome: showHome()
679 }
681 Rectangle {
682 id: shutdownFadeOutRectangle
683 z: edgeDemo.z + 10
684 enabled: false
685 visible: false
686 color: "black"
687 anchors.fill: parent
688 opacity: 0.0
689 NumberAnimation on opacity {
690 id: shutdownFadeOut
691 from: 0.0
692 to: 1.0
693 onStopped: {
694 if (shutdownFadeOutRectangle.enabled && shutdownFadeOutRectangle.visible) {
695 DBusUnitySessionService.Shutdown();
696 }
697 }
698 }
699 }