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