/* * Copyright (C) 2013-2016 Canonical, Ltd. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ import QtQuick 2.4 import QtQuick.Window 2.2 import AccountsService 0.1 import Unity.Application 0.1 import Ubuntu.Components 1.3 import Ubuntu.Components.Popups 1.3 import Ubuntu.Gestures 0.1 import Ubuntu.Telephony 0.1 as Telephony import Unity.Connectivity 0.1 import Unity.Launcher 0.1 import GlobalShortcut 1.0 // has to be before Utils, because of WindowInputFilter import GSettings 1.0 import ImageCache 0.1 import Utils 0.1 import Powerd 0.1 import SessionBroadcast 0.1 import "Greeter" import "Launcher" import "Panel" import "Components" import "Notifications" import "Stage" import "Tutorial" import "Wizard" import Unity.Notifications 1.0 as NotificationBackend import Unity.Session 0.1 import Unity.Indicators 0.1 as Indicators import Cursor 1.1 import WindowManager 1.0 StyledItem { id: shell theme.name: "Ubuntu.Components.Themes.SuruDark" // to be set from outside property int orientationAngle: 0 property int orientation property Orientations orientations property real nativeWidth property real nativeHeight property alias panelAreaShowProgress: panel.panelAreaShowProgress property string usageScenario: "phone" // supported values: "phone", "tablet" or "desktop" property string mode: "full-greeter" property alias oskEnabled: inputMethod.enabled function updateFocusedAppOrientation() { stage.updateFocusedAppOrientation(); } function updateFocusedAppOrientationAnimated() { stage.updateFocusedAppOrientationAnimated(); } property bool hasMouse: false property bool hasKeyboard: false property bool hasTouchscreen: false // to be read from outside readonly property int mainAppWindowOrientationAngle: stage.mainAppWindowOrientationAngle readonly property bool orientationChangesEnabled: panel.indicators.fullyClosed && stage.orientationChangesEnabled && (!greeter || !greeter.animating) readonly property bool showingGreeter: greeter && greeter.shown property bool startingUp: true Timer { id: finishStartUpTimer; interval: 500; onTriggered: startingUp = false } property int supportedOrientations: { if (startingUp) { // Ensure we don't rotate during start up return Qt.PrimaryOrientation; } else if (showingGreeter || notifications.topmostIsFullscreen) { return Qt.PrimaryOrientation; } else { return shell.orientations.map(stage.supportedOrientations); } } readonly property var mainApp: stage.mainApp onMainAppChanged: { if (mainApp) { _onMainAppChanged(mainApp.appId); } } Connections { target: ApplicationManager onFocusRequested: { if (shell.mainApp && shell.mainApp.appId === appId) { _onMainAppChanged(appId); } } } function _onMainAppChanged(appId) { if (wizard.active && appId != "") { // If this happens on first boot, we may be in the // wizard while receiving a call. But a call is more // important than the wizard so just bail out of it. wizard.hide(); } if (appId === "dialer-app" && callManager.hasCalls && greeter.locked) { // If we are in the middle of a call, make dialer lockedApp and show it. // This can happen if user backs out of dialer back to greeter, then // launches dialer again. greeter.lockedApp = appId; } greeter.notifyAppFocusRequested(appId); panel.indicators.hide(); launcher.hide(launcher.ignoreHideIfMouseOverLauncher); } // For autopilot consumption readonly property string focusedApplicationId: ApplicationManager.focusedApplicationId // Note when greeter is waiting on PAM, so that we can disable edges until // we know which user data to show and whether the session is locked. readonly property bool waitingOnGreeter: greeter && greeter.waiting property real edgeSize: units.gu(settings.edgeDragWidth) WallpaperResolver { id: wallpaperResolver objectName: "wallpaperResolver" readonly property url defaultBackground: "file://" + Constants.defaultWallpaper readonly property bool hasCustomBackground: background != defaultBackground // Use a cached version of the scaled-down wallpaper (as sometimes the // image can be quite big compared to the device size, including for // our default wallpaper). We use a name=wallpaper argument here to // make sure we don't litter our cache with lots of scaled images. We // only need to bother caching one at a time. readonly property url cachedBackground: background.toString().indexOf("file:///") === 0 ? "image://unity8imagecache/" + background + "?name=wallpaper" : background GSettings { id: backgroundSettings schema.id: "org.gnome.desktop.background" } candidates: [ AccountsService.backgroundFile, backgroundSettings.pictureUri, defaultBackground ] } readonly property alias greeter: greeterLoader.item function activateApplication(appId) { // Either open the app in our own session, or -- if we're acting as a // greeter -- ask the user's session to open it for us. if (shell.mode === "greeter") { activateURL("application:///" + appId + ".desktop"); } else { startApp(appId); } } function activateURL(url) { SessionBroadcast.requestUrlStart(AccountsService.user, url); greeter.notifyUserRequestedApp(); panel.indicators.hide(); } function startApp(appId) { if (ApplicationManager.findApplication(appId)) { ApplicationManager.requestFocusApplication(appId); } else { ApplicationManager.startApplication(appId); } } function startLockedApp(app) { if (greeter.locked) { greeter.lockedApp = app; } startApp(app); // locked apps are always in our same session } Binding { target: LauncherModel property: "applicationManager" value: ApplicationManager } Component.onCompleted: { finishStartUpTimer.start(); } VolumeControl { id: volumeControl } PhysicalKeysMapper { id: physicalKeysMapper objectName: "physicalKeysMapper" onPowerKeyLongPressed: dialogs.showPowerDialog(); onVolumeDownTriggered: volumeControl.volumeDown(); onVolumeUpTriggered: volumeControl.volumeUp(); onScreenshotTriggered: itemGrabber.capture(shell); } GlobalShortcut { // dummy shortcut to force creation of GlobalShortcutRegistry before WindowInputFilter } WindowInputFilter { id: inputFilter Keys.onPressed: physicalKeysMapper.onKeyPressed(event, lastInputTimestamp); Keys.onReleased: physicalKeysMapper.onKeyReleased(event, lastInputTimestamp); } WindowInputMonitor { objectName: "windowInputMonitor" onHomeKeyActivated: { // Ignore when greeter is active, to avoid pocket presses if (!greeter.active) { launcher.openDrawer(false); } } onTouchBegun: { cursor.opacity = 0; } onTouchEnded: { // move the (hidden) cursor to the last known touch position var mappedCoords = mapFromItem(null, pos.x, pos.y); cursor.x = mappedCoords.x; cursor.y = mappedCoords.y; cursor.mouseNeverMoved = false; } } AvailableDesktopArea { id: availableDesktopAreaItem anchors.fill: parent anchors.topMargin: panel.fullscreenMode ? 0 : panel.minimizedPanelHeight anchors.leftMargin: launcher.lockedVisible ? launcher.panelWidth : 0 } GSettings { id: settings schema.id: "com.canonical.Unity8" } Item { id: stages objectName: "stages" width: parent.width height: parent.height SurfaceManager { id: surfaceMan objectName: "surfaceManager" } TopLevelWindowModel { id: topLevelSurfaceList objectName: "topLevelSurfaceList" applicationManager: ApplicationManager // it's a singleton surfaceManager: surfaceMan } Stage { id: stage objectName: "stage" anchors.fill: parent focus: true dragAreaWidth: shell.edgeSize background: wallpaperResolver.background applicationManager: ApplicationManager topLevelSurfaceList: topLevelSurfaceList inputMethodRect: inputMethod.visibleRect rightEdgePushProgress: rightEdgeBarrier.progress availableDesktopArea: availableDesktopAreaItem property string usageScenario: shell.usageScenario === "phone" || greeter.hasLockedApp ? "phone" : shell.usageScenario mode: usageScenario == "phone" ? "staged" : usageScenario == "tablet" ? "stagedWithSideStage" : "windowed" shellOrientation: shell.orientation shellOrientationAngle: shell.orientationAngle orientations: shell.orientations nativeWidth: shell.nativeWidth nativeHeight: shell.nativeHeight interactive: (!greeter || !greeter.shown) && panel.indicators.fullyClosed && !notifications.useModal onInteractiveChanged: { if (interactive) { focus = true; } } suspended: greeter.shown altTabPressed: physicalKeysMapper.altTabPressed oskEnabled: shell.oskEnabled spreadEnabled: tutorial.spreadEnabled && (!greeter || (!greeter.hasLockedApp && !greeter.shown)) onSpreadShownChanged: { panel.indicators.hide(); panel.applicationMenus.hide(); } } TouchGestureArea { anchors.fill: stage minimumTouchPoints: 4 maximumTouchPoints: minimumTouchPoints readonly property bool recognisedPress: status == TouchGestureArea.Recognized && touchPoints.length >= minimumTouchPoints && touchPoints.length <= maximumTouchPoints property bool wasPressed: false onRecognisedPressChanged: { if (recognisedPress) { wasPressed = true; } } onStatusChanged: { if (status !== TouchGestureArea.Recognized) { if (status === TouchGestureArea.WaitingForTouch) { if (wasPressed && !dragging) { launcher.openDrawer(true); } } wasPressed = false; } } } } InputMethod { id: inputMethod objectName: "inputMethod" surface: topLevelSurfaceList.inputMethodSurface anchors { fill: parent topMargin: panel.panelHeight leftMargin: launcher.lockedVisible ? launcher.panelWidth : 0 } z: notifications.useModal || panel.indicators.shown || wizard.active || tutorial.running || launcher.drawerShown ? overlay.z + 1 : overlay.z - 1 } Loader { id: greeterLoader objectName: "greeterLoader" anchors.fill: parent anchors.topMargin: panel.panelHeight sourceComponent: shell.mode != "shell" ? integratedGreeter : Qt.createComponent(Qt.resolvedUrl("Greeter/ShimGreeter.qml")); onLoaded: { item.objectName = "greeter" } property bool openDrawerAfterUnlock: false Connections { target: greeter onActiveChanged: { if (!greeter.active && greeterLoader.openDrawerAfterUnlock) { launcher.openDrawer(false); greeterLoader.openDrawerAfterUnlock = false; } } } } Component { id: integratedGreeter Greeter { enabled: panel.indicators.fullyClosed // hides OSK when panel is open hides: [launcher, panel.indicators, panel.applicationMenus] tabletMode: shell.usageScenario != "phone" forcedUnlock: wizard.active || shell.mode === "full-shell" background: wallpaperResolver.cachedBackground hasCustomBackground: wallpaperResolver.hasCustomBackground allowFingerprint: !dialogs.hasActiveDialog && !notifications.topmostIsFullscreen && !panel.indicators.shown // avoid overlapping with Launcher's edge drag area // FIXME: Fix TouchRegistry & friends and remove this workaround // Issue involves launcher's DDA getting disabled on a long // left-edge drag dragHandleLeftMargin: launcher.available ? launcher.dragAreaWidth + 1 : 0 onSessionStarted: { launcher.hide(); } onTease: { if (!tutorial.running) { launcher.tease(); } } onEmergencyCall: startLockedApp("dialer-app") } } Timer { // See powerConnection for why this is useful id: showGreeterDelayed interval: 1 onTriggered: { // Go through the dbus service, because it has checks for whether // we are even allowed to lock or not. DBusUnitySessionService.PromptLock(); } } Connections { id: callConnection target: callManager onHasCallsChanged: { if (greeter.locked && callManager.hasCalls && greeter.lockedApp !== "dialer-app") { // We just received an incoming call while locked. The // indicator will have already launched dialer-app for us, but // there is a race between "hasCalls" changing and the dialer // starting up. So in case we lose that race, we'll start/ // focus the dialer ourselves here too. Even if the indicator // didn't launch the dialer for some reason (or maybe a call // started via some other means), if an active call is // happening, we want to be in the dialer. startLockedApp("dialer-app") } } } Connections { id: powerConnection target: Powerd onStatusChanged: { if (Powerd.status === Powerd.Off && reason !== Powerd.Proximity && !callManager.hasCalls && !wizard.active) { // We don't want to simply call greeter.showNow() here, because // that will take too long. Qt will delay button event // handling until the greeter is done loading and may think the // user held down the power button the whole time, leading to a // power dialog being shown. Instead, delay showing the // greeter until we've finished handling the event. We could // make the greeter load asynchronously instead, but that // introduces a whole host of timing issues, especially with // its animations. So this is simpler. showGreeterDelayed.start(); } } } function showHome() { greeter.notifyUserRequestedApp(); if (shell.mode === "greeter") { SessionBroadcast.requestHomeShown(AccountsService.user); } else { if (!greeter.active) { launcher.openDrawer(false); } else { greeterLoader.openDrawerAfterUnlock = true; } } } Item { id: overlay z: 10 anchors.fill: parent Panel { id: panel objectName: "panel" anchors.fill: parent //because this draws indicator menus mode: shell.usageScenario == "desktop" ? "windowed" : "staged" minimizedPanelHeight: units.gu(3) expandedPanelHeight: units.gu(7) indicatorMenuWidth: parent.width > units.gu(60) ? units.gu(40) : parent.width applicationMenuWidth: parent.width > units.gu(60) ? units.gu(40) : parent.width applicationMenuContentX: launcher.lockedVisible ? launcher.panelWidth : 0 indicators { hides: [launcher] available: tutorial.panelEnabled && ((!greeter || !greeter.locked) || AccountsService.enableIndicatorsWhileLocked) && (!greeter || !greeter.hasLockedApp) && !shell.waitingOnGreeter && settings.enableIndicatorMenu model: Indicators.IndicatorsModel { // tablet and phone both use the same profile // FIXME: use just "phone" for greeter too, but first fix // greeter app launching to either load the app inside the // greeter or tell the session to load the app. This will // involve taking the url-dispatcher dbus name and using // SessionBroadcast to tell the session. profile: shell.mode === "greeter" ? "desktop_greeter" : "phone" Component.onCompleted: load(); } } applicationMenus { hides: [launcher] available: (!greeter || !greeter.shown) && !shell.waitingOnGreeter && !stage.spreadShown } readonly property bool focusedSurfaceIsFullscreen: topLevelSurfaceList.focusedWindow ? topLevelSurfaceList.focusedWindow.state === Mir.FullscreenState : false fullscreenMode: (focusedSurfaceIsFullscreen && !LightDMService.greeter.active && launcher.progress == 0 && !stage.spreadShown) || greeter.hasLockedApp greeterShown: greeter && greeter.shown hasKeyboard: shell.hasKeyboard } Launcher { id: launcher objectName: "launcher" anchors.top: parent.top anchors.topMargin: inverted ? 0 : panel.panelHeight anchors.bottom: parent.bottom width: parent.width dragAreaWidth: shell.edgeSize available: tutorial.launcherEnabled && (!greeter.locked || AccountsService.enableLauncherWhileLocked) && !greeter.hasLockedApp && !shell.waitingOnGreeter && settings.enableLauncher inverted: shell.usageScenario !== "desktop" superPressed: physicalKeysMapper.superPressed superTabPressed: physicalKeysMapper.superTabPressed panelWidth: units.gu(settings.launcherWidth) lockedVisible: shell.usageScenario == "desktop" && !settings.autohideLauncher && !panel.fullscreenMode blurSource: greeter.shown ? greeter : stages topPanelHeight: panel.panelHeight drawerEnabled: !greeter.active privateMode: greeter.active onShowDashHome: showHome() onLauncherApplicationSelected: { greeter.notifyUserRequestedApp(); shell.activateApplication(appId); } onShownChanged: { if (shown) { panel.indicators.hide(); panel.applicationMenus.hide(); } } onDrawerShownChanged: { if (drawerShown) { panel.indicators.hide(); panel.applicationMenus.hide(); } } onFocusChanged: { if (!focus) { stage.focus = true; } } GlobalShortcut { shortcut: Qt.MetaModifier | Qt.Key_A onTriggered: { launcher.openDrawer(true); } } GlobalShortcut { shortcut: Qt.AltModifier | Qt.Key_F1 onTriggered: { launcher.openForKeyboardNavigation(); } } GlobalShortcut { shortcut: Qt.MetaModifier | Qt.Key_0 onTriggered: { if (LauncherModel.get(9)) { activateApplication(LauncherModel.get(9).appId); } } } Repeater { model: 9 GlobalShortcut { shortcut: Qt.MetaModifier | (Qt.Key_1 + index) onTriggered: { if (LauncherModel.get(index)) { activateApplication(LauncherModel.get(index).appId); } } } } } KeyboardShortcutsOverlay { objectName: "shortcutsOverlay" enabled: launcher.shortcutHintsShown && width < parent.width - (launcher.lockedVisible ? launcher.panelWidth : 0) - padding && height < parent.height - padding - panel.panelHeight anchors.centerIn: parent anchors.horizontalCenterOffset: launcher.lockedVisible ? launcher.panelWidth/2 : 0 anchors.verticalCenterOffset: panel.panelHeight/2 visible: opacity > 0 opacity: enabled ? 0.95 : 0 Behavior on opacity { UbuntuNumberAnimation {} } } Tutorial { id: tutorial objectName: "tutorial" anchors.fill: parent paused: callManager.hasCalls || !greeter || greeter.active || wizard.active || !hasTouchscreen // TODO #1661557 something better for no touchscreen delayed: dialogs.hasActiveDialog || notifications.hasNotification || inputMethod.visible || (launcher.shown && !launcher.lockedVisible) || panel.indicators.shown || stage.rightEdgeDragProgress > 0 usageScenario: shell.usageScenario lastInputTimestamp: inputFilter.lastInputTimestamp launcher: launcher panel: panel stage: stage } Wizard { id: wizard objectName: "wizard" anchors.fill: parent deferred: shell.mode === "greeter" function unlockWhenDoneWithWizard() { if (!active) { Connectivity.unlockAllModems(); } } Component.onCompleted: unlockWhenDoneWithWizard() onActiveChanged: unlockWhenDoneWithWizard() } MouseArea { // modal notifications prevent interacting with other contents anchors.fill: parent visible: notifications.useModal enabled: visible } Notifications { id: notifications model: NotificationBackend.Model margin: units.gu(1) hasMouse: shell.hasMouse background: wallpaperResolver.cachedBackground y: topmostIsFullscreen ? 0 : panel.panelHeight height: parent.height - (topmostIsFullscreen ? 0 : panel.panelHeight) states: [ State { name: "narrow" when: overlay.width <= units.gu(60) AnchorChanges { target: notifications anchors.left: parent.left anchors.right: parent.right } }, State { name: "wide" when: overlay.width > units.gu(60) AnchorChanges { target: notifications anchors.left: undefined anchors.right: parent.right } PropertyChanges { target: notifications; width: units.gu(38) } } ] } EdgeBarrier { id: rightEdgeBarrier enabled: !greeter.shown // NB: it does its own positioning according to the specified edge edge: Qt.RightEdge onPassed: { panel.indicators.hide() } material: Component { Item { Rectangle { width: parent.height height: parent.width rotation: 90 anchors.centerIn: parent gradient: Gradient { GradientStop { position: 0.0; color: Qt.rgba(0.16,0.16,0.16,0.5)} GradientStop { position: 1.0; color: Qt.rgba(0.16,0.16,0.16,0)} } } } } } } Dialogs { id: dialogs objectName: "dialogs" anchors.fill: parent visible: hasActiveDialog z: overlay.z + 10 usageScenario: shell.usageScenario hasKeyboard: shell.hasKeyboard onPowerOffClicked: { shutdownFadeOutRectangle.enabled = true; shutdownFadeOutRectangle.visible = true; shutdownFadeOut.start(); } } Connections { target: SessionBroadcast onShowHome: if (shell.mode !== "greeter") showHome() } URLDispatcher { id: urlDispatcher objectName: "urlDispatcher" active: shell.mode === "greeter" onUrlRequested: shell.activateURL(url) } ItemGrabber { id: itemGrabber anchors.fill: parent z: dialogs.z + 10 GlobalShortcut { shortcut: Qt.Key_Print; onTriggered: itemGrabber.capture(shell) } Connections { target: stage ignoreUnknownSignals: true onItemSnapshotRequested: itemGrabber.capture(item) } } Timer { id: cursorHidingTimer interval: 3000 running: panel.focusedSurfaceIsFullscreen && cursor.opacity > 0 onTriggered: cursor.opacity = 0; } Cursor { id: cursor objectName: "cursor" visible: shell.hasMouse z: itemGrabber.z + 1 topBoundaryOffset: panel.panelHeight confiningItem: stage.itemConfiningMouseCursor property bool mouseNeverMoved: true Binding { target: cursor; property: "x"; value: shell.width / 2 when: cursor.mouseNeverMoved && cursor.visible } Binding { target: cursor; property: "y"; value: shell.height / 2 when: cursor.mouseNeverMoved && cursor.visible } height: units.gu(3) readonly property var previewRectangle: stage.previewRectangle.target && stage.previewRectangle.target.dragging ? stage.previewRectangle : null onPushedLeftBoundary: { if (buttons === Qt.NoButton) { launcher.pushEdge(amount); } else if (buttons === Qt.LeftButton && previewRectangle && previewRectangle.target.canBeMaximizedLeftRight) { previewRectangle.maximizeLeft(amount); } } onPushedRightBoundary: { if (buttons === Qt.NoButton) { rightEdgeBarrier.push(amount); } else if (buttons === Qt.LeftButton && previewRectangle && previewRectangle.target.canBeMaximizedLeftRight) { previewRectangle.maximizeRight(amount); } } onPushedTopBoundary: { if (buttons === Qt.LeftButton && previewRectangle && previewRectangle.target.canBeMaximized) { previewRectangle.maximize(amount); } } onPushedTopLeftCorner: { if (buttons === Qt.LeftButton && previewRectangle && previewRectangle.target.canBeCornerMaximized) { previewRectangle.maximizeTopLeft(amount); } } onPushedTopRightCorner: { if (buttons === Qt.LeftButton && previewRectangle && previewRectangle.target.canBeCornerMaximized) { previewRectangle.maximizeTopRight(amount); } } onPushedBottomLeftCorner: { if (buttons === Qt.LeftButton && previewRectangle && previewRectangle.target.canBeCornerMaximized) { previewRectangle.maximizeBottomLeft(amount); } } onPushedBottomRightCorner: { if (buttons === Qt.LeftButton && previewRectangle && previewRectangle.target.canBeCornerMaximized) { previewRectangle.maximizeBottomRight(amount); } } onPushStopped: { if (previewRectangle) { previewRectangle.stop(); } } onMouseMoved: { mouseNeverMoved = false; cursor.opacity = 1; } Behavior on opacity { UbuntuNumberAnimation {} } } // non-visual objects KeymapSwitcher { focusedSurface: topLevelSurfaceList.focusedWindow ? topLevelSurfaceList.focusedWindow.surface : null } BrightnessControl {} Rectangle { id: shutdownFadeOutRectangle z: cursor.z + 1 enabled: false visible: false color: "black" anchors.fill: parent opacity: 0.0 NumberAnimation on opacity { id: shutdownFadeOut from: 0.0 to: 1.0 onStopped: { if (shutdownFadeOutRectangle.enabled && shutdownFadeOutRectangle.visible) { DBusUnitySessionService.shutdown(); } } } } }