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  ScreenGrabber {
154  id: screenGrabber
155  z: dialogs.z + 10
156  enabled: Powerd.status === Powerd.On
157  }
158 
159  Binding {
160  target: ApplicationManager
161  property: "forceDashActive"
162  value: launcher.shown || launcher.dashSwipe
163  }
164 
165  VolumeKeyFilter {
166  id: volumeKeyFilter
167  onVolumeDownPressed: volumeControl.volumeDown()
168  onVolumeUpPressed: volumeControl.volumeUp()
169  onBothVolumeKeysPressed: screenGrabber.capture()
170  }
171 
172  WindowKeysFilter {
173  Keys.onPressed: {
174  // Nokia earpieces give TogglePlayPause, while the iPhone's earpiece gives Play
175  if (event.key == Qt.Key_MediaTogglePlayPause || event.key == Qt.Key_MediaPlay) {
176  event.accepted = callManager.handleMediaKey(false);
177  } else if (event.key == Qt.Key_PowerOff || event.key == Qt.Key_PowerDown) {
178  // FIXME: We only consider power key presses if the screen is
179  // on because of bugs 1410830/1409003. The theory is that when
180  // those bugs are encountered, there is a >2s delay between the
181  // power press event and the power release event, which causes
182  // the shutdown dialog to appear on resume. So to avoid that
183  // symptom while we investigate the root cause, we simply won't
184  // initiate any dialogs when the screen is off.
185  if (Powerd.status === Powerd.On) {
186  dialogs.onPowerKeyPressed();
187  }
188  event.accepted = true;
189  } else {
190  volumeKeyFilter.onKeyPressed(event.key);
191  event.accepted = false;
192  }
193  }
194 
195  Keys.onReleased: {
196  if (event.key == Qt.Key_PowerOff || event.key == Qt.Key_PowerDown) {
197  dialogs.onPowerKeyReleased();
198  event.accepted = true;
199  } else {
200  volumeKeyFilter.onKeyReleased(event.key);
201  event.accepted = false;
202  }
203  }
204  }
205 
206  Item {
207  id: stages
208  objectName: "stages"
209  width: parent.width
210  height: parent.height
211  visible: !ApplicationManager.empty
212 
213  Connections {
214  target: ApplicationManager
215 
216  // This signal is also fired when we try to focus the current app
217  // again. We rely on this!
218  onFocusedApplicationIdChanged: {
219  var appId = ApplicationManager.focusedApplicationId;
220 
221  if (tutorial.running && appId != "unity8-dash") {
222  // If this happens on first boot, we may be in edge
223  // tutorial or wizard while receiving a call. But a call
224  // is more important than wizard so just bail out of those.
225  tutorial.finish();
226  wizard.hide();
227  }
228 
229  if (appId === "dialer-app" && callManager.hasCalls && greeter.locked) {
230  // If we are in the middle of a call, make dialer lockedApp and show it.
231  // This can happen if user backs out of dialer back to greeter, then
232  // launches dialer again.
233  greeter.lockedApp = appId;
234  }
235  greeter.notifyAppFocused(appId);
236 
237  panel.indicators.hide();
238  }
239 
240  onApplicationAdded: {
241  launcher.hide();
242  }
243  }
244 
245  Loader {
246  id: applicationsDisplayLoader
247  objectName: "applicationsDisplayLoader"
248  anchors.fill: parent
249 
250  // When we have a locked app, we only want to show that one app.
251  // FIXME: do this in a less traumatic way. We currently only allow
252  // locked apps in phone mode (see FIXME in Lockscreen component in
253  // this same file). When that changes, we need to do something
254  // nicer here. But this code is currently just to prevent a
255  // theoretical attack where user enters lockedApp mode, then makes
256  // the screen larger (maybe connects to monitor) and tries to enter
257  // tablet mode.
258  property bool tabletMode: shell.sideStageEnabled && !greeter.hasLockedApp
259  source: usageModeSettings.usageMode === "Windowed" ? "Stages/DesktopStage.qml"
260  : tabletMode ? "Stages/TabletStage.qml" : "Stages/PhoneStage.qml"
261 
262  property bool interactive: tutorial.spreadEnabled
263  && !greeter.shown
264  && panel.indicators.fullyClosed
265  && launcher.progress == 0
266  && !notifications.useModal
267 
268  onInteractiveChanged: { if (interactive) { focus = true; } }
269 
270  Binding {
271  target: applicationsDisplayLoader.item
272  property: "objectName"
273  value: "stage"
274  }
275  Binding {
276  target: applicationsDisplayLoader.item
277  property: "dragAreaWidth"
278  value: shell.edgeSize
279  }
280  Binding {
281  target: applicationsDisplayLoader.item
282  property: "maximizedAppTopMargin"
283  // Not just using panel.panelHeight as that changes depending on the focused app.
284  value: panel.indicators.minimizedPanelHeight + units.dp(2) // dp(2) for orange line
285  }
286  Binding {
287  target: applicationsDisplayLoader.item
288  property: "interactive"
289  value: applicationsDisplayLoader.interactive
290  }
291  Binding {
292  target: applicationsDisplayLoader.item
293  property: "spreadEnabled"
294  value: tutorial.spreadEnabled && !greeter.hasLockedApp
295  }
296  Binding {
297  target: applicationsDisplayLoader.item
298  property: "inverseProgress"
299  value: greeter.locked ? 0 : launcher.progress
300  }
301  Binding {
302  target: applicationsDisplayLoader.item
303  property: "orientation"
304  value: shell.orientation
305  }
306  Binding {
307  target: applicationsDisplayLoader.item
308  property: "background"
309  value: shell.background
310  }
311  }
312  }
313 
314  InputMethod {
315  id: inputMethod
316  objectName: "inputMethod"
317  anchors { fill: parent; topMargin: panel.panelHeight }
318  z: notifications.useModal || panel.indicators.shown || wizard.active ? overlay.z + 1 : overlay.z - 1
319  }
320 
321  Connections {
322  target: SurfaceManager
323  onSurfaceCreated: {
324  if (surface.type == MirSurfaceItem.InputMethod) {
325  inputMethod.surface = surface;
326  }
327  }
328 
329  onSurfaceDestroyed: {
330  if (inputMethod.surface == surface) {
331  inputMethod.surface = null;
332  surface.parent = null;
333  }
334  if (!surface.parent) {
335  // there's no one displaying it. delete it right away
336  surface.release();
337  }
338  }
339  }
340  Connections {
341  target: SessionManager
342  onSessionStopping: {
343  if (!session.parentSession && !session.application) {
344  // nothing is using it. delete it right away
345  session.release();
346  }
347  }
348  }
349 
350  Greeter {
351  id: greeter
352  objectName: "greeter"
353 
354  hides: [launcher, panel.indicators]
355  tabletMode: shell.sideStageEnabled
356  launcherOffset: launcher.progress
357  forcedUnlock: tutorial.running
358  background: shell.background
359 
360  anchors.fill: parent
361  anchors.topMargin: panel.panelHeight
362 
363  // avoid overlapping with Launcher's edge drag area
364  // FIXME: Fix TouchRegistry & friends and remove this workaround
365  // Issue involves launcher's DDA getting disabled on a long
366  // left-edge drag
367  dragHandleLeftMargin: launcher.available ? launcher.dragAreaWidth + 1 : 0
368 
369  onSessionStarted: {
370  launcher.hide();
371  }
372 
373  onTease: {
374  if (!tutorial.running) {
375  launcher.tease();
376  }
377  }
378 
379  onEmergencyCall: startLockedApp("dialer-app")
380 
381  Timer {
382  // See powerConnection for why this is useful
383  id: showGreeterDelayed
384  interval: 1
385  onTriggered: {
386  greeter.forceShow();
387  }
388  }
389 
390  Binding {
391  target: ApplicationManager
392  property: "suspended"
393  value: greeter.shown
394  }
395  }
396 
397  Connections {
398  id: callConnection
399  target: callManager
400 
401  onHasCallsChanged: {
402  if (greeter.locked && callManager.hasCalls && greeter.lockedApp !== "dialer-app") {
403  // We just received an incoming call while locked. The
404  // indicator will have already launched dialer-app for us, but
405  // there is a race between "hasCalls" changing and the dialer
406  // starting up. So in case we lose that race, we'll start/
407  // focus the dialer ourselves here too. Even if the indicator
408  // didn't launch the dialer for some reason (or maybe a call
409  // started via some other means), if an active call is
410  // happening, we want to be in the dialer.
411  startLockedApp("dialer-app")
412  }
413  }
414  }
415 
416  Connections {
417  id: powerConnection
418  target: Powerd
419 
420  onStatusChanged: {
421  if (Powerd.status === Powerd.Off && reason !== Powerd.Proximity &&
422  !callManager.hasCalls && !tutorial.running) {
423  // We don't want to simply call greeter.showNow() here, because
424  // that will take too long. Qt will delay button event
425  // handling until the greeter is done loading and may think the
426  // user held down the power button the whole time, leading to a
427  // power dialog being shown. Instead, delay showing the
428  // greeter until we've finished handling the event. We could
429  // make the greeter load asynchronously instead, but that
430  // introduces a whole host of timing issues, especially with
431  // its animations. So this is simpler.
432  showGreeterDelayed.start();
433  }
434  }
435  }
436 
437  function showHome() {
438  if (tutorial.running) {
439  return
440  }
441 
442  greeter.notifyAboutToFocusApp("unity8-dash");
443 
444  var animate = !LightDM.Greeter.active && !stages.shown
445  dash.setCurrentScope(0, animate, false)
446  ApplicationManager.requestFocusApplication("unity8-dash")
447  }
448 
449  function showDash() {
450  if (greeter.notifyShowingDashFromDrag()) {
451  launcher.fadeOut();
452  }
453 
454  if (!greeter.locked && ApplicationManager.focusedApplicationId != "unity8-dash") {
455  ApplicationManager.requestFocusApplication("unity8-dash")
456  launcher.fadeOut();
457  }
458  }
459 
460  Item {
461  id: overlay
462  z: 10
463 
464  anchors.fill: parent
465 
466  Panel {
467  id: panel
468  objectName: "panel"
469  anchors.fill: parent //because this draws indicator menus
470  indicators {
471  hides: [launcher]
472  available: tutorial.panelEnabled
473  && (!greeter.locked || AccountsService.enableIndicatorsWhileLocked)
474  && !greeter.hasLockedApp
475  contentEnabled: tutorial.panelContentEnabled
476  width: parent.width > units.gu(60) ? units.gu(40) : parent.width
477 
478  minimizedPanelHeight: units.gu(3)
479  expandedPanelHeight: units.gu(7)
480 
481  indicatorsModel: Indicators.IndicatorsModel {
482  // TODO: This should be sourced by device type (e.g. "desktop", "tablet", "phone"...)
483  profile: indicatorProfile
484  Component.onCompleted: load()
485  }
486  }
487  callHint {
488  greeterShown: greeter.shown
489  }
490 
491  property bool topmostApplicationIsFullscreen:
492  ApplicationManager.focusedApplicationId &&
493  ApplicationManager.findApplication(ApplicationManager.focusedApplicationId).fullscreen
494 
495  fullscreenMode: (topmostApplicationIsFullscreen && !LightDM.Greeter.active && launcher.progress == 0)
496  || greeter.hasLockedApp
497  }
498 
499  Launcher {
500  id: launcher
501  objectName: "launcher"
502 
503  readonly property bool dashSwipe: progress > 0
504 
505  anchors.top: parent.top
506  anchors.topMargin: inverted ? 0 : panel.panelHeight
507  anchors.bottom: parent.bottom
508  width: parent.width
509  dragAreaWidth: shell.edgeSize
510  available: tutorial.launcherEnabled
511  && (!greeter.locked || AccountsService.enableLauncherWhileLocked)
512  && !greeter.hasLockedApp
513  inverted: usageModeSettings.usageMode === "Staged"
514  shadeBackground: !tutorial.running
515 
516  onShowDashHome: showHome()
517  onDash: showDash()
518  onDashSwipeChanged: {
519  if (dashSwipe) {
520  dash.setCurrentScope(0, false, true)
521  }
522  }
523  onLauncherApplicationSelected: {
524  if (!tutorial.running) {
525  greeter.notifyAboutToFocusApp(appId);
526  shell.activateApplication(appId)
527  }
528  }
529  onShownChanged: {
530  if (shown) {
531  panel.indicators.hide()
532  }
533  }
534  }
535 
536  Wizard {
537  id: wizard
538  anchors.fill: parent
539  background: shell.background
540  }
541 
542  Rectangle {
543  id: modalNotificationBackground
544 
545  visible: notifications.useModal && (notifications.state == "narrow")
546  color: "#000000"
547  anchors.fill: parent
548  opacity: 0.9
549 
550  MouseArea {
551  anchors.fill: parent
552  }
553  }
554 
555  Notifications {
556  id: notifications
557 
558  model: NotificationBackend.Model
559  margin: units.gu(1)
560 
561  y: topmostIsFullscreen ? 0 : panel.panelHeight
562  height: parent.height - (topmostIsFullscreen ? 0 : panel.panelHeight)
563 
564  states: [
565  State {
566  name: "narrow"
567  when: overlay.width <= units.gu(60)
568  AnchorChanges {
569  target: notifications
570  anchors.left: parent.left
571  anchors.right: parent.right
572  }
573  },
574  State {
575  name: "wide"
576  when: overlay.width > units.gu(60)
577  AnchorChanges {
578  target: notifications
579  anchors.left: undefined
580  anchors.right: parent.right
581  }
582  PropertyChanges { target: notifications; width: units.gu(38) }
583  }
584  ]
585  }
586  }
587 
588  Dialogs {
589  id: dialogs
590  anchors.fill: parent
591  z: overlay.z + 10
592  onPowerOffClicked: {
593  shutdownFadeOutRectangle.enabled = true;
594  shutdownFadeOutRectangle.visible = true;
595  shutdownFadeOut.start();
596  }
597  }
598 
599  Tutorial {
600  id: tutorial
601  objectName: "tutorial"
602  active: AccountsService.demoEdges
603  paused: LightDM.Greeter.active
604  launcher: launcher
605  panel: panel
606  stages: stages
607  overlay: overlay
608  edgeSize: shell.edgeSize
609 
610  onFinished: {
611  AccountsService.demoEdges = false;
612  active = false; // for immediate response / if AS is having problems
613  }
614  }
615 
616  Connections {
617  target: SessionBroadcast
618  onShowHome: showHome()
619  }
620 
621  Rectangle {
622  id: shutdownFadeOutRectangle
623  z: screenGrabber.z + 10
624  enabled: false
625  visible: false
626  color: "black"
627  anchors.fill: parent
628  opacity: 0.0
629  NumberAnimation on opacity {
630  id: shutdownFadeOut
631  from: 0.0
632  to: 1.0
633  onStopped: {
634  if (shutdownFadeOutRectangle.enabled && shutdownFadeOutRectangle.visible) {
635  DBusUnitySessionService.Shutdown();
636  }
637  }
638  }
639  }
640 
641 }