Unity 8
 All Classes Functions
Shell.qml
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  */
16 
17 import QtQuick 2.0
18 import QtQuick.Window 2.0
19 import AccountsService 0.1
20 import GSettings 1.0
21 import Unity.Application 0.1
22 import Ubuntu.Components 0.1
23 import Ubuntu.Components.Popups 1.0
24 import Ubuntu.Gestures 0.1
25 import Ubuntu.SystemImage 0.1
26 import Ubuntu.Telephony 0.1 as Telephony
27 import Unity.Connectivity 0.1
28 import Unity.Launcher 0.1
29 import Utils 0.1
30 import LightDM 0.1 as LightDM
31 import Powerd 0.1
32 import SessionBroadcast 0.1
33 import "Greeter"
34 import "Launcher"
35 import "Panel"
36 import "Components"
37 import "Notifications"
38 import "Stages"
39 import Unity.Notifications 1.0 as NotificationBackend
40 import Unity.Session 0.1
41 import Unity.DashCommunicator 0.1
42 
43 Item {
44  id: shell
45 
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)
50 
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
55 
56  readonly property bool locked: LightDM.Greeter.active && !LightDM.Greeter.authenticated
57 
58  property bool sideStageEnabled: shell.width >= units.gu(100)
59  readonly property string focusedApplicationId: ApplicationManager.focusedApplicationId
60 
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
64 
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  }
80 
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  }
89 
90  function setFakeActiveForApp(app) {
91  if (shell.locked) {
92  greeter.fakeActiveForApp = app
93  lockscreen.hide()
94  }
95  }
96 
97  Binding {
98  target: LauncherModel
99  property: "applicationManager"
100  value: ApplicationManager
101  }
102 
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  }
112 
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  }
121 
122  VolumeControl {
123  id: volumeControl
124  }
125 
126  DashCommunicator {
127  id: dash
128  objectName: "dashCommunicator"
129  }
130 
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; }
135 
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  }
144 
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  }
154 
155  Item {
156  id: stages
157  objectName: "stages"
158  width: parent.width
159  height: parent.height
160  visible: !ApplicationManager.empty
161 
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  }
175 
176  onFocusedApplicationIdChanged: {
177  if (greeter.fakeActiveForApp !== "" && greeter.fakeActiveForApp !== ApplicationManager.focusedApplicationId) {
178  lockscreen.show();
179  }
180  panel.indicators.hide();
181  }
182 
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  }
195 
196  Loader {
197  id: applicationsDisplayLoader
198  anchors.fill: parent
199 
200  source: shell.sideStageEnabled ? "Stages/TabletStage.qml" : "Stages/PhoneStage.qml"
201 
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  }
240 
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  }
247 
248  Connections {
249  target: SurfaceManager
250  onSurfaceCreated: {
251  if (surface.type == MirSurfaceItem.InputMethod) {
252  inputMethod.surface = surface;
253  }
254  }
255 
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  }
276 
277  Lockscreen {
278  id: lockscreen
279  objectName: "lockscreen"
280 
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
294 
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
303 
304  onEntered: LightDM.Greeter.respond(passphrase);
305  onCancel: greeter.show()
306  onEmergencyCall: shell.activateApplication("dialer-app") // will automatically enter fake-active mode
307 
308  onShownChanged: if (shown) greeter.fakeActiveForApp = ""
309 
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  }
322 
323  Component.onCompleted: {
324  if (greeter.narrowMode) {
325  LightDM.Greeter.authenticate(LightDM.Users.data(0, LightDM.UserRoles.NameRole))
326  }
327  }
328  }
329 
330  Connections {
331  target: LightDM.Greeter
332 
333  onShowGreeter: greeter.show()
334  onHideGreeter: greeter.login()
335 
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  }
351 
352  lockscreen.show();
353  }
354  }
355 
356  onPromptlessChanged: {
357  if (LightDM.Greeter.promptless && LightDM.Greeter.authenticated) {
358  lockscreen.hide()
359  } else {
360  lockscreen.reset();
361  lockscreen.show();
362  }
363  }
364 
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.
372 
373  if (LightDM.Greeter.promptless) {
374  return;
375  }
376 
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  }
399 
400  lockscreen.clear(true);
401  if (greeter.narrowMode) {
402  LightDM.Greeter.authenticate(LightDM.Users.data(0, LightDM.UserRoles.NameRole))
403  }
404  }
405  }
406  }
407 
408  Binding {
409  target: LightDM.Greeter
410  property: "active"
411  value: greeter.shown || lockscreen.shown || greeter.fakeActiveForApp != ""
412  }
413 
414  Rectangle {
415  anchors.fill: parent
416  color: "black"
417  opacity: greeterWrapper.showProgress * 0.8
418  }
419 
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
427 
428  Behavior on x {
429  enabled: !launcher.dashSwipe
430  StandardAnimation {}
431  }
432 
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  }
441 
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  }
452 
453  Greeter {
454  id: greeter
455  objectName: "greeter"
456 
457  signal sessionStarted() // helpful for tests
458 
459  property string fakeActiveForApp: ""
460 
461  available: true
462  hides: [launcher, panel.indicators]
463  shown: true
464  loadContent: required || lockscreen.required // keeps content in memory for quick show()
465 
466  defaultBackground: shell.background
467 
468  width: parent.width
469  height: parent.height
470 
471  dragHandleWidth: shell.edgeSize
472 
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  }
483 
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  }
493 
494  /* TODO re-enable when the corresponding changes in the service land (LP: #1361074)
495  Component.onCompleted: {
496  Connectivity.unlockAllModems()
497  } */
498 
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  }
506 
507  onTease: launcher.tease()
508 
509  Binding {
510  target: ApplicationManager
511  property: "suspended"
512  value: greeter.shown && greeterWrapper.showProgress == 1
513  }
514  }
515  }
516 
517  Connections {
518  id: powerConnection
519  target: Powerd
520 
521  onStatusChanged: {
522  if (Powerd.status === Powerd.Off && !callManager.hasCalls && !edgeDemo.running) {
523  greeter.showNow()
524  }
525  }
526  }
527 
528  function showHome() {
529  if (edgeDemo.running) {
530  return
531  }
532 
533  if (LightDM.Greeter.active) {
534  if (!LightDM.Greeter.authenticated) {
535  lockscreen.show()
536  }
537  greeter.hide()
538  }
539 
540  var animate = !LightDM.Greeter.active && !stages.shown
541  dash.setCurrentScope("clickscope", animate, false)
542  ApplicationManager.requestFocusApplication("unity8-dash")
543  }
544 
545  function showDash() {
546  if (greeter.fakeActiveForApp !== "") { // just in case user gets here
547  return
548  }
549 
550  if (greeter.shown) {
551  greeter.hideRight();
552  launcher.fadeOut();
553  }
554 
555  if (ApplicationManager.focusedApplicationId != "unity8-dash") {
556  ApplicationManager.requestFocusApplication("unity8-dash")
557  launcher.fadeOut();
558  }
559  }
560 
561  Item {
562  id: overlay
563  z: 10
564 
565  anchors.fill: parent
566 
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)
577  }
578 
579  property bool topmostApplicationIsFullscreen:
580  ApplicationManager.focusedApplicationId &&
581  ApplicationManager.findApplication(ApplicationManager.focusedApplicationId).fullscreen
582 
583  fullscreenMode: (topmostApplicationIsFullscreen && !LightDM.Greeter.active && launcher.progress == 0)
584  || greeter.fakeActiveForApp !== ""
585  }
586 
587  Launcher {
588  id: launcher
589  objectName: "launcher"
590 
591  readonly property bool dashSwipe: progress > 0
592 
593  anchors.top: parent.top
594  anchors.bottom: parent.bottom
595  width: parent.width
596  dragAreaWidth: shell.edgeSize
597  available: edgeDemo.launcherEnabled && (!shell.locked || AccountsService.enableLauncherWhileLocked) && greeter.fakeActiveForApp === ""
598 
599  onShowDashHome: showHome()
600  onDash: {
601  if (ApplicationManager.focusedApplicationId != "unity8-dash") {
602  showDash()
603  }
604  }
605  onDashSwipeChanged: {
606  if (dashSwipe) {
607  dash.setCurrentScope("clickscope", false, true)
608  }
609  }
610  onLauncherApplicationSelected: {
611  if (greeter.fakeActiveForApp !== "") {
612  lockscreen.show()
613  }
614  if (!edgeDemo.running)
615  shell.activateApplication(appId)
616  }
617  onShownChanged: {
618  if (shown) {
619  panel.indicators.hide()
620  }
621  }
622  }
623 
624  Rectangle {
625  id: modalNotificationBackground
626 
627  visible: notifications.useModal && !greeter.shown && (notifications.state == "narrow")
628  color: "#000000"
629  anchors.fill: parent
630  opacity: 0.9
631 
632  MouseArea {
633  anchors.fill: parent
634  }
635  }
636 
637  Notifications {
638  id: notifications
639 
640  model: NotificationBackend.Model
641  margin: units.gu(1)
642 
643  y: panel.panelHeight
644  width: parent.width
645  height: parent.height - panel.panelHeight
646 
647  states: [
648  State {
649  name: "narrow"
650  when: overlay.width <= units.gu(60)
651  AnchorChanges { target: notifications; anchors.left: parent.left }
652  },
653  State {
654  name: "wide"
655  when: overlay.width > units.gu(60)
656  AnchorChanges { target: notifications; anchors.left: undefined }
657  PropertyChanges { target: notifications; width: units.gu(38) }
658  }
659  ]
660  }
661  }
662 
663  Dialogs {
664  id: dialogs
665  anchors.fill: parent
666  z: overlay.z + 10
667  onPowerOffClicked: {
668  shutdownFadeOutRectangle.enabled = true;
669  shutdownFadeOutRectangle.visible = true;
670  shutdownFadeOut.start();
671  }
672  }
673 
674  Label {
675  id: alphaDisclaimerLabel
676  anchors.centerIn: parent
677  visible: ApplicationManager.fake ? ApplicationManager.fake : false
678  z: dialogs.z + 10
679  text: "EARLY ALPHA\nNOT READY FOR USE"
680  color: "lightgrey"
681  opacity: 0.2
682  font.weight: Font.Black
683  horizontalAlignment: Text.AlignHCenter
684  verticalAlignment: Text.AlignVCenter
685  fontSizeMode: Text.Fit
686  rotation: -45
687  scale: Math.min(parent.width, parent.height) / width
688  }
689 
690  EdgeDemo {
691  id: edgeDemo
692  z: alphaDisclaimerLabel.z + 10
693  paused: Powerd.status === Powerd.Off // Saves power
694  greeter: greeter
695  launcher: launcher
696  indicators: panel.indicators
697  stages: stages
698  }
699 
700  Connections {
701  target: SessionBroadcast
702  onShowHome: showHome()
703  }
704 
705  Rectangle {
706  id: shutdownFadeOutRectangle
707  z: edgeDemo.z + 10
708  enabled: false
709  visible: false
710  color: "black"
711  anchors.fill: parent
712  opacity: 0.0
713  NumberAnimation on opacity {
714  id: shutdownFadeOut
715  from: 0.0
716  to: 1.0
717  onStopped: {
718  if (shutdownFadeOutRectangle.enabled && shutdownFadeOutRectangle.visible) {
719  DBusUnitySessionService.Shutdown();
720  }
721  }
722  }
723  }
724 
725 }