Unity 8
Shell.qml
1 /*
2  * Copyright (C) 2013-2015 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.Telephony 0.1 as Telephony
26 import Unity.Connectivity 0.1
27 import Unity.Launcher 0.1
28 import Utils 0.1
29 import LightDM 0.1 as LightDM
30 import Powerd 0.1
31 import SessionBroadcast 0.1
32 import "Greeter"
33 import "Launcher"
34 import "Panel"
35 import "Components"
36 import "Notifications"
37 import "Stages"
38 import "Tutorial"
39 import "Wizard"
40 import Unity.Notifications 1.0 as NotificationBackend
41 import Unity.Session 0.1
42 import Unity.DashCommunicator 0.1
43 import Unity.Indicators 0.1 as Indicators
44 
45 Item {
46  id: shell
47 
48  // to be set from outside
49  property int orientationAngle: 0
50  property int orientation
51  property int primaryOrientation
52  property int nativeOrientation
53  property real nativeWidth
54  property real nativeHeight
55  property alias indicatorAreaShowProgress: panel.indicatorAreaShowProgress
56  property bool beingResized
57  property string usageScenario: "phone" // supported values: "phone", "tablet" or "desktop"
58  property string mode: "full-greeter"
59  function updateFocusedAppOrientation() {
60  applicationsDisplayLoader.item.updateFocusedAppOrientation();
61  }
62  function updateFocusedAppOrientationAnimated() {
63  applicationsDisplayLoader.item.updateFocusedAppOrientationAnimated();
64  }
65 
66  // to be read from outside
67  readonly property int mainAppWindowOrientationAngle:
68  applicationsDisplayLoader.item ? applicationsDisplayLoader.item.mainAppWindowOrientationAngle : 0
69 
70  readonly property bool orientationChangesEnabled: panel.indicators.fullyClosed
71  && (applicationsDisplayLoader.item && applicationsDisplayLoader.item.orientationChangesEnabled)
72  && (!greeter || !greeter.animating)
73 
74  readonly property bool showingGreeter: greeter && greeter.shown
75 
76  property bool startingUp: true
77  Timer { id: finishStartUpTimer; interval: 500; onTriggered: startingUp = false }
78 
79  property int supportedOrientations: {
80  if (startingUp) {
81  // Ensure we don't rotate during start up
82  return Qt.PrimaryOrientation;
83  } else if (greeter && greeter.shown) {
84  return Qt.PrimaryOrientation;
85  } else if (mainApp) {
86  return mainApp.supportedOrientations;
87  } else {
88  // we just don't care
89  return Qt.PortraitOrientation
90  | Qt.LandscapeOrientation
91  | Qt.InvertedPortraitOrientation
92  | Qt.InvertedLandscapeOrientation;
93  }
94  }
95 
96  // For autopilot consumption
97  readonly property string focusedApplicationId: ApplicationManager.focusedApplicationId
98 
99  // internal props from here onwards
100  readonly property var mainApp:
101  applicationsDisplayLoader.item ? applicationsDisplayLoader.item.mainApp : null
102 
103  // Disable everything while greeter is waiting, so that the user can't swipe
104  // the greeter or launcher until we know whether the session is locked.
105  enabled: greeter && !greeter.waiting
106 
107  property real edgeSize: units.gu(2)
108  property url defaultBackground: Qt.resolvedUrl(shell.width >= units.gu(60) ? "graphics/tablet_background.jpg" : "graphics/phone_background.jpg")
109  property url background: asImageTester.status == Image.Ready ? asImageTester.source
110  : gsImageTester.status == Image.Ready ? gsImageTester.source : defaultBackground
111 
112  readonly property alias greeter: greeterLoader.item
113 
114  function activateApplication(appId) {
115  if (ApplicationManager.findApplication(appId)) {
116  ApplicationManager.requestFocusApplication(appId);
117  } else {
118  var execFlags = shell.usageScenario === "phone" ? ApplicationManager.ForceMainStage
119  : ApplicationManager.NoFlag;
120  ApplicationManager.startApplication(appId, execFlags);
121  }
122  }
123 
124  function startLockedApp(app) {
125  if (greeter.locked) {
126  greeter.lockedApp = app;
127  }
128  shell.activateApplication(app);
129  }
130 
131  // This is a dummy image to detect if the custom AS set wallpaper loads successfully.
132  Image {
133  id: asImageTester
134  source: AccountsService.backgroundFile != undefined && AccountsService.backgroundFile.length > 0 ? AccountsService.backgroundFile : ""
135  height: 0
136  width: 0
137  sourceSize.height: 0
138  sourceSize.width: 0
139  }
140 
141  GSettings {
142  id: backgroundSettings
143  schema.id: "org.gnome.desktop.background"
144  }
145 
146  // This is a dummy image to detect if the custom GSettings set wallpaper loads successfully.
147  Image {
148  id: gsImageTester
149  source: backgroundSettings.pictureUri != undefined && backgroundSettings.pictureUri.length > 0 ? backgroundSettings.pictureUri : ""
150  height: 0
151  width: 0
152  sourceSize.height: 0
153  sourceSize.width: 0
154  }
155 
156  Binding {
157  target: LauncherModel
158  property: "applicationManager"
159  value: ApplicationManager
160  }
161 
162  Component.onCompleted: {
163  Theme.name = "Ubuntu.Components.Themes.SuruGradient"
164  if (ApplicationManager.count > 0) {
165  ApplicationManager.focusApplication(ApplicationManager.get(0).appId);
166  }
167  finishStartUpTimer.start();
168  }
169 
170  VolumeControl {
171  id: volumeControl
172  }
173 
174  DashCommunicator {
175  id: dash
176  objectName: "dashCommunicator"
177  }
178 
179  PhysicalKeysMapper {
180  id: physicalKeysMapper
181  objectName: "physicalKeysMapper"
182 
183  onPowerKeyLongPressed: dialogs.showPowerDialog()
184  onVolumeDownTriggered: volumeControl.volumeDown();
185  onVolumeUpTriggered: volumeControl.volumeUp();
186  onScreenshotTriggered: screenGrabber.capture();
187  }
188 
189  ScreenGrabber {
190  id: screenGrabber
191  z: dialogs.z + 10
192  }
193 
194  WindowKeysFilter {
195  Keys.onPressed: physicalKeysMapper.onKeyPressed(event);
196  Keys.onReleased: physicalKeysMapper.onKeyReleased(event);
197  }
198 
199  HomeKeyWatcher {
200  onActivated: { launcher.fadeOut(); shell.showHome(); }
201  }
202 
203  Item {
204  id: stages
205  objectName: "stages"
206  width: parent.width
207  height: parent.height
208 
209  Connections {
210  target: ApplicationManager
211 
212  // This signal is also fired when we try to focus the current app
213  // again. We rely on this!
214  onFocusedApplicationIdChanged: {
215  var appId = ApplicationManager.focusedApplicationId;
216 
217  if (tutorial.running && appId != "" && appId != "unity8-dash") {
218  // If this happens on first boot, we may be in edge
219  // tutorial or wizard while receiving a call. But a call
220  // is more important than wizard so just bail out of those.
221  tutorial.finish();
222  wizard.hide();
223  }
224 
225  if (appId === "dialer-app" && callManager.hasCalls && greeter.locked) {
226  // If we are in the middle of a call, make dialer lockedApp and show it.
227  // This can happen if user backs out of dialer back to greeter, then
228  // launches dialer again.
229  greeter.lockedApp = appId;
230  }
231  greeter.notifyAppFocused(appId);
232 
233  panel.indicators.hide();
234  }
235 
236  onApplicationAdded: {
237  launcher.hide();
238  }
239  }
240 
241  Loader {
242  id: applicationsDisplayLoader
243  objectName: "applicationsDisplayLoader"
244  anchors.fill: parent
245 
246  // When we have a locked app, we only want to show that one app.
247  // FIXME: do this in a less traumatic way. We currently only allow
248  // locked apps in phone mode (see FIXME in Lockscreen component in
249  // this same file). When that changes, we need to do something
250  // nicer here. But this code is currently just to prevent a
251  // theoretical attack where user enters lockedApp mode, then makes
252  // the screen larger (maybe connects to monitor) and tries to enter
253  // tablet mode.
254 
255  property string usageScenario: shell.usageScenario === "phone" || greeter.hasLockedApp
256  ? "phone"
257  : shell.usageScenario
258  source: {
259  if(shell.mode === "greeter") {
260  return "Stages/ShimStage.qml"
261  } else if (applicationsDisplayLoader.usageScenario === "phone") {
262  return "Stages/PhoneStage.qml";
263  } else if (applicationsDisplayLoader.usageScenario === "tablet") {
264  return "Stages/TabletStage.qml";
265  } else {
266  return "Stages/DesktopStage.qml";
267  }
268  }
269 
270  property bool interactive: tutorial.spreadEnabled
271  && (!greeter || !greeter.shown)
272  && panel.indicators.fullyClosed
273  && launcher.progress == 0
274  && !notifications.useModal
275 
276  onInteractiveChanged: { if (interactive) { focus = true; } }
277 
278  Binding {
279  target: applicationsDisplayLoader.item
280  property: "objectName"
281  value: "stage"
282  }
283  Binding {
284  target: applicationsDisplayLoader.item
285  property: "dragAreaWidth"
286  value: shell.edgeSize
287  }
288  Binding {
289  target: applicationsDisplayLoader.item
290  property: "maximizedAppTopMargin"
291  // Not just using panel.panelHeight as that changes depending on the focused app.
292  value: panel.indicators.minimizedPanelHeight + units.dp(2) // dp(2) for orange line
293  }
294  Binding {
295  target: applicationsDisplayLoader.item
296  property: "interactive"
297  value: applicationsDisplayLoader.interactive
298  }
299  Binding {
300  target: applicationsDisplayLoader.item
301  property: "spreadEnabled"
302  value: tutorial.spreadEnabled && (!greeter || (!greeter.hasLockedApp && !greeter.shown))
303  }
304  Binding {
305  target: applicationsDisplayLoader.item
306  property: "inverseProgress"
307  value: greeter && greeter.locked ? 0 : launcher.progress
308  }
309  Binding {
310  target: applicationsDisplayLoader.item
311  property: "shellOrientationAngle"
312  value: shell.orientationAngle
313  }
314  Binding {
315  target: applicationsDisplayLoader.item
316  property: "shellOrientation"
317  value: shell.orientation
318  }
319  Binding {
320  target: applicationsDisplayLoader.item
321  property: "background"
322  value: shell.background
323  }
324  Binding {
325  target: applicationsDisplayLoader.item
326  property: "shellPrimaryOrientation"
327  value: shell.primaryOrientation
328  }
329  Binding {
330  target: applicationsDisplayLoader.item
331  property: "nativeOrientation"
332  value: shell.nativeOrientation
333  }
334  Binding {
335  target: applicationsDisplayLoader.item
336  property: "nativeWidth"
337  value: shell.nativeWidth
338  }
339  Binding {
340  target: applicationsDisplayLoader.item
341  property: "nativeHeight"
342  value: shell.nativeHeight
343  }
344  Binding {
345  target: applicationsDisplayLoader.item
346  property: "beingResized"
347  value: shell.beingResized
348  }
349  Binding {
350  target: applicationsDisplayLoader.item
351  property: "keepDashRunning"
352  value: launcher.shown || launcher.dashSwipe
353  }
354  Binding {
355  target: applicationsDisplayLoader.item
356  property: "suspended"
357  value: greeter.shown
358  }
359  Binding {
360  target: applicationsDisplayLoader.item
361  property: "altTabPressed"
362  value: physicalKeysMapper.altTabPressed
363  }
364  }
365 
366  Tutorial {
367  id: tutorial
368  objectName: "tutorial"
369  anchors.fill: parent
370  active: AccountsService.demoEdges
371  paused: LightDM.Greeter.active
372  launcher: launcher
373  panel: panel
374  edgeSize: shell.edgeSize
375 
376  // EdgeDragAreas don't work with mice. So to avoid trapping the user,
377  // we'll tell the tutorial to avoid using them on the Desktop. The
378  // Desktop doesn't use the same spread design anyway. The tutorial is
379  // all a bit of a placeholder on non-phone form factors right now.
380  // When the design team gives us more guidance, we can do something
381  // more clever here.
382  // TODO: use DeviceConfiguration instead of checking source
383  useEdgeDragArea: applicationsDisplayLoader.source != Qt.resolvedUrl("Stages/DesktopStage.qml")
384 
385  onFinished: {
386  AccountsService.demoEdges = false;
387  active = false; // for immediate response / if AS is having problems
388  }
389  }
390  }
391 
392  InputMethod {
393  id: inputMethod
394  objectName: "inputMethod"
395  anchors { fill: parent; topMargin: panel.panelHeight }
396  z: notifications.useModal || panel.indicators.shown || wizard.active ? overlay.z + 1 : overlay.z - 1
397  }
398 
399  Connections {
400  target: SurfaceManager
401  onSurfaceCreated: {
402  if (surface.type == MirSurfaceItem.InputMethod) {
403  inputMethod.surface = surface;
404  }
405  }
406 
407  onSurfaceDestroyed: {
408  if (inputMethod.surface == surface) {
409  inputMethod.surface = null;
410  surface.parent = null;
411  }
412  if (!surface.parent) {
413  // there's no one displaying it. delete it right away
414  surface.release();
415  }
416  }
417  }
418  Connections {
419  target: SessionManager
420  onSessionStopping: {
421  if (!session.parentSession && !session.application) {
422  // nothing is using it. delete it right away
423  session.release();
424  }
425  }
426  }
427 
428  Loader {
429  id: greeterLoader
430  anchors.fill: parent
431  anchors.topMargin: panel.panelHeight
432  sourceComponent: shell.mode != "shell" ? integratedGreeter :
433  Qt.createComponent(Qt.resolvedUrl("Greeter/ShimGreeter.qml"));
434  onLoaded: {
435  item.objectName = "greeter"
436  }
437  }
438 
439  Component {
440  id: integratedGreeter
441  Greeter {
442 
443  hides: [launcher, panel.indicators]
444  tabletMode: shell.usageScenario != "phone"
445  launcherOffset: launcher.progress
446  forcedUnlock: tutorial.running
447  background: shell.background
448 
449  // avoid overlapping with Launcher's edge drag area
450  // FIXME: Fix TouchRegistry & friends and remove this workaround
451  // Issue involves launcher's DDA getting disabled on a long
452  // left-edge drag
453  dragHandleLeftMargin: launcher.available ? launcher.dragAreaWidth + 1 : 0
454 
455  onSessionStarted: {
456  launcher.hide();
457  }
458 
459  onTease: {
460  if (!tutorial.running) {
461  launcher.tease();
462  }
463  }
464 
465  onEmergencyCall: startLockedApp("dialer-app")
466  }
467  }
468 
469  Timer {
470  // See powerConnection for why this is useful
471  id: showGreeterDelayed
472  interval: 1
473  onTriggered: {
474  greeter.forceShow();
475  }
476  }
477 
478  Connections {
479  id: callConnection
480  target: callManager
481 
482  onHasCallsChanged: {
483  if (greeter.locked && callManager.hasCalls && greeter.lockedApp !== "dialer-app") {
484  // We just received an incoming call while locked. The
485  // indicator will have already launched dialer-app for us, but
486  // there is a race between "hasCalls" changing and the dialer
487  // starting up. So in case we lose that race, we'll start/
488  // focus the dialer ourselves here too. Even if the indicator
489  // didn't launch the dialer for some reason (or maybe a call
490  // started via some other means), if an active call is
491  // happening, we want to be in the dialer.
492  startLockedApp("dialer-app")
493  }
494  }
495  }
496 
497  Connections {
498  id: powerConnection
499  target: Powerd
500 
501  onStatusChanged: {
502  if (Powerd.status === Powerd.Off && reason !== Powerd.Proximity &&
503  !callManager.hasCalls && !tutorial.running) {
504  // We don't want to simply call greeter.showNow() here, because
505  // that will take too long. Qt will delay button event
506  // handling until the greeter is done loading and may think the
507  // user held down the power button the whole time, leading to a
508  // power dialog being shown. Instead, delay showing the
509  // greeter until we've finished handling the event. We could
510  // make the greeter load asynchronously instead, but that
511  // introduces a whole host of timing issues, especially with
512  // its animations. So this is simpler.
513  showGreeterDelayed.start();
514  }
515  }
516  }
517 
518  function showHome() {
519  if (tutorial.running) {
520  return
521  }
522 
523  greeter.notifyAboutToFocusApp("unity8-dash");
524 
525  var animate = !LightDM.Greeter.active && !stages.shown
526  dash.setCurrentScope(0, animate, false)
527  ApplicationManager.requestFocusApplication("unity8-dash")
528  }
529 
530  function showDash() {
531  if (greeter.notifyShowingDashFromDrag()) {
532  launcher.fadeOut();
533  }
534 
535  if (!greeter.locked && ApplicationManager.focusedApplicationId != "unity8-dash") {
536  ApplicationManager.requestFocusApplication("unity8-dash")
537  launcher.fadeOut();
538  }
539  }
540 
541  Item {
542  id: overlay
543  z: 10
544 
545  anchors.fill: parent
546 
547  Panel {
548  id: panel
549  objectName: "panel"
550  anchors.fill: parent //because this draws indicator menus
551  indicators {
552  hides: [launcher]
553  available: tutorial.panelEnabled
554  && ((!greeter || !greeter.locked) || AccountsService.enableIndicatorsWhileLocked)
555  && (!greeter || !greeter.hasLockedApp)
556  contentEnabled: tutorial.panelContentEnabled
557  width: parent.width > units.gu(60) ? units.gu(40) : parent.width
558 
559  minimizedPanelHeight: units.gu(3)
560  expandedPanelHeight: units.gu(7)
561 
562  indicatorsModel: Indicators.IndicatorsModel {
563  // tablet and phone both use the same profile
564  profile: shell.usageScenario === "desktop" ? "desktop" : "phone"
565  Component.onCompleted: load();
566  }
567  }
568 
569  callHint {
570  greeterShown: greeter.shown
571  }
572 
573  property bool mainAppIsFullscreen: shell.mainApp && shell.mainApp.fullscreen
574  fullscreenMode: (mainAppIsFullscreen && !LightDM.Greeter.active && launcher.progress == 0)
575  || greeter.hasLockedApp
576  }
577 
578  Launcher {
579  id: launcher
580  objectName: "launcher"
581 
582  readonly property bool dashSwipe: progress > 0
583 
584  anchors.top: parent.top
585  anchors.topMargin: inverted ? 0 : panel.panelHeight
586  anchors.bottom: parent.bottom
587  width: parent.width
588  dragAreaWidth: shell.edgeSize
589  available: tutorial.launcherEnabled
590  && (!greeter.locked || AccountsService.enableLauncherWhileLocked)
591  && !greeter.hasLockedApp
592  inverted: shell.usageScenario !== "desktop"
593  shadeBackground: !tutorial.running
594 
595  onShowDashHome: showHome()
596  onDash: showDash()
597  onDashSwipeChanged: {
598  if (dashSwipe) {
599  dash.setCurrentScope(0, false, true)
600  }
601  }
602  onLauncherApplicationSelected: {
603  if (!tutorial.running) {
604  greeter.notifyAboutToFocusApp(appId);
605  shell.activateApplication(appId)
606  }
607  }
608  onShownChanged: {
609  if (shown) {
610  panel.indicators.hide()
611  }
612  }
613  }
614 
615  Wizard {
616  id: wizard
617  objectName: "wizard"
618  anchors.fill: parent
619  background: shell.background
620 
621  function unlockWhenDoneWithWizard() {
622  if (!active) {
623  Connectivity.unlockAllModems();
624  }
625  }
626 
627  Component.onCompleted: unlockWhenDoneWithWizard()
628  onActiveChanged: unlockWhenDoneWithWizard()
629  }
630 
631  Rectangle {
632  id: modalNotificationBackground
633 
634  visible: notifications.useModal
635  color: "#000000"
636  anchors.fill: parent
637  opacity: 0.9
638 
639  MouseArea {
640  anchors.fill: parent
641  }
642  }
643 
644  Notifications {
645  id: notifications
646 
647  model: NotificationBackend.Model
648  margin: units.gu(1)
649 
650  y: topmostIsFullscreen ? 0 : panel.panelHeight
651  height: parent.height - (topmostIsFullscreen ? 0 : panel.panelHeight)
652 
653  states: [
654  State {
655  name: "narrow"
656  when: overlay.width <= units.gu(60)
657  AnchorChanges {
658  target: notifications
659  anchors.left: parent.left
660  anchors.right: parent.right
661  }
662  },
663  State {
664  name: "wide"
665  when: overlay.width > units.gu(60)
666  AnchorChanges {
667  target: notifications
668  anchors.left: undefined
669  anchors.right: parent.right
670  }
671  PropertyChanges { target: notifications; width: units.gu(38) }
672  }
673  ]
674  }
675  }
676 
677  Dialogs {
678  id: dialogs
679  objectName: "dialogs"
680  anchors.fill: parent
681  z: overlay.z + 10
682  onPowerOffClicked: {
683  shutdownFadeOutRectangle.enabled = true;
684  shutdownFadeOutRectangle.visible = true;
685  shutdownFadeOut.start();
686  }
687  }
688 
689  Connections {
690  target: SessionBroadcast
691  onShowHome: showHome()
692  }
693 
694  Rectangle {
695  id: shutdownFadeOutRectangle
696  z: screenGrabber.z + 10
697  enabled: false
698  visible: false
699  color: "black"
700  anchors.fill: parent
701  opacity: 0.0
702  NumberAnimation on opacity {
703  id: shutdownFadeOut
704  from: 0.0
705  to: 1.0
706  onStopped: {
707  if (shutdownFadeOutRectangle.enabled && shutdownFadeOutRectangle.visible) {
708  DBusUnitySessionService.Shutdown();
709  }
710  }
711  }
712  }
713 
714 }