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