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