on xenial help sign in
Stages now control the "requestedState" property of applications

We now decide whether a application is suspended or not via the new Application.requestedState property. Previously that was controlled by qtmir and tied to Application.focus. We don't want that anymore since it does not apply to a windowed usage mode scenario, for instance. Approved by: Gerry Boland

commita9a969a817c831001f4ca39ab99a1035253d8682 authorDaniel d'Andrada <daniel.dandrada@canonical.com> dateTue 4 Aug 2015, 20:55 (11 years ago) parenta6c2aeeb8f72d1
22 files changed+468−121
CMakeLists.txt+1−1 debian/control+1−1 plugins/Greeter/Unity/Launcher/CMakeLists.txt−1 plugins/Unity/Launcher/CMakeLists.txt−1 qml/Components/Unity8Settings.qml+50 qml/OrientedShell.qml+3−3 qml/Shell.qml+10−12 qml/Stages/DesktopStage.qml+13 qml/Stages/PhoneStage.qml+10 qml/Stages/TabletStage.qml+14 tests/mocks/Unity/Application/ApplicationInfo.cpp+40−6 tests/mocks/Unity/Application/ApplicationInfo.h+7 tests/mocks/Unity/Application/ApplicationManager.cpp+2−49 tests/mocks/Unity/Application/ApplicationManager.h−7 tests/plugins/Greeter/Unity/Launcher/CMakeLists.txt−1 tests/plugins/Unity/Launcher/launchermodeltest.cpp+3−4 tests/qmltests/Stages/ApplicationCheckBox.qml+21 tests/qmltests/Stages/tst_DesktopStage.qml+2−2 tests/qmltests/Stages/tst_PhoneStage.qml+75−3 tests/qmltests/Stages/tst_TabletStage.qml+161−1 tests/qmltests/tst_OrientedShell.qml+3−3 tests/qmltests/tst_Shell.qml+52−26
CMakeLists.txt+1−1
@@ -56,7 +56,7 @@ find_package(Qt5DBus 5.2 REQUIRED)
5656find_package(Qt5Concurrent 5.2 REQUIRED)
5757find_package(Qt5Sql 5.2 REQUIRED)
5858
59−pkg_check_modules(APPLICATION_API REQUIRED unity-shell-application=6)
59+pkg_check_modules(APPLICATION_API REQUIRED unity-shell-application=7)
6060
6161# Standard install paths
6262include(GNUInstallDirs)
debian/control+1−1
@@ -28,7 +28,7 @@ Build-Depends: cmake,
2828 libqt5xmlpatterns5-dev,
2929 libsystemsettings-dev,
3030 libudev-dev,
31− libunity-api-dev (>= 7.98),
31+ libunity-api-dev (>= 7.99),
3232 libusermetricsoutput1-dev,
3333 libxcb1-dev,
3434 pkg-config,
plugins/Greeter/Unity/Launcher/CMakeLists.txt−1
@@ -1,5 +1,4 @@
11pkg_check_modules(LAUNCHER_API REQUIRED unity-shell-launcher=7)
2−pkg_check_modules(APPLICATION_API REQUIRED unity-shell-application=6)
32pkg_check_modules(GSETTINGS_QT REQUIRED gsettings-qt)
43
54add_definitions(-DSM_BUSNAME=systemBus)
plugins/Unity/Launcher/CMakeLists.txt−1
@@ -1,5 +1,4 @@
11pkg_check_modules(LAUNCHER_API REQUIRED unity-shell-launcher=7)
2−pkg_check_modules(APPLICATION_API REQUIRED unity-shell-application=6)
32pkg_check_modules(GSETTINGS_QT REQUIRED gsettings-qt)
43
54add_definitions(-DSM_BUSNAME=systemBus)
qml/Components/Unity8Settings.qmlnew file+50
@@ -0,0 +1,50 @@
1+/*
2+ * Copyright (C) 2015 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 GSettings 1.0
19+
20+QtObject {
21+ id: root
22+
23+ property string usageMode: "Staged"
24+
25+ // FIXME: Works around a bug where if we change a Loader's source in response to a GSettings
26+ // property change in the same event loop, the Loader's previously loaded component does not
27+ // get destroyed and its bindings continue to operate!
28+ //
29+ // Shouldn't be needed after
30+ // https://code.launchpad.net/~lukas-kde/gsettings-qt/queued-processing/+merge/259883 gets
31+ // merged.
32+ property var timer: Timer {
33+ interval: 1
34+ onTriggered: { root.usageMode = root.wrapped.usageMode; }
35+ }
36+ property var wrappedConnections: Connections {
37+ target: root.wrapped
38+ ignoreUnknownSignals: true // don't spam us
39+ onUsageModeChanged: { root.timer.start(); }
40+ }
41+ property var wrapped: GSettings {
42+ schema.id: "com.canonical.Unity8"
43+ Component.onCompleted: {
44+ // init the value. it's a dynamic prop, so we have to check first
45+ if (root.usageMode) {
46+ root.usageMode = root.wrapped.usageMode;
47+ }
48+ }
49+ }
50+}
qml/OrientedShell.qml+3−3
@@ -43,7 +43,7 @@ Rectangle {
4343
4444
4545 // to be overwritten by tests
46− property var usageModeSettings: GSettings { schema.id: "com.canonical.Unity8" }
46+ property var unity8Settings: Unity8Settings {}
4747 property var oskSettings: GSettings { schema.id: "com.canonical.keyboard.maliit" }
4848
4949 property int physicalOrientation: Screen.orientation
@@ -160,9 +160,9 @@ Rectangle {
160160 // TODO: Factor in the connected input devices (eg: physical keyboard, mouse, touchscreen),
161161 // what's the output device (eg: big TV, desktop monitor, phone display), etc.
162162 usageScenario: {
163− if (root.usageModeSettings.usageMode === "Windowed") {
163+ if (root.unity8Settings.usageMode === "Windowed") {
164164 return "desktop";
165− } else if (root.usageModeSettings.usageMode === "Staged") {
165+ } else if (root.unity8Settings.usageMode === "Staged") {
166166 if (deviceConfiguration.category === "phone") {
167167 return "phone";
168168 } else {
qml/Shell.qml+10−12
@@ -191,12 +191,6 @@ Item {
191191 z: dialogs.z + 10
192192 }
193193
194− Binding {
195− target: ApplicationManager
196− property: "forceDashActive"
197− value: launcher.shown || launcher.dashSwipe
198− }
199−
200194 WindowKeysFilter {
201195 Keys.onPressed: physicalKeysMapper.onKeyPressed(event);
202196 Keys.onReleased: physicalKeysMapper.onKeyReleased(event);
@@ -352,6 +346,16 @@ Item {
352346 property: "beingResized"
353347 value: shell.beingResized
354348 }
349+ Binding {
350+ target: applicationsDisplayLoader.item
351+ property: "keepDashRunning"
352+ value: launcher.shown || launcher.dashSwipe
353+ }
354+ Binding {
355+ target: applicationsDisplayLoader.item
356+ property: "suspended"
357+ value: greeter.shown
358+ }
355359 Binding {
356360 target: applicationsDisplayLoader.item
357361 property: "altTabPressed"
@@ -459,12 +463,6 @@ Item {
459463 }
460464
461465 onEmergencyCall: startLockedApp("dialer-app")
462−
463− Binding {
464− target: ApplicationManager
465− property: "suspended"
466− value: greeter.shown
467− }
468466 }
469467 }
470468
qml/Stages/DesktopStage.qml+13
@@ -39,6 +39,8 @@ Rectangle {
3939 property int shellPrimaryOrientation
4040 property int nativeOrientation
4141 property bool beingResized: false
42+ property bool keepDashRunning: true
43+ property bool suspended: false
4244
4345 // functions to be called from outside
4446 function updateFocusedAppOrientation() { /* TODO */ }
@@ -215,6 +217,17 @@ Rectangle {
215217 }
216218 }
217219
220+ Binding {
221+ target: ApplicationManager.get(index)
222+ property: "requestedState"
223+ // TODO: figure out some lifecycle policy, like suspending minimized apps
224+ // if running on a tablet or something.
225+ // TODO: If the device has a dozen suspended apps because it was running
226+ // in staged mode, when it switches to Windowed mode it will suddenly
227+ // resume all those apps at once. We might want to avoid that.
228+ value: ApplicationInfoInterface.RequestedRunning // Always running for now
229+ }
230+
218231 function maximize() {
219232 minimized = false;
220233 maximized = true;
qml/Stages/PhoneStage.qml+10
@@ -36,6 +36,8 @@ Rectangle {
3636 property bool altTabEnabled: true
3737 property real startScale: 1.1
3838 property real endScale: 0.7
39+ property bool keepDashRunning: true
40+ property bool suspended: false
3941 property int shellOrientationAngle: 0
4042 property int shellOrientation
4143 property int shellPrimaryOrientation
@@ -428,6 +430,14 @@ Rectangle {
428430 dropShadow: spreadView.active || priv.focusedAppDelegateIsDislocated
429431 focusFirstApp: root.focusFirstApp
430432
433+ Binding {
434+ target: appDelegate.application
435+ property: "requestedState"
436+ value: (isDash && root.keepDashRunning) || (!root.suspended && appDelegate.focus)
437+ ? ApplicationInfoInterface.RequestedRunning
438+ : ApplicationInfoInterface.RequestedSuspended
439+ }
440+
431441 readonly property bool isDash: model.appId == "unity8-dash"
432442
433443 z: isDash && !spreadView.active ? -1 : behavioredIndex
qml/Stages/TabletStage.qml+14
@@ -36,6 +36,8 @@ Rectangle {
3636 property bool spreadEnabled: true // If false, animations and right edge will be disabled
3737
3838 property real inverseProgress: 0 // This is the progress for left edge drags, in pixels.
39+ property bool keepDashRunning: true
40+ property bool suspended: false
3941 property int shellOrientationAngle: 0
4042 property int shellOrientation
4143 property int shellPrimaryOrientation
@@ -117,6 +119,7 @@ Rectangle {
117119
118120 QtObject {
119121 id: priv
122+ objectName: "stagesPriv"
120123
121124 property string focusedAppId: ApplicationManager.focusedApplicationId
122125 readonly property var focusedAppDelegate: {
@@ -626,6 +629,17 @@ Rectangle {
626629
627630 readonly property bool isDash: model.appId == "unity8-dash"
628631
632+ Binding {
633+ target: spreadTile.application
634+ property: "requestedState"
635+ value: (spreadTile.isDash && root.keepDashRunning)
636+ ||
637+ (!root.suspended && (model.appId == priv.mainStageAppId
638+ || model.appId == priv.sideStageAppId))
639+ ? ApplicationInfoInterface.RequestedRunning
640+ : ApplicationInfoInterface.RequestedSuspended
641+ }
642+
629643 // FIXME: A regular binding doesn't update any more after closing an app.
630644 // Using a Binding for now.
631645 Binding {
tests/mocks/Unity/Application/ApplicationInfo.cpp+40−6
@@ -30,7 +30,7 @@ ApplicationInfo::ApplicationInfo(const QString &appId, QObject *parent)
3030 : ApplicationInfoInterface(appId, parent)
3131 , m_appId(appId)
3232 , m_stage(MainStage)
33− , m_state(Starting)
33+ , m_state(Stopped)
3434 , m_focused(false)
3535 , m_fullscreen(false)
3636 , m_session(0)
@@ -39,6 +39,7 @@ ApplicationInfo::ApplicationInfo(const QString &appId, QObject *parent)
3939 Qt::InvertedPortraitOrientation |
4040 Qt::InvertedLandscapeOrientation)
4141 , m_rotatesWindowContents(false)
42+ , m_requestedState(RequestedRunning)
4243 , m_manualSurfaceCreation(false)
4344{
4445}
@@ -46,7 +47,7 @@ ApplicationInfo::ApplicationInfo(const QString &appId, QObject *parent)
4647ApplicationInfo::ApplicationInfo(QObject *parent)
4748 : ApplicationInfoInterface(QString(), parent)
4849 , m_stage(MainStage)
49− , m_state(Starting)
50+ , m_state(Stopped)
5051 , m_focused(false)
5152 , m_fullscreen(false)
5253 , m_session(0)
@@ -55,6 +56,7 @@ ApplicationInfo::ApplicationInfo(QObject *parent)
5556 Qt::InvertedPortraitOrientation |
5657 Qt::InvertedLandscapeOrientation)
5758 , m_rotatesWindowContents(false)
59+ , m_requestedState(RequestedRunning)
5860 , m_manualSurfaceCreation(false)
5961{
6062}
@@ -74,7 +76,6 @@ void ApplicationInfo::createSession()
7476
7577void ApplicationInfo::setSession(Session* session)
7678{
77− qDebug() << "Application::setSession - appId=" << appId() << "session=" << session;
7879 if (m_session == session)
7980 return;
8081
@@ -91,6 +92,8 @@ void ApplicationInfo::setSession(Session* session)
9192 m_session->setApplication(this);
9293 m_session->setParent(this);
9394 SessionManager::singleton()->registerSession(m_session);
95+ connect(m_session, &Session::surfaceChanged,
96+ this, &ApplicationInfo::onSessionSurfaceChanged);
9497
9598 if (!m_manualSurfaceCreation) {
9699 QTimer::singleShot(500, m_session, SLOT(createSurface()));
@@ -161,11 +164,12 @@ void ApplicationInfo::setState(State value)
161164 m_state = value;
162165 Q_EMIT stateChanged(value);
163166
164− if (!m_manualSurfaceCreation && m_state == ApplicationInfo::Running) {
167+ if (!m_manualSurfaceCreation && !m_session && m_state == ApplicationInfo::Starting) {
165168 QTimer::singleShot(500, this, SLOT(createSession()));
166169 } else if (m_state == ApplicationInfo::Stopped) {
167− delete m_session;
168− m_session = nullptr;
170+ Session *session = m_session;
171+ setSession(nullptr);
172+ delete session;
169173 }
170174 }
171175}
@@ -213,3 +217,33 @@ void ApplicationInfo::setRotatesWindowContents(bool value)
213217{
214218 m_rotatesWindowContents = value;
215219}
220+
221+ApplicationInfo::RequestedState ApplicationInfo::requestedState() const
222+{
223+ return m_requestedState;
224+}
225+
226+void ApplicationInfo::setRequestedState(RequestedState value)
227+{
228+ if (m_requestedState != value) {
229+ m_requestedState = value;
230+ Q_EMIT requestedStateChanged(m_requestedState);
231+
232+ if (m_requestedState == RequestedRunning && m_state == Suspended) {
233+ setState(Running);
234+ } else if (m_requestedState == RequestedSuspended && m_state == Running) {
235+ setState(Suspended);
236+ }
237+ }
238+}
239+
240+void ApplicationInfo::onSessionSurfaceChanged(MirSurfaceItem* surface)
241+{
242+ if (surface != nullptr && m_state == Starting) {
243+ if (m_requestedState == RequestedRunning) {
244+ setState(Running);
245+ } else {
246+ setState(Suspended);
247+ }
248+ }
249+}
tests/mocks/Unity/Application/ApplicationInfo.h+7
@@ -46,6 +46,9 @@ public:
4646 ApplicationInfo(const QString &appId, QObject *parent = nullptr);
4747 ~ApplicationInfo();
4848
49+ RequestedState requestedState() const override;
50+ void setRequestedState(RequestedState) override;
51+
4952 void setIconId(const QString &iconId);
5053 void setScreenshotId(const QString &screenshotId);
5154
@@ -101,6 +104,9 @@ Q_SIGNALS:
101104public Q_SLOTS:
102105 Q_INVOKABLE void createSession();
103106
107+private Q_SLOTS:
108+ void onSessionSurfaceChanged(MirSurfaceItem*);
109+
104110private:
105111 void setIcon(const QUrl &value);
106112
@@ -116,6 +122,7 @@ private:
116122 Session* m_session;
117123 Qt::ScreenOrientations m_supportedOrientations;
118124 bool m_rotatesWindowContents;
125+ RequestedState m_requestedState;
119126
120127 bool m_manualSurfaceCreation;
121128};
tests/mocks/Unity/Application/ApplicationManager.cpp+2−49
@@ -49,8 +49,6 @@ ApplicationManager *ApplicationManager::singleton()
4949
5050ApplicationManager::ApplicationManager(QObject *parent)
5151 : ApplicationManagerInterface(parent)
52− , m_suspended(false)
53− , m_forceDashActive(false)
5452{
5553 m_roleNames.insert(RoleSession, "session");
5654 m_roleNames.insert(RoleFullscreen, "fullscreen");
@@ -217,7 +215,7 @@ ApplicationInfo* ApplicationManager::startApplication(const QString &appId,
217215 && application->stage() == ApplicationInfo::SideStage) {
218216 application->setStage(ApplicationInfo::MainStage);
219217 }
220− application->setState(ApplicationInfo::Running);
218+ application->setState(ApplicationInfo::Starting);
221219
222220 return application;
223221}
@@ -262,50 +260,6 @@ QString ApplicationManager::focusedApplicationId() const {
262260 return QString();
263261}
264262
265−bool ApplicationManager::suspended() const
266−{
267− return m_suspended;
268−}
269−
270−void ApplicationManager::setSuspended(bool suspended)
271−{
272− ApplicationInfo *focusedApp = findApplication(focusedApplicationId());
273− if (focusedApp) {
274− if (suspended) {
275− focusedApp->setState(ApplicationInfo::Suspended);
276− } else {
277− focusedApp->setState(ApplicationInfo::Running);
278− }
279− }
280− m_suspended = suspended;
281− Q_EMIT suspendedChanged();
282−}
283−
284−bool ApplicationManager::forceDashActive() const
285−{
286− return m_forceDashActive;
287−}
288−
289−void ApplicationManager::setForceDashActive(bool forceDashActive)
290−{
291− if (m_forceDashActive == forceDashActive) {
292− return;
293− }
294−
295− ApplicationInfo *dash = findApplication("unity8-dash");
296− if (dash) {
297− if (forceDashActive) {
298− dash->setState(ApplicationInfo::Running);
299− } else {
300− if (!dash->focused()) {
301− dash->setState(ApplicationInfo::Suspended);
302− }
303− }
304− }
305− m_forceDashActive = forceDashActive;
306− Q_EMIT forceDashActiveChanged();
307−}
308−
309263bool ApplicationManager::focusApplication(const QString &appId)
310264{
311265 ApplicationInfo *application = findApplication(appId);
@@ -316,13 +270,11 @@ bool ApplicationManager::focusApplication(const QString &appId)
316270 for (ApplicationInfo *app : m_runningApplications) {
317271 if (app->focused()) {
318272 app->setFocused(false);
319− app->setState(ApplicationInfo::Suspended);
320273 }
321274 }
322275
323276 // focus this app
324277 application->setFocused(true);
325− application->setState(ApplicationInfo::Running);
326278
327279 // move app to top of stack
328280 move(m_runningApplications.indexOf(application), 0);
@@ -394,6 +346,7 @@ void ApplicationManager::buildListOfAvailableApplications()
394346 application->setScreenshotId("gallery");
395347 application->setIconId("gallery");
396348 application->setFullscreen(true);
349+ application->setStage(ApplicationInfo::MainStage);
397350 m_availableApplications.append(application);
398351
399352 application = new ApplicationInfo(this);
tests/mocks/Unity/Application/ApplicationManager.h−7
@@ -69,11 +69,6 @@ class ApplicationManager : public ApplicationManagerInterface {
6969 Q_INVOKABLE bool stopApplication(const QString &appId) override;
7070
7171 QString focusedApplicationId() const override;
72− bool suspended() const override;
73− void setSuspended(bool suspended) override;
74−
75− bool forceDashActive() const override;
76− void setForceDashActive(bool forceDashActive) override;
7772
7873 // Only for testing
7974 QStringList availableApplications();
@@ -96,8 +91,6 @@ class ApplicationManager : public ApplicationManagerInterface {
9691 void remove(ApplicationInfo* application);
9792 void buildListOfAvailableApplications();
9893 void onWindowCreated();
99− bool m_suspended;
100− bool m_forceDashActive;
10194 QList<ApplicationInfo*> m_runningApplications;
10295 QList<ApplicationInfo*> m_availableApplications;
10396 QTimer m_windowCreatedTimer;
tests/plugins/Greeter/Unity/Launcher/CMakeLists.txt−1
@@ -1,5 +1,4 @@
11pkg_check_modules(LAUNCHER_API REQUIRED unity-shell-launcher=7)
2−pkg_check_modules(APPLICATION_API REQUIRED unity-shell-application=6)
32
43include_directories(
54 ${CMAKE_CURRENT_SOURCE_DIR}
tests/plugins/Unity/Launcher/launchermodeltest.cpp+3−4
@@ -39,6 +39,9 @@ class MockApp: public unity::shell::application::ApplicationInfoInterface
3939 Q_OBJECT
4040public:
4141 MockApp(const QString &appId, QObject *parent = 0): ApplicationInfoInterface(appId, parent), m_appId(appId), m_focused(false) { }
42+
43+ RequestedState requestedState() const override { return RequestedRunning; }
44+ void setRequestedState(RequestedState) override {}
4245 QString appId() const override { return m_appId; }
4346 QString name() const override { return "mock"; }
4447 QString comment() const override { return "this is a mock"; }
@@ -117,10 +120,6 @@ public:
117120 endRemoveRows();
118121 }
119122 bool requestFocusApplication(const QString &appId) override { Q_UNUSED(appId); return true; }
120− bool suspended() const override { return false; }
121− void setSuspended(bool) override {}
122− bool forceDashActive() const override { return false; }
123− void setForceDashActive(bool) override {}
124123
125124private:
126125 QList<MockApp*> m_list;
tests/qmltests/Stages/ApplicationCheckBox.qml+21
@@ -86,8 +86,29 @@ RowLayout {
8686 }
8787 }
8888 Label {
89+ id: appIdLabel
8990 text: root.appId
9091 color: "white"
9192 anchors.verticalCenter: parent.verticalCenter
9293 }
94+ Rectangle {
95+ color: {
96+ if (d.application) {
97+ if (d.application.state === ApplicationInfoInterface.Starting) {
98+ return "yellow";
99+ } else if (d.application.state === ApplicationInfoInterface.Running) {
100+ return "green";
101+ } else if (d.application.state === ApplicationInfoInterface.Suspended) {
102+ return "blue";
103+ } else {
104+ return "darkred";
105+ }
106+ } else {
107+ return "darkred";
108+ }
109+ }
110+ width: height
111+ height: appIdLabel.height * 0.7
112+ anchors.verticalCenter: parent.verticalCenter
113+ }
93114}
tests/qmltests/Stages/tst_DesktopStage.qml+2−2
@@ -24,9 +24,8 @@ import Utils 0.1
2424
2525import "../../../qml/Stages"
2626
27−Rectangle {
27+Item {
2828 id: root
29− color: "darkblue"
3029 width: desktopStageLoader.width + controls.width
3130 height: desktopStageLoader.height
3231
@@ -57,6 +56,7 @@ Rectangle {
5756 property bool itemDestroyed: false
5857 sourceComponent: Component {
5958 DesktopStage {
59+ color: "darkblue"
6060 anchors.fill: parent
6161 Component.onDestruction: {
6262 desktopStageLoader.itemDestroyed = true;
tests/qmltests/Stages/tst_PhoneStage.qml+75−3
@@ -301,16 +301,16 @@ Item {
301301 addApps(2);
302302
303303 var secondApp = ApplicationManager.get(0);
304− tryCompare(secondApp, "state", ApplicationInfoInterface.Running);
304+ tryCompare(secondApp, "requestedState", ApplicationInfoInterface.RequestedRunning);
305305 tryCompare(secondApp, "focused", true);
306306
307307 var firstApp = ApplicationManager.get(1);
308− tryCompare(firstApp, "state", ApplicationInfoInterface.Suspended);
308+ tryCompare(firstApp, "requestedState", ApplicationInfoInterface.RequestedSuspended);
309309 tryCompare(firstApp, "focused", false);
310310
311311 ApplicationManager.stopApplication(secondApp.appId);
312312
313− tryCompare(firstApp, "state", ApplicationInfoInterface.Running);
313+ tryCompare(firstApp, "requestedState", ApplicationInfoInterface.RequestedRunning);
314314 tryCompare(firstApp, "focused", true);
315315 }
316316
@@ -388,5 +388,77 @@ Item {
388388
389389 tryCompare(focusedDelegate, "x", 0);
390390 }
391+
392+ function test_focusedAppIsTheOnlyRunningApp() {
393+ addApps(2);
394+
395+ var delegateA = findChild(phoneStage, "appDelegate0");
396+ verify(delegateA);
397+ var delegateB = findChild(phoneStage, "appDelegate1");
398+ verify(delegateB);
399+
400+ // A is focused and running, B is unfocused and suspended
401+ compare(delegateA.focus, true);
402+ compare(delegateA.application.requestedState, ApplicationInfoInterface.RequestedRunning);
403+ compare(delegateB.focus, false);
404+ compare(delegateB.application.requestedState, ApplicationInfoInterface.RequestedSuspended);
405+
406+ // Switch foreground/focused appp from A to B
407+ goToSpread();
408+ phoneStage.select(delegateB.application.appId);
409+
410+ // Now it's the other way round
411+ // A is unfocused and suspended, B is focused and running
412+ tryCompare(delegateA, "focus", false);
413+ tryCompare(delegateA.application, "requestedState", ApplicationInfoInterface.RequestedSuspended);
414+ tryCompare(delegateB, "focus", true);
415+ tryCompare(delegateB.application, "requestedState", ApplicationInfoInterface.RequestedRunning);
416+ }
417+
418+ function test_dashRemainsRunningIfStageIsToldSo() {
419+ addApps(1);
420+
421+ var delegateDash = findChild(phoneStage, "appDelegate1");
422+ verify(delegateDash);
423+ compare(delegateDash.application.appId, "unity8-dash");
424+
425+ var delegateOther = findChild(phoneStage, "appDelegate0");
426+ verify(delegateOther);
427+
428+ goToSpread();
429+ phoneStage.select("unity8-dash");
430+
431+ tryCompare(delegateDash, "focus", true);
432+ tryCompare(delegateDash.application, "requestedState", ApplicationInfoInterface.RequestedRunning);
433+ compare(delegateOther.focus, false);
434+ compare(delegateOther.application.requestedState, ApplicationInfoInterface.RequestedSuspended);
435+
436+ goToSpread();
437+ phoneStage.select(delegateOther.application.appId);
438+
439+ // The other app gets focused and running but dash is kept running despite being unfocused
440+ tryCompare(delegateDash, "focus", false);
441+ tryCompare(delegateDash.application, "requestedState", ApplicationInfoInterface.RequestedRunning);
442+ compare(delegateOther.focus, true);
443+ compare(delegateOther.application.requestedState, ApplicationInfoInterface.RequestedRunning);
444+ }
445+
446+ function test_foregroundAppIsSuspendedWhenStageIsSuspended() {
447+ addApps(1);
448+
449+ var delegate = findChild(phoneStage, "appDelegate0");
450+ verify(delegate);
451+
452+ compare(delegate.focus, true);
453+ compare(delegate.application.requestedState, ApplicationInfoInterface.RequestedRunning);
454+
455+ phoneStage.suspended = true;
456+
457+ tryCompare(delegate.application, "requestedState", ApplicationInfoInterface.RequestedSuspended);
458+
459+ phoneStage.suspended = false;
460+
461+ tryCompare(delegate.application, "requestedState", ApplicationInfoInterface.RequestedRunning);
462+ }
391463 }
392464}
tests/qmltests/Stages/tst_TabletStage.qml+161−1
@@ -76,10 +76,18 @@ Rectangle {
7676 id: webbrowserCheckBox
7777 appId: "webbrowser-app"
7878 }
79+ ApplicationCheckBox {
80+ id: galleryCheckBox
81+ appId: "gallery-app"
82+ }
7983 ApplicationCheckBox {
8084 id: dialerCheckBox
8185 appId: "dialer-app"
8286 }
87+ ApplicationCheckBox {
88+ id: facebookCheckBox
89+ appId: "facebook-webapp"
90+ }
8391 }
8492 }
8593
@@ -93,6 +101,19 @@ Rectangle {
93101 function init() {
94102 tabletStageLoader.active = true;
95103 tryCompare(tabletStageLoader, "status", Loader.Ready);
104+
105+ // this is very strange, but sometimes the test starts without
106+ // TabletStage components having finished loading themselves
107+ var appWindow = null;
108+ while (!appWindow) {
109+ appWindow = findChild(tabletStage, "appWindow_unity8-dash");
110+ if (!appWindow) {
111+ console.log("didn't find appWindow_unity8-dash in " + tabletStage + ". Trying again...");
112+ wait(50);
113+ }
114+ }
115+
116+ waitUntilAppSurfaceShowsUp("unity8-dash");
96117 }
97118
98119 function cleanup() {
@@ -109,7 +130,9 @@ Rectangle {
109130
110131 // kill all (fake) running apps
111132 webbrowserCheckBox.checked = false;
133+ galleryCheckBox.checked = false;
112134 dialerCheckBox.checked = false;
135+ facebookCheckBox.checked = false;
113136 }
114137
115138 function waitUntilAppSurfaceShowsUp(appId) {
@@ -120,6 +143,34 @@ Rectangle {
120143 tryCompare(appWindowStates, "state", "surface");
121144 }
122145
146+ function switchToApp(targetAppId) {
147+ touchFlick(tabletStage,
148+ tabletStage.width - (tabletStage.dragAreaWidth / 2), tabletStage.height / 2,
149+ tabletStage.x + 1, tabletStage.height / 2);
150+
151+ var spreadView = findChild(tabletStage, "spreadView");
152+ verify(spreadView);
153+ tryCompare(spreadView, "phase", 2);
154+ tryCompare(spreadView, "flicking", false);
155+ tryCompare(spreadView, "moving", false);
156+
157+ waitUntilAppDelegateStopsMoving(targetAppId);
158+
159+ var targetAppWindow = findChild(tabletStage, "appWindow_" + targetAppId);
160+ tap(targetAppWindow, 10, 10);
161+ }
162+
163+ function waitUntilAppDelegateStopsMoving(targetAppId)
164+ {
165+ var targetAppDelegate = findChild(tabletStage, "tabletSpreadDelegate_" + targetAppId);
166+ verify(targetAppDelegate);
167+ var lastValue = undefined;
168+ do {
169+ lastValue = targetAppDelegate.animatedProgress;
170+ wait(30);
171+ } while (lastValue != targetAppDelegate.animatedProgress);
172+ }
173+
123174 function test_tappingSwitchesFocusBetweenStages() {
124175 webbrowserCheckBox.checked = true;
125176 waitUntilAppSurfaceShowsUp(webbrowserCheckBox.appId);
@@ -155,7 +206,6 @@ Rectangle {
155206 tryCompare(webbrowserApp.session.surface, "activeFocus", false);
156207 }
157208
158−
159209 function test_closeAppInSideStage() {
160210 dialerCheckBox.checked = true;
161211 waitUntilAppSurfaceShowsUp(dialerCheckBox.appId);
@@ -200,6 +250,116 @@ Rectangle {
200250 appWindow.width / 2, appWindow.height / 2,
201251 appWindow.width / 2, -appWindow.height / 2);
202252 }
253+
254+ function test_suspendsAndResumesAppsInMainStage() {
255+ webbrowserCheckBox.checked = true;
256+ var webbrowserApp = ApplicationManager.findApplication(webbrowserCheckBox.appId);
257+ compare(webbrowserApp.stage, ApplicationInfoInterface.MainStage);
258+
259+ tryCompare(webbrowserApp, "state", ApplicationInfoInterface.Running);
260+
261+ galleryCheckBox.checked = true;
262+ var galleryApp = ApplicationManager.findApplication(galleryCheckBox.appId);
263+ compare(galleryApp.stage, ApplicationInfoInterface.MainStage);
264+
265+ tryCompare(galleryApp, "state", ApplicationInfoInterface.Running);
266+ tryCompare(webbrowserApp, "state", ApplicationInfoInterface.Suspended);
267+
268+ switchToApp(webbrowserApp.appId);
269+
270+ tryCompare(galleryApp, "state", ApplicationInfoInterface.Suspended);
271+ tryCompare(webbrowserApp, "state", ApplicationInfoInterface.Running);
272+
273+ switchToApp(galleryApp.appId);
274+
275+ tryCompare(galleryApp, "state", ApplicationInfoInterface.Running);
276+ tryCompare(webbrowserApp, "state", ApplicationInfoInterface.Suspended);
277+ }
278+
279+
280+ function test_foregroundMainAndSideStageAppsAreKeptRunning() {
281+
282+ var stagesPriv = findInvisibleChild(tabletStage, "stagesPriv");
283+ verify(stagesPriv);
284+
285+ // launch two main stage apps
286+ // gallery will be on foreground and webbrowser on background
287+
288+ webbrowserCheckBox.checked = true;
289+ waitUntilAppSurfaceShowsUp(webbrowserCheckBox.appId)
290+ var webbrowserApp = ApplicationManager.findApplication(webbrowserCheckBox.appId);
291+ compare(webbrowserApp.stage, ApplicationInfoInterface.MainStage);
292+
293+ galleryCheckBox.checked = true;
294+ waitUntilAppSurfaceShowsUp(galleryCheckBox.appId)
295+ var galleryApp = ApplicationManager.findApplication(galleryCheckBox.appId);
296+ compare(galleryApp.stage, ApplicationInfoInterface.MainStage);
297+
298+ compare(stagesPriv.mainStageAppId, galleryCheckBox.appId);
299+
300+ // then launch two side stage apps
301+ // facebook will be on foreground and dialer on background
302+
303+ dialerCheckBox.checked = true;
304+ waitUntilAppSurfaceShowsUp(dialerCheckBox.appId)
305+ var dialerApp = ApplicationManager.findApplication(dialerCheckBox.appId);
306+ compare(dialerApp.stage, ApplicationInfoInterface.SideStage);
307+
308+ facebookCheckBox.checked = true;
309+ waitUntilAppSurfaceShowsUp(facebookCheckBox.appId)
310+ var facebookApp = ApplicationManager.findApplication(facebookCheckBox.appId);
311+ compare(facebookApp.stage, ApplicationInfoInterface.SideStage);
312+
313+ compare(stagesPriv.sideStageAppId, facebookCheckBox.appId);
314+
315+ // Now check that the foreground apps are running and that the background ones
316+ // are suspended
317+
318+ tryCompare(galleryApp, "state", ApplicationInfoInterface.Running);
319+ tryCompare(webbrowserApp, "state", ApplicationInfoInterface.Suspended);
320+ tryCompare(facebookApp, "state", ApplicationInfoInterface.Running);
321+ tryCompare(dialerApp, "state", ApplicationInfoInterface.Suspended);
322+
323+ switchToApp(dialerCheckBox.appId);
324+
325+ tryCompare(galleryApp, "state", ApplicationInfoInterface.Running);
326+ tryCompare(webbrowserApp, "state", ApplicationInfoInterface.Suspended);
327+ tryCompare(facebookApp, "state", ApplicationInfoInterface.Suspended);
328+ tryCompare(dialerApp, "state", ApplicationInfoInterface.Running);
329+
330+ switchToApp(webbrowserCheckBox.appId);
331+
332+ tryCompare(galleryApp, "state", ApplicationInfoInterface.Suspended);
333+ tryCompare(webbrowserApp, "state", ApplicationInfoInterface.Running);
334+ tryCompare(facebookApp, "state", ApplicationInfoInterface.Suspended);
335+ tryCompare(dialerApp, "state", ApplicationInfoInterface.Running);
336+ }
337+
338+ function test_foregroundAppsAreSuspendedWhenStageIsSuspended() {
339+ webbrowserCheckBox.checked = true;
340+ waitUntilAppSurfaceShowsUp(webbrowserCheckBox.appId)
341+ var webbrowserApp = ApplicationManager.findApplication(webbrowserCheckBox.appId);
342+ compare(webbrowserApp.stage, ApplicationInfoInterface.MainStage);
343+
344+ dialerCheckBox.checked = true;
345+ waitUntilAppSurfaceShowsUp(dialerCheckBox.appId)
346+ var dialerApp = ApplicationManager.findApplication(dialerCheckBox.appId);
347+ compare(dialerApp.stage, ApplicationInfoInterface.SideStage);
348+
349+
350+ compare(webbrowserApp.requestedState, ApplicationInfoInterface.RequestedRunning);
351+ compare(dialerApp.requestedState, ApplicationInfoInterface.RequestedRunning);
352+
353+ tabletStage.suspended = true;
354+
355+ tryCompare(webbrowserApp, "requestedState", ApplicationInfoInterface.RequestedSuspended);
356+ tryCompare(dialerApp, "requestedState", ApplicationInfoInterface.RequestedSuspended);
357+
358+ tabletStage.suspended = false;
359+
360+ tryCompare(webbrowserApp, "requestedState", ApplicationInfoInterface.RequestedRunning);
361+ tryCompare(dialerApp, "requestedState", ApplicationInfoInterface.RequestedRunning);
362+ }
203363 }
204364
205365}
tests/qmltests/tst_OrientedShell.qml+3−3
@@ -46,7 +46,7 @@ Rectangle {
4646 }
4747
4848 QtObject {
49− id: mockUsageModeSettings
49+ id: mockUnity8Settings
5050 property string usageMode: usageModeSelector.model[usageModeSelector.selectedIndex]
5151 }
5252
@@ -125,7 +125,7 @@ Rectangle {
125125 sourceComponent: Component {
126126 OrientedShell {
127127 anchors.fill: parent
128− usageModeSettings: mockUsageModeSettings
128+ unity8Settings: mockUnity8Settings
129129 oskSettings: mockOskSettings
130130 physicalOrientation: root.physicalOrientation0
131131 orientationLocked: orientationLockedCheckBox.checked
@@ -926,7 +926,7 @@ Rectangle {
926926
927927 function test_attachRemoveInputDevices() {
928928 usageModeSelector.selectedIndex = 2;
929− tryCompare(mockUsageModeSettings, "usageMode", "Automatic")
929+ tryCompare(mockUnity8Settings, "usageMode", "Automatic")
930930
931931 loadShell("mako")
932932 var shell = findChild(orientedShell, "shell");
tests/qmltests/tst_Shell.qml+52−26
@@ -67,11 +67,6 @@ Rectangle {
6767 width: units.gu(40)
6868 height: units.gu(71)
6969 }
70− StateChangeScript {
71− script: {
72− GSettingsController.setUsageMode("Staged")
73− }
74− }
7570 },
7671 State {
7772 name: "tablet"
@@ -80,11 +75,6 @@ Rectangle {
8075 width: units.gu(100)
8176 height: units.gu(71)
8277 }
83− StateChangeScript {
84− script: {
85− GSettingsController.setUsageMode("Staged")
86− }
87− }
8878 },
8979 State {
9080 name: "desktop"
@@ -97,11 +87,6 @@ Rectangle {
9787 target: mouseEmulation
9888 checked: false
9989 }
100− StateChangeScript {
101− script: {
102− GSettingsController.setUsageMode("Windowed")
103− }
104− }
10590 }
10691 ]
10792
@@ -317,7 +302,6 @@ Rectangle {
317302 mouseEmulation.checked = true;
318303 tryCompare(shell, "enabled", true); // make sure greeter didn't leave us in disabled state
319304 tearDown();
320− GSettingsController.setUsageMode("Staged");
321305 }
322306
323307 function loadShell(formFactor) {
@@ -602,9 +586,9 @@ Rectangle {
602586 verify(mainAppId != "");
603587 var mainApp = ApplicationManager.findApplication(mainAppId);
604588 verify(mainApp);
605− tryCompare(mainApp, "state", ApplicationInfoInterface.Running);
589+ tryCompare(mainApp, "requestedState", ApplicationInfoInterface.RequestedRunning);
606590
607− // Suspend while call is active...
591+ // Display off while call is active...
608592 callManager.foregroundCall = phoneCall;
609593 Powerd.status = Powerd.Off;
610594 tryCompare(greeter, "shown", false);
@@ -615,8 +599,7 @@ Rectangle {
615599 Powerd.status = Powerd.Off;
616600 tryCompare(greeter, "fullyShown", true);
617601
618− tryCompare(ApplicationManager, "suspended", true);
619− compare(mainApp.state, ApplicationInfoInterface.Suspended);
602+ compare(mainApp.requestedState, ApplicationInfoInterface.RequestedSuspended);
620603
621604 // And wake up
622605 Powerd.status = Powerd.On;
@@ -632,8 +615,7 @@ Rectangle {
632615 removeTimeConstraintsFromDirectionalDragAreas(greeter);
633616 swipeAwayGreeter();
634617
635− tryCompare(ApplicationManager, "suspended", false);
636− compare(mainApp.state, ApplicationInfoInterface.Running);
618+ compare(mainApp.requestedState, ApplicationInfoInterface.RequestedRunning);
637619 tryCompare(ApplicationManager, "focusedApplicationId", mainAppId);
638620 }
639621
@@ -804,15 +786,15 @@ Rectangle {
804786
805787 function test_launchedAppHasActiveFocus_data() {
806788 return [
807− {tag: "phone", formFactor: "phone", usageMode: "Staged"},
808− {tag: "tablet", formFactor: "tablet", usageMode: "Staged"},
809− {tag: "desktop", formFactor: "tablet", usageMode: "Windowed"}
789+ {tag: "phone", formFactor: "phone", usageScenario: "phone"},
790+ {tag: "tablet", formFactor: "tablet", usageScenario: "tablet"},
791+ {tag: "desktop", formFactor: "tablet", usageScenario: "desktop"}
810792 ]
811793 }
812794
813795 function test_launchedAppHasActiveFocus(data) {
814− GSettingsController.setUsageMode(data.usageMode);
815796 loadShell(data.formFactor);
797+ shell.usageScenario = data.usageScenario;
816798 swipeAwayGreeter();
817799
818800 var webApp = ApplicationManager.startApplication("webbrowser-app");
@@ -1347,6 +1329,50 @@ Rectangle {
13471329 compare(launcher.inverted, data.launcherInverted);
13481330 }
13491331
1332+ function test_unfocusedAppsGetSuspendedAfterEnteringStagedMode() {
1333+ loadShell("tablet");
1334+ shell.usageScenario = "desktop";
1335+
1336+ var webBrowserApp = ApplicationManager.startApplication("webbrowser-app");
1337+ waitUntilAppWindowIsFullyLoaded(webBrowserApp);
1338+
1339+ var galleryApp = ApplicationManager.startApplication("gallery-app");
1340+ waitUntilAppWindowIsFullyLoaded(galleryApp);
1341+
1342+ ApplicationManager.requestFocusApplication("unity8-dash");
1343+ tryCompare(ApplicationManager, "focusedApplicationId", "unity8-dash");
1344+
1345+ compare(webBrowserApp.requestedState, ApplicationInfoInterface.RequestedRunning);
1346+ compare(galleryApp.requestedState, ApplicationInfoInterface.RequestedRunning);
1347+
1348+ shell.usageScenario = "tablet";
1349+
1350+ tryCompare(webBrowserApp, "requestedState", ApplicationInfoInterface.RequestedSuspended);
1351+ tryCompare(galleryApp, "requestedState", ApplicationInfoInterface.RequestedSuspended);
1352+ }
1353+
1354+ function test_unfocusedAppsAreResumedWhenEnteringWindowedMode() {
1355+ loadShell("tablet");
1356+ shell.usageScenario = "tablet";
1357+
1358+ var webBrowserApp = ApplicationManager.startApplication("webbrowser-app");
1359+ waitUntilAppWindowIsFullyLoaded(webBrowserApp);
1360+
1361+ var galleryApp = ApplicationManager.startApplication("gallery-app");
1362+ waitUntilAppWindowIsFullyLoaded(galleryApp);
1363+
1364+ ApplicationManager.requestFocusApplication("unity8-dash");
1365+ tryCompare(ApplicationManager, "focusedApplicationId", "unity8-dash");
1366+
1367+ compare(webBrowserApp.requestedState, ApplicationInfoInterface.RequestedSuspended);
1368+ compare(galleryApp.requestedState, ApplicationInfoInterface.RequestedSuspended);
1369+
1370+ shell.usageScenario = "desktop";
1371+
1372+ tryCompare(webBrowserApp, "requestedState", ApplicationInfoInterface.RequestedRunning);
1373+ tryCompare(galleryApp, "requestedState", ApplicationInfoInterface.RequestedRunning);
1374+ }
1375+
13501376 function test_altTabSwitchesFocus() {
13511377 loadShell("desktop");
13521378 shell.usageScenario = "desktop"