on xenial help sign in
merge prereq
commit1dbbef56a782f05fad9477075759a98885973e6e authorMichael Zanetti <michael.zanetti@canonical.com> dateThu 5 Nov 2015, 18:52 (11 years ago) parent26855ad6c2a43d
46 files changed+1108−251
CMakeLists.txt+1−1 debian/control+2−2 plugins/Utils/CMakeLists.txt+2 plugins/Utils/applicationsfiltermodel.cpp+101 plugins/Utils/applicationsfiltermodel.h+69 plugins/Utils/plugin.cpp+2 plugins/Utils/windowstatestorage.cpp+61−22 plugins/Utils/windowstatestorage.h+13−1 qml/Components/Dialogs.qml+29−2 qml/Components/ModeSwitchWarningDialog.qml+95 qml/Components/Orientations.qml+29 qml/Components/ShellDialog.qml+1−1 qml/Components/WindowControlButtons.qml+3 qml/Dash/graphics/phone/screenshots/libreoffice@12.png qml/DeviceConfiguration.qml+84−64 qml/OrientedShell.qml+31−21 qml/Panel/Panel.qml+5−3 qml/Shell.qml+7−11 qml/Stages/AbstractStage.qml+63 qml/Stages/DesktopStage.qml+14−24 qml/Stages/PhoneStage.qml+21−34 qml/Stages/ShimStage.qml+1−1 qml/Stages/SpreadDelegate.qml+8−9 qml/Stages/TabletStage.qml+24−36 qml/Stages/WindowResizeArea.qml+38−7 qml/graphics/applicationIcons/libreoffice@18.png tests/mocks/GSettings.1.0/fake_gsettings.cpp+31 tests/mocks/GSettings.1.0/fake_gsettings.h+10 tests/mocks/Unity/Application/ApplicationInfo.cpp+12 tests/mocks/Unity/Application/ApplicationInfo.h+4 tests/mocks/Unity/Application/ApplicationManager.cpp+10 tests/mocks/Unity/Application/ApplicationManager.h+1−1 tests/mocks/Unity/Launcher/MockLauncherModel.cpp+3 tests/mocks/Utils/CMakeLists.txt+2 tests/mocks/Utils/plugin.cpp+2 tests/mocks/Utils/windowstatestorage.cpp+17 tests/mocks/Utils/windowstatestorage.h+12 tests/plugins/Unity/Launcher/launchermodeltest.cpp+1 tests/qmltests/CMakeLists.txt+1 tests/qmltests/Components/tst_ModeSwitchWarningDialog.qml+75 tests/qmltests/Stages/tst_PhoneStage.qml−2 tests/qmltests/Stages/tst_SpreadDelegate.qml+6−2 tests/qmltests/Stages/tst_TabletStage.qml+2−2 tests/qmltests/Stages/tst_WindowResizeArea.qml+35−2 tests/qmltests/tst_OrientedShell.qml+1−1 tests/qmltests/tst_Shell.qml+179−2
CMakeLists.txt+1−1
@@ -57,7 +57,7 @@ find_package(Qt5DBus 5.2 REQUIRED)
5757find_package(Qt5Concurrent 5.2 REQUIRED)
5858find_package(Qt5Sql 5.2 REQUIRED)
5959
60−pkg_check_modules(APPLICATION_API REQUIRED unity-shell-application=9)
60+pkg_check_modules(APPLICATION_API REQUIRED unity-shell-application=10)
6161
6262# Standard install paths
6363include(GNUInstallDirs)
debian/control+2−2
@@ -127,7 +127,7 @@ Depends: qml-module-qtquick-layouts,
127127 qtdeclarative5-ubuntu-ui-toolkit-plugin (>= 1.3.1627) | qtdeclarative5-ubuntu-ui-toolkit-plugin-gles (>= 1.3.1627),
128128 qtdeclarative5-unity-notifications-plugin (>= 0.1.2) | unity-notifications-impl,
129129 ubuntu-thumbnailer-impl-0,
130− unity-application-impl-9,
130+ unity-application-impl-10,
131131 unity-notifications-impl-3,
132132 unity-plugin-scopes | unity-scopes-impl,
133133 unity-scopes-impl-7,
@@ -173,7 +173,7 @@ Pre-Depends: ${misc:Pre-Depends},
173173Depends: ${misc:Depends},
174174 ${shlibs:Depends},
175175Provides: unity-application-impl,
176− unity-application-impl-9,
176+ unity-application-impl-10,
177177Replaces: unity8-autopilot (<< 8.02+15.04.20150422-0ubuntu1)
178178Description: Fake environment for running Unity 8 shell
179179 Provides fake implementations of some QML modules used by Unity 8 shell
plugins/Utils/CMakeLists.txt+2
@@ -10,6 +10,8 @@ include_directories(
1010
1111set(QMLPLUGIN_SRC
1212 activefocuslogger.cpp
13+ ${APPLICATION_API_INCLUDEDIR}/unity/shell/application/ApplicationManagerInterface.h
14+ applicationsfiltermodel.cpp
1315 constants.cpp
1416 HomeKeyWatcher.cpp
1517 inputwatcher.cpp
plugins/Utils/applicationsfiltermodel.cppnew file+101
@@ -0,0 +1,101 @@
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+// unity-api
18+#include <unity/shell/application/ApplicationManagerInterface.h>
19+#include <unity/shell/application/ApplicationInfoInterface.h>
20+
21+#include "applicationsfiltermodel.h"
22+
23+using namespace unity::shell::application;
24+
25+ApplicationsFilterModel::ApplicationsFilterModel(QObject *parent):
26+ QSortFilterProxyModel(parent),
27+ m_appModel(nullptr),
28+ m_filterTouchApps(false),
29+ m_filterLegacyApps(false)
30+{
31+}
32+
33+ApplicationManagerInterface *ApplicationsFilterModel::applicationsModel() const
34+{
35+ return m_appModel;
36+}
37+
38+void ApplicationsFilterModel::setApplicationsModel(ApplicationManagerInterface *applicationsModel)
39+{
40+ if (m_appModel != applicationsModel) {
41+ if (m_appModel) {
42+ disconnect(m_appModel, &ApplicationManagerInterface::countChanged, this, &ApplicationsFilterModel::countChanged);
43+ }
44+ m_appModel = applicationsModel;
45+ setSourceModel(m_appModel);
46+ Q_EMIT applicationsModelChanged();
47+ connect(m_appModel, &ApplicationManagerInterface::countChanged, this, &ApplicationsFilterModel::countChanged);
48+ }
49+}
50+
51+bool ApplicationsFilterModel::filterTouchApps() const
52+{
53+ return m_filterTouchApps;
54+}
55+
56+void ApplicationsFilterModel::setFilterTouchApps(bool filterTouchApps)
57+{
58+ if (m_filterTouchApps != filterTouchApps) {
59+ m_filterTouchApps = filterTouchApps;
60+ Q_EMIT filterTouchAppsChanged();
61+
62+ invalidateFilter();
63+ Q_EMIT countChanged();
64+ }
65+}
66+
67+bool ApplicationsFilterModel::filterLegacyApps() const
68+{
69+ return m_filterLegacyApps;
70+}
71+
72+void ApplicationsFilterModel::setFilterLegacyApps(bool filterLegacyApps)
73+{
74+ if (m_filterLegacyApps != filterLegacyApps) {
75+ m_filterLegacyApps = filterLegacyApps;
76+ Q_EMIT filterLegacyAppsChanged();
77+
78+ invalidateFilter();
79+ Q_EMIT countChanged();
80+ }
81+}
82+
83+bool ApplicationsFilterModel::filterAcceptsRow(int source_row, const QModelIndex &source_parent) const
84+{
85+ Q_UNUSED(source_parent);
86+
87+ ApplicationInfoInterface *app = m_appModel->get(source_row);
88+ Q_ASSERT(app);
89+ if (m_filterLegacyApps && !app->isTouchApp()) {
90+ return false;
91+ }
92+ if (m_filterTouchApps && app->isTouchApp()) {
93+ return false;
94+ }
95+ return true;
96+}
97+
98+ApplicationInfoInterface *ApplicationsFilterModel::get(int index) const
99+{
100+ return m_appModel->get(mapToSource(this->index(index, 0)).row());
101+}
plugins/Utils/applicationsfiltermodel.hnew file+69
@@ -0,0 +1,69 @@
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+#ifndef APPLICATIONSFILTERMODEL_H
18+#define APPLICATIONSFILTERMODEL_H
19+
20+#include <QSortFilterProxyModel>
21+
22+namespace unity {
23+namespace shell {
24+namespace application {
25+class ApplicationManagerInterface;
26+class ApplicationInfoInterface;
27+}
28+}
29+}
30+using namespace unity::shell::application;
31+
32+class ApplicationsFilterModel: public QSortFilterProxyModel
33+{
34+ Q_OBJECT
35+
36+ Q_PROPERTY(unity::shell::application::ApplicationManagerInterface* applicationsModel READ applicationsModel WRITE setApplicationsModel NOTIFY applicationsModelChanged)
37+ Q_PROPERTY(bool filterTouchApps READ filterTouchApps WRITE setFilterTouchApps NOTIFY filterTouchAppsChanged)
38+ Q_PROPERTY(bool filterLegacyApps READ filterLegacyApps WRITE setFilterLegacyApps NOTIFY filterLegacyAppsChanged)
39+
40+ Q_PROPERTY(int count READ rowCount NOTIFY countChanged)
41+public:
42+ ApplicationsFilterModel(QObject *parent = 0);
43+
44+ ApplicationManagerInterface* applicationsModel() const;
45+ void setApplicationsModel(ApplicationManagerInterface* applicationsModel);
46+
47+ bool filterTouchApps() const;
48+ void setFilterTouchApps(bool filterTouchApps);
49+
50+ bool filterLegacyApps() const;
51+ void setFilterLegacyApps(bool filterLegacyApps);
52+
53+ bool filterAcceptsRow(int source_row, const QModelIndex &source_parent) const override;
54+
55+ Q_INVOKABLE unity::shell::application::ApplicationInfoInterface* get(int index) const;
56+
57+Q_SIGNALS:
58+ void applicationsModelChanged();
59+ void filterTouchAppsChanged();
60+ void filterLegacyAppsChanged();
61+ void countChanged();
62+
63+private:
64+ ApplicationManagerInterface* m_appModel;
65+ bool m_filterTouchApps;
66+ bool m_filterLegacyApps;
67+};
68+
69+#endif
plugins/Utils/plugin.cpp+2
@@ -35,6 +35,7 @@
3535#include "windowstatestorage.h"
3636#include "constants.h"
3737#include "timezoneFormatter.h"
38+#include "applicationsfiltermodel.h"
3839
3940static QObject *createWindowStateStorage(QQmlEngine *engine, QJSEngine *scriptEngine)
4041{
@@ -66,6 +67,7 @@ void UtilsPlugin::registerTypes(const char *uri)
6667 qmlRegisterSingletonType<TimezoneFormatter>(uri, 0, 1, "TimezoneFormatter",
6768 [](QQmlEngine*, QJSEngine*) -> QObject* { return new TimezoneFormatter; });
6869 qmlRegisterType<ActiveFocusLogger>(uri, 0, 1, "ActiveFocusLogger");
70+ qmlRegisterType<ApplicationsFilterModel>(uri, 0, 1, "ApplicationsFilterModel");
6971}
7072
7173void UtilsPlugin::initializeEngine(QQmlEngine *engine, const char *uri)
plugins/Utils/windowstatestorage.cpp+61−22
@@ -47,26 +47,38 @@ WindowStateStorage::~WindowStateStorage()
4747 m_db.close();
4848}
4949
50−void WindowStateStorage::saveGeometry(const QString &windowId, const QRect &rect)
50+void WindowStateStorage::saveState(const QString &windowId, WindowStateStorage::WindowState state)
5151{
52− QMutexLocker mutexLocker(&s_mutex);
52+ const QString queryString = QStringLiteral("INSERT OR REPLACE INTO state (windowId, state) values ('%1', '%2');")
53+ .arg(windowId)
54+ .arg((int)state);
55+
56+ saveValue(queryString);
57+}
58+
59+WindowStateStorage::WindowState WindowStateStorage::getState(const QString &windowId, WindowStateStorage::WindowState defaultValue) const
60+{
61+ const QString queryString = QStringLiteral("SELECT * FROM state WHERE windowId = '%1';")
62+ .arg(windowId);
5363
54− QString queryString = QStringLiteral("INSERT OR REPLACE INTO geometry (windowId, x, y, width, height) values ('%1', '%2', '%3', '%4', '%5');")
64+ QSqlQuery query = getValue(queryString);
65+
66+ if (!query.first()) {
67+ return defaultValue;
68+ }
69+ return (WindowState)query.value("state").toInt();
70+}
71+
72+void WindowStateStorage::saveGeometry(const QString &windowId, const QRect &rect)
73+{
74+ const QString queryString = QStringLiteral("INSERT OR REPLACE INTO geometry (windowId, x, y, width, height) values ('%1', '%2', '%3', '%4', '%5');")
5575 .arg(windowId)
5676 .arg(rect.x())
5777 .arg(rect.y())
5878 .arg(rect.width())
5979 .arg(rect.height());
6080
61− QFuture<void> future = QtConcurrent::run(executeAsyncQuery, queryString);
62− m_asyncQueries.append(future);
63−
64− QFutureWatcher<void> *futureWatcher = new QFutureWatcher<void>();
65− futureWatcher->setFuture(future);
66− connect(futureWatcher, &QFutureWatcher<void>::finished,
67− this,
68− [=](){ m_asyncQueries.removeAll(futureWatcher->future());
69− futureWatcher->deleteLater(); });
81+ saveValue(queryString);
7082}
7183
7284void WindowStateStorage::executeAsyncQuery(const QString &queryString)
@@ -82,20 +94,13 @@ void WindowStateStorage::executeAsyncQuery(const QString &queryString)
8294 }
8395}
8496
85−QRect WindowStateStorage::getGeometry(const QString &windowId, const QRect &defaultValue)
97+QRect WindowStateStorage::getGeometry(const QString &windowId, const QRect &defaultValue) const
8698{
87− QMutexLocker l(&s_mutex);
8899 QString queryString = QStringLiteral("SELECT * FROM geometry WHERE windowId = '%1';")
89100 .arg(windowId);
90− QSqlQuery query;
91101
92− bool ok = query.exec(queryString);
93− if (!ok) {
94− qWarning() << "Error retrieving window state for" << windowId
95− << "Driver error:" << query.lastError().driverText()
96− << "Database error:" << query.lastError().databaseText();
97− return defaultValue;
98− }
102+ QSqlQuery query = getValue(queryString);
103+
99104 if (!query.first()) {
100105 return defaultValue;
101106 }
@@ -114,4 +119,38 @@ void WindowStateStorage::initdb()
114119 QSqlQuery query;
115120 query.exec(QStringLiteral("CREATE TABLE geometry(windowId TEXT UNIQUE, x INTEGER, y INTEGER, width INTEGER, height INTEGER);"));
116121 }
122+
123+ if (!m_db.tables().contains("state")) {
124+ QSqlQuery query;
125+ query.exec("CREATE TABLE state(windowId TEXT UNIQUE, state INTEGER);");
126+ }
127+}
128+
129+void WindowStateStorage::saveValue(const QString &queryString)
130+{
131+ QMutexLocker mutexLocker(&s_mutex);
132+
133+ QFuture<void> future = QtConcurrent::run(executeAsyncQuery, queryString);
134+ m_asyncQueries.append(future);
135+
136+ QFutureWatcher<void> *futureWatcher = new QFutureWatcher<void>();
137+ futureWatcher->setFuture(future);
138+ connect(futureWatcher, &QFutureWatcher<void>::finished,
139+ this,
140+ [=](){ m_asyncQueries.removeAll(futureWatcher->future());
141+ futureWatcher->deleteLater(); });
142+}
143+
144+QSqlQuery WindowStateStorage::getValue(const QString &queryString) const
145+{
146+ QMutexLocker l(&s_mutex);
147+ QSqlQuery query;
148+
149+ bool ok = query.exec(queryString);
150+ if (!ok) {
151+ qWarning() << "Error retrieving database query:" << queryString
152+ << "Driver error:" << query.lastError().driverText()
153+ << "Database error:" << query.lastError().databaseText();
154+ }
155+ return query;
117156}
plugins/Utils/windowstatestorage.h+13−1
@@ -22,16 +22,28 @@
2222class WindowStateStorage: public QObject
2323{
2424 Q_OBJECT
25+ Q_ENUMS(WindowState)
2526public:
27+ enum WindowState {
28+ WindowStateNormal,
29+ WindowStateMaximized
30+ };
31+
2632 WindowStateStorage(QObject *parent = 0);
2733 virtual ~WindowStateStorage();
2834
35+ Q_INVOKABLE void saveState(const QString &windowId, WindowState state);
36+ Q_INVOKABLE WindowState getState(const QString &windowId, WindowState defaultValue) const;
37+
2938 Q_INVOKABLE void saveGeometry(const QString &windowId, const QRect &rect);
30− Q_INVOKABLE QRect getGeometry(const QString &windowId, const QRect &defaultValue);
39+ Q_INVOKABLE QRect getGeometry(const QString &windowId, const QRect &defaultValue) const;
3140
3241private:
3342 void initdb();
3443
44+ void saveValue(const QString &queryString);
45+ QSqlQuery getValue(const QString &queryString) const;
46+
3547 static void executeAsyncQuery(const QString &queryString);
3648 static QMutex s_mutex;
3749
qml/Components/Dialogs.qml+29−2
@@ -1,5 +1,5 @@
11/*
2− * Copyright (C) 2014 Canonical, Ltd.
2+ * Copyright (C) 2014-2015 Canonical, Ltd.
33 *
44 * This program is free software; you can redistribute it and/or modify
55 * it under the terms of the GNU General Public License as published by
@@ -21,6 +21,7 @@ import Unity.Session 0.1
2121import Ubuntu.Components 1.1
2222import GlobalShortcut 1.0
2323import Unity.Platform 1.0
24+import Utils 0.1
2425import "../Greeter"
2526
2627Item {
@@ -45,6 +46,31 @@ Item {
4546 d.showPowerDialog();
4647 }
4748
49+ onUsageScenarioChanged: {
50+ if (usageScenario != "desktop" && legacyAppsModel.count > 0 && !d.modeSwitchWarningPopup) {
51+ var comp = Qt.createComponent(Qt.resolvedUrl("ModeSwitchWarningDialog.qml"))
52+ d.modeSwitchWarningPopup = comp.createObject(root, {model: legacyAppsModel});
53+ d.modeSwitchWarningPopup.forceClose.connect(function() {
54+ while (legacyAppsModel.count > 0) {
55+ ApplicationManager.stopApplication(legacyAppsModel.get(0).appId);
56+ }
57+ d.modeSwitchWarningPopup.hide();
58+ d.modeSwitchWarningPopup.destroy();
59+ d.modeSwitchWarningPopup = null;
60+ })
61+ } else if (usageScenario == "desktop" && d.modeSwitchWarningPopup) {
62+ d.modeSwitchWarningPopup.hide();
63+ d.modeSwitchWarningPopup.destroy();
64+ d.modeSwitchWarningPopup = null;
65+ }
66+ }
67+
68+ ApplicationsFilterModel {
69+ id: legacyAppsModel
70+ applicationsModel: ApplicationManager
71+ filterTouchApps: true
72+ }
73+
4874 GlobalShortcut { // reboot/shutdown dialog
4975 shortcut: Qt.Key_PowerDown
5076 active: Platform.isPC
@@ -91,6 +117,8 @@ Item {
91117 id: d // private stuff
92118 objectName: "dialogsPrivate"
93119
120+ property var modeSwitchWarningPopup: null
121+
94122 function showPowerDialog() {
95123 if (!dialogLoader.active) {
96124 dialogLoader.sourceComponent = powerDialogComponent;
@@ -270,5 +298,4 @@ Item {
270298 unitySessionService.endSession();
271299 }
272300 }
273−
274301}
qml/Components/ModeSwitchWarningDialog.qmlnew file+95
@@ -0,0 +1,95 @@
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.4
18+import QtQuick.Layouts 1.1
19+import Ubuntu.Components 1.2
20+import Ubuntu.Components.ListItems 1.2
21+import Ubuntu.Components.Popups 1.2
22+
23+ShellDialog {
24+ id: root
25+ objectName: "modeSwitchWarningDialog"
26+
27+ property alias model: appRepeater.model
28+
29+ signal forceClose();
30+
31+ Label {
32+ text: i18n.tr("Apps may have unsaved data:")
33+ fontSize: "large"
34+ color: "#5D5D5D"
35+ }
36+
37+ Repeater {
38+ id: appRepeater
39+ RowLayout {
40+ spacing: units.gu(2)
41+ Image {
42+ Layout.preferredHeight: units.gu(2)
43+ Layout.preferredWidth: units.gu(2)
44+ source: model.icon
45+ sourceSize.width: width
46+ sourceSize.height: height
47+ }
48+ Label {
49+ Layout.fillWidth: true
50+ text: model.name
51+ color: "#888888"
52+ }
53+ }
54+ }
55+
56+ Label {
57+ text: i18n.tr("Re-dock, save your work and close these apps to continue.")
58+ wrapMode: Text.WordWrap
59+ color: "#888888"
60+ }
61+
62+ Label {
63+ text: i18n.tr("Or force close now (unsaved data will be lost).")
64+ wrapMode: Text.WordWrap
65+ color: "#888888"
66+ }
67+
68+ ThinDivider {}
69+
70+ RowLayout {
71+ Label {
72+ objectName: "reconnectLabel"
73+ Layout.fillWidth: true
74+ property bool clicked: false
75+ property string notClickedText: i18n.tr("OK, I will reconnect")
76+ property string clickedText: i18n.tr("Reconnect now!")
77+ text: clicked ? clickedText : notClickedText
78+ color: "#333333"
79+
80+ MouseArea {
81+ anchors.fill: parent
82+ onClicked: parent.clicked = true;
83+ }
84+ }
85+
86+ Button {
87+ objectName: "forceCloseButton"
88+ text: i18n.tr("Close all")
89+ color: UbuntuColors.red
90+ onClicked: {
91+ root.forceClose();
92+ }
93+ }
94+ }
95+}
qml/Components/Orientations.qmlnew file+29
@@ -0,0 +1,29 @@
1+/*
2+ * Copyright 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.4
18+
19+QtObject {
20+ // Just because "native" is a reserved keyword :(
21+ property int native_: Qt.PortraitOrientation
22+
23+ property int primary: Qt.PortraitOrientation
24+
25+ property int landscape: Qt.LandscapeOrientation
26+ property int invertedLandscape: Qt.InvertedLandscapeOrientation
27+ property int portrait: Qt.PortraitOrientation
28+ property int invertedPortrait: Qt.InvertedPortraitOrientation
29+}
qml/Components/ShellDialog.qml+1−1
@@ -31,7 +31,7 @@ Dialog {
3131 // NB: PopupBase, Dialog's superclass, will check for the existence of this property
3232 property bool reparentToRootItem: false
3333
34− onVisibleChanged: { if (!visible) { dialogLoader.active = false; } }
34+ onVisibleChanged: { if (!visible && dialogLoader) { dialogLoader.active = false; } }
3535
3636 Keys.onEscapePressed: hide()
3737
qml/Components/WindowControlButtons.qml+3
@@ -28,6 +28,7 @@ Row {
2828 signal maximize()
2929
3030 Rectangle {
31+ objectName: "closeWindowButton"
3132 height: parent.height; width: height; radius: height / 2
3233 gradient: Gradient {
3334 GradientStop { color: "#F49073"; position: 0 }
@@ -38,6 +39,7 @@ Row {
3839 MouseArea { anchors.fill: parent; onClicked: root.close() }
3940 }
4041 Rectangle {
42+ objectName: "minimizeWindowButton"
4143 height: parent.height; width: height; radius: height / 2
4244 gradient: Gradient {
4345 GradientStop { color: "#92918C"; position: 0 }
@@ -48,6 +50,7 @@ Row {
4850 MouseArea { anchors.fill: parent; onClicked: root.minimize() }
4951 }
5052 Rectangle {
53+ objectName: "maximizeWindowButton"
5154 height: parent.height; width: height; radius: height / 2
5255 gradient: Gradient {
5356 GradientStop { color: "#92918C"; position: 0 }
qml/Dash/graphics/phone/screenshots/libreoffice@12.pngnew file

Binary file not shown.

qml/DeviceConfiguration.qml+84−64
@@ -16,79 +16,99 @@
1616
1717import QtQuick 2.0
1818
19−StateGroup {
19+QtObject {
2020 id: root
2121
2222 readonly property int useNativeOrientation: -1
2323
24− property int primaryOrientation: useNativeOrientation
24+ // The only writable property in the API
25+ // all other properties are set according to the device name
26+ property alias name: priv.state
2527
26− property int supportedOrientations: Qt.PortraitOrientation
27− | Qt.InvertedPortraitOrientation
28− | Qt.LandscapeOrientation
29− | Qt.InvertedLandscapeOrientation
28+ readonly property alias primaryOrientation: priv.primaryOrientation
29+ readonly property alias supportedOrientations: priv.supportedOrientations
30+ readonly property alias landscapeOrientation: priv.landscapeOrientation
31+ readonly property alias invertedLandscapeOrientation: priv.invertedLandscapeOrientation
32+ readonly property alias portraitOrientation: priv.portraitOrientation
33+ readonly property alias invertedPortraitOrientation: priv.invertedPortraitOrientation
3034
31− // Supported values so far:
32− // "phone", "tablet" or "desktop"
33− property string category: "phone"
35+ readonly property alias category: priv.category
3436
35− property int ignoredMice: 0
37+ readonly property alias ignoredMice: priv.ignoredMice
3638
39+ readonly property var priv: StateGroup {
40+ id: priv
3741
38− property alias name: root.state
42+ property int primaryOrientation: root.useNativeOrientation
3943
40− states: [
41− State {
42− name: "mako"
43− PropertyChanges {
44− target: root
45− supportedOrientations: Qt.PortraitOrientation
46− | Qt.LandscapeOrientation
47− | Qt.InvertedLandscapeOrientation
48− }
49− },
50− State {
51− name: "krillin"
52− PropertyChanges {
53− target: root
54− supportedOrientations: Qt.PortraitOrientation
55− | Qt.LandscapeOrientation
56− | Qt.InvertedLandscapeOrientation
57− }
58− },
59− State {
60− name: "arale"
61− PropertyChanges {
62− target: root
63− supportedOrientations: Qt.PortraitOrientation
64− | Qt.LandscapeOrientation
65− | Qt.InvertedLandscapeOrientation
66− ignoredMice: 1
67− }
68− },
69− State {
70− name: "manta"
71− PropertyChanges {
72− target: root
73− category: "tablet"
74− }
75− },
76− State {
77− name: "flo"
78− PropertyChanges {
79− target: root
80− primaryOrientation: Qt.InvertedLandscapeOrientation
81− category: "tablet"
82− }
83− },
84− State {
85− name: "desktop"
86− PropertyChanges {
87− target: root
88− category: "desktop"
89− supportedOrientations: root.useNativeOrientation
90− }
91− }
92− ]
44+ property int supportedOrientations: Qt.PortraitOrientation
45+ | Qt.InvertedPortraitOrientation
46+ | Qt.LandscapeOrientation
47+ | Qt.InvertedLandscapeOrientation
48+
49+ property int landscapeOrientation: Qt.LandscapeOrientation
50+ property int invertedLandscapeOrientation: Qt.InvertedLandscapeOrientation
51+ property int portraitOrientation: Qt.PortraitOrientation
52+ property int invertedPortraitOrientation: Qt.InvertedPortraitOrientation
53+
54+ // Supported values so far:
55+ // "phone", "tablet" or "desktop"
56+ property string category: "phone"
9357
58+ property int ignoredMice: 0
59+
60+ states: [
61+ State {
62+ name: "mako"
63+ PropertyChanges {
64+ target: priv
65+ supportedOrientations: Qt.PortraitOrientation
66+ | Qt.LandscapeOrientation
67+ | Qt.InvertedLandscapeOrientation
68+ }
69+ },
70+ State {
71+ name: "krillin"
72+ PropertyChanges {
73+ target: priv
74+ supportedOrientations: Qt.PortraitOrientation
75+ | Qt.LandscapeOrientation
76+ | Qt.InvertedLandscapeOrientation
77+ }
78+ },
79+ State {
80+ name: "arale"
81+ PropertyChanges {
82+ target: priv
83+ supportedOrientations: Qt.PortraitOrientation
84+ | Qt.LandscapeOrientation
85+ | Qt.InvertedLandscapeOrientation
86+ ignoredMice: 1
87+ }
88+ },
89+ State {
90+ name: "manta"
91+ PropertyChanges {
92+ target: priv
93+ category: "tablet"
94+ }
95+ },
96+ State {
97+ name: "flo"
98+ PropertyChanges {
99+ target: priv
100+ landscapeOrientation: Qt.InvertedLandscapeOrientation
101+ primaryOrientation: Qt.InvertedLandscapeOrientation
102+ category: "tablet"
103+ }
104+ },
105+ State {
106+ name: "desktop"
107+ PropertyChanges {
108+ target: priv
109+ category: "desktop"
110+ }
111+ }
112+ ]
113+ }
94114}
qml/OrientedShell.qml+31−21
@@ -32,20 +32,31 @@ Rectangle {
3232 implicitWidth: units.gu(40)
3333 implicitHeight: units.gu(71)
3434
35− // NB: native and primary orientations here don't map exactly to their QScreen counterparts
36− readonly property int nativeOrientation: width > height ? Qt.LandscapeOrientation : Qt.PortraitOrientation
37−
38− readonly property int primaryOrientation:
39− deviceConfiguration.primaryOrientation == deviceConfiguration.useNativeOrientation
40− ? nativeOrientation : deviceConfiguration.primaryOrientation
41−
4235 DeviceConfiguration {
4336 id: deviceConfiguration
4437 name: applicationArguments.deviceName
4538 }
4639
40+ property alias orientations: d.orientations
41+
42+ Item {
43+ id: d
44+
45+ property Orientations orientations: Orientations {
46+ id: orientations
47+ // NB: native and primary orientations here don't map exactly to their QScreen counterparts
48+ native_: root.width > root.height ? Qt.LandscapeOrientation : Qt.PortraitOrientation
4749
48− // to be overwritten by tests
50+ primary: deviceConfiguration.primaryOrientation == deviceConfiguration.useNativeOrientation
51+ ? native_ : deviceConfiguration.primaryOrientation
52+
53+ landscape: deviceConfiguration.landscapeOrientation
54+ invertedLandscape: deviceConfiguration.invertedLandscapeOrientation
55+ portrait: deviceConfiguration.portraitOrientation
56+ invertedPortrait: deviceConfiguration.invertedPortraitOrientation
57+ }
58+ }
59+ // to be overwritten by tests
4960 property var unity8Settings: Unity8Settings {}
5061 property var oskSettings: GSettings { schema.id: "com.canonical.keyboard.maliit" }
5162
@@ -101,7 +112,7 @@ Rectangle {
101112
102113 property int acceptedOrientationAngle: {
103114 if (orientation & supportedOrientations) {
104− return Screen.angleBetween(nativeOrientation, orientation);
115+ return Screen.angleBetween(orientations.native_, orientation);
105116 } else if (shell.orientation & supportedOrientations) {
106117 // stay where we are
107118 return shell.orientationAngle;
@@ -111,16 +122,16 @@ Rectangle {
111122 // rotate to some supported orientation as we can't stay where we currently are
112123 // TODO: Choose the closest to the current one
113124 if (supportedOrientations & Qt.PortraitOrientation) {
114− return Screen.angleBetween(nativeOrientation, Qt.PortraitOrientation);
125+ return Screen.angleBetween(orientations.native_, Qt.PortraitOrientation);
115126 } else if (supportedOrientations & Qt.LandscapeOrientation) {
116− return Screen.angleBetween(nativeOrientation, Qt.LandscapeOrientation);
127+ return Screen.angleBetween(orientations.native_, Qt.LandscapeOrientation);
117128 } else if (supportedOrientations & Qt.InvertedPortraitOrientation) {
118− return Screen.angleBetween(nativeOrientation, Qt.InvertedPortraitOrientation);
129+ return Screen.angleBetween(orientations.native_, Qt.InvertedPortraitOrientation);
119130 } else if (supportedOrientations & Qt.InvertedLandscapeOrientation) {
120− return Screen.angleBetween(nativeOrientation, Qt.InvertedLandscapeOrientation);
131+ return Screen.angleBetween(orientations.native_, Qt.InvertedLandscapeOrientation);
121132 } else {
122133 // if all fails, fallback to primary orientation
123− return Screen.angleBetween(nativeOrientation, primaryOrientation);
134+ return Screen.angleBetween(orientations.native_, orientations.primary);
124135 }
125136 }
126137 }
@@ -128,19 +139,19 @@ Rectangle {
128139 function angleToOrientation(angle) {
129140 switch (angle) {
130141 case 0:
131− return nativeOrientation;
142+ return orientations.native_;
132143 case 90:
133− return nativeOrientation === Qt.PortraitOrientation ? Qt.InvertedLandscapeOrientation
144+ return orientations.native_ === Qt.PortraitOrientation ? Qt.InvertedLandscapeOrientation
134145 : Qt.PortraitOrientation;
135146 case 180:
136− return nativeOrientation === Qt.PortraitOrientation ? Qt.InvertedPortraitOrientation
147+ return orientations.native_ === Qt.PortraitOrientation ? Qt.InvertedPortraitOrientation
137148 : Qt.InvertedLandscapeOrientation;
138149 case 270:
139− return nativeOrientation === Qt.PortraitOrientation ? Qt.LandscapeOrientation
150+ return orientations.native_ === Qt.PortraitOrientation ? Qt.LandscapeOrientation
140151 : Qt.InvertedPortraitOrientation;
141152 default:
142153 console.warn("angleToOrientation: Invalid orientation angle: " + angle);
143− return primaryOrientation;
154+ return orientations.primary;
144155 }
145156 }
146157
@@ -159,8 +170,7 @@ Rectangle {
159170 width: root.width
160171 height: root.height
161172 orientation: root.angleToOrientation(orientationAngle)
162− primaryOrientation: root.primaryOrientation
163− nativeOrientation: root.nativeOrientation
173+ orientations: root.orientations
164174 nativeWidth: root.width
165175 nativeHeight: root.height
166176 mode: applicationArguments.mode
qml/Panel/Panel.qml+5−3
@@ -28,6 +28,7 @@ Item {
2828 property alias callHint: __callHint
2929 property bool fullscreenMode: false
3030 property real indicatorAreaShowProgress: 1.0
31+ property bool locked: false
3132
3233 opacity: fullscreenMode && indicators.fullyClosed ? 0.0 : 1.0
3334
@@ -136,7 +137,7 @@ Item {
136137 }
137138
138139 shown: false
139− width: root.width - (PanelState.buttonsVisible ? windowControlButtons.width : 0)
140+ width: root.width - (windowControlButtons.visible ? windowControlButtons.width : 0)
140141 minimizedPanelHeight: units.gu(3)
141142 expandedPanelHeight: units.gu(7)
142143 openedHeight: root.height - indicatorOrangeLine.height
@@ -164,13 +165,14 @@ Item {
164165
165166 WindowControlButtons {
166167 id: windowControlButtons
168+ objectName: "panelWindowControlButtons"
167169 anchors {
168170 left: parent.left
169171 top: parent.top
170172 margins: units.gu(0.7)
171173 }
172174 height: indicators.minimizedPanelHeight - anchors.margins * 2
173− visible: PanelState.buttonsVisible
175+ visible: PanelState.buttonsVisible && !root.locked
174176 onClose: PanelState.close()
175177 onMinimize: PanelState.minimize()
176178 onMaximize: PanelState.maximize()
@@ -189,7 +191,7 @@ Item {
189191 id: __callHint
190192 anchors {
191193 top: parent.top
192− left: PanelState.buttonsVisible ? windowControlButtons.right : parent.left
194+ left: windowControlButtons.visible ? windowControlButtons.right : parent.left
193195 }
194196 height: indicators.minimizedPanelHeight
195197 visible: active && indicators.state == "initial"
qml/Shell.qml+7−11
@@ -49,14 +49,14 @@ Item {
4949 // to be set from outside
5050 property int orientationAngle: 0
5151 property int orientation
52− property int primaryOrientation
53− property int nativeOrientation
52+ property Orientations orientations
5453 property real nativeWidth
5554 property real nativeHeight
5655 property alias indicatorAreaShowProgress: panel.indicatorAreaShowProgress
5756 property bool beingResized
5857 property string usageScenario: "phone" // supported values: "phone", "tablet" or "desktop"
5958 property string mode: "full-greeter"
59+ property bool cursorVisible: false
6060 function updateFocusedAppOrientation() {
6161 applicationsDisplayLoader.item.updateFocusedAppOrientation();
6262 }
@@ -298,18 +298,13 @@ Item {
298298 }
299299 Binding {
300300 target: applicationsDisplayLoader.item
301− property: "background"
302− value: wallpaperResolver.background
301+ property: "orientations"
302+ value: shell.orientations
303303 }
304304 Binding {
305305 target: applicationsDisplayLoader.item
306− property: "shellPrimaryOrientation"
307− value: shell.primaryOrientation
308− }
309− Binding {
310− target: applicationsDisplayLoader.item
311− property: "nativeOrientation"
312− value: shell.nativeOrientation
306+ property: "background"
307+ value: wallpaperResolver.background
313308 }
314309 Binding {
315310 target: applicationsDisplayLoader.item
@@ -536,6 +531,7 @@ Item {
536531
537532 fullscreenMode: (topmostApplicationIsFullscreen && !lightDM.greeter.active && launcher.progress == 0)
538533 || greeter.hasLockedApp
534+ locked: greeter && greeter.active
539535 }
540536
541537 Launcher {
qml/Stages/AbstractStage.qmlnew file+63
@@ -0,0 +1,63 @@
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.4
18+import Ubuntu.Components 1.3
19+import GSettings 1.0
20+
21+Rectangle {
22+ id: root
23+
24+ color: "#111111"
25+
26+ // Controls to be set from outside
27+ property bool altTabPressed
28+ property url background
29+ property bool beingResized
30+ property int dragAreaWidth
31+ property bool interactive
32+ property real inverseProgress // This is the progress for left edge drags, in pixels.
33+ property bool keepDashRunning: true
34+ property real maximizedAppTopMargin
35+ property real nativeHeight
36+ property real nativeWidth
37+ property QtObject orientations
38+ property int shellOrientation
39+ property int shellOrientationAngle
40+ property bool spreadEnabled: true // If false, animations and right edge will be disabled
41+ property bool suspended
42+
43+ // To be read from outside
44+ property var mainApp: null
45+ property int mainAppWindowOrientationAngle
46+ property bool orientationChangesEnabled
47+
48+ // Shared code for use in stage implementations
49+ GSettings {
50+ id: lifecycleExceptions
51+ schema.id: "com.canonical.qtmir"
52+ }
53+
54+ function isExemptFromLifecycle(appId) {
55+ var shortAppId = appId.split('_')[0];
56+ for (var i = 0; i < lifecycleExceptions.lifecycleExemptAppids.length; i++) {
57+ if (shortAppId === lifecycleExceptions.lifecycleExemptAppids[i]) {
58+ return true;
59+ }
60+ }
61+ return false;
62+ }
63+}
qml/Stages/DesktopStage.qml+14−24
@@ -22,39 +22,21 @@ import Ubuntu.Components 1.1
2222import Unity.Application 0.1
2323import "../Components"
2424import "../Components/PanelState"
25+import "../Components"
2526import Utils 0.1
2627import Ubuntu.Gestures 0.1
2728
28−Rectangle {
29+AbstractStage {
2930 id: root
3031 anchors.fill: parent
3132
32− // Controls to be set from outside
33− property int dragAreaWidth // just to comply with the interface shared between stages
34− property real maximizedAppTopMargin
35− property bool interactive
36− property bool spreadEnabled // just to comply with the interface shared between stages
37− property real inverseProgress: 0 // just to comply with the interface shared between stages
38− property int shellOrientationAngle: 0
39− property int shellOrientation
40− property int shellPrimaryOrientation
41− property int nativeOrientation
42− property bool beingResized: false
43− property bool keepDashRunning: true
44− property bool suspended: false
45− property alias background: wallpaper.source
46− property alias altTabPressed: spread.altTabPressed
47−
4833 // functions to be called from outside
4934 function updateFocusedAppOrientation() { /* TODO */ }
5035 function updateFocusedAppOrientationAnimated() { /* TODO */}
5136
52− // To be read from outside
53− readonly property var mainApp: ApplicationManager.focusedApplicationId
37+ mainApp: ApplicationManager.focusedApplicationId
5438 ? ApplicationManager.findApplication(ApplicationManager.focusedApplicationId)
5539 : null
56− property int mainAppWindowOrientationAngle: 0
57− readonly property bool orientationChangesEnabled: false
5840
5941 Connections {
6042 target: ApplicationManager
@@ -111,6 +93,7 @@ Rectangle {
11193 property: "buttonsVisible"
11294 value: priv.focusedAppDelegate !== null && priv.focusedAppDelegate.state === "maximized"
11395 }
96+ Component.onDestruction: PanelState.buttonsVisible = false;
11497
11598 FocusScope {
11699 id: appContainer
@@ -121,6 +104,7 @@ Rectangle {
121104 CrossFadeImage {
122105 id: wallpaper
123106 anchors.fill: parent
107+ source: root.background
124108 sourceSize { height: root.height; width: root.width }
125109 fillMode: Image.PreserveAspectCrop
126110 }
@@ -140,6 +124,7 @@ Rectangle {
140124
141125 property bool maximized: false
142126 property bool minimized: false
127+ property bool animationsEnabled: true
143128
144129 onFocusChanged: {
145130 if (focus && ApplicationManager.focusedApplicationId !== model.appId) {
@@ -158,15 +143,18 @@ Rectangle {
158143 value: ApplicationInfoInterface.RequestedRunning // Always running for now
159144 }
160145
161− function maximize() {
146+ function maximize(animated) {
147+ animationsEnabled = (animated === undefined) || animated;
162148 minimized = false;
163149 maximized = true;
164150 }
165− function minimize() {
151+ function minimize(animated) {
152+ animationsEnabled = (animated === undefined) || animated;
166153 maximized = false;
167154 minimized = true;
168155 }
169− function unmaximize() {
156+ function unmaximize(animated) {
157+ animationsEnabled = (animated === undefined) || animated;
170158 minimized = false;
171159 maximized = false;
172160 }
@@ -188,6 +176,7 @@ Rectangle {
188176 Transition {
189177 from: "maximized,minimized,normal,"
190178 to: "maximized,minimized,normal,"
179+ enabled: appDelegate.animationsEnabled
191180 PropertyAnimation { target: appDelegate; properties: "x,y,opacity,width,height,scale" }
192181 },
193182 Transition {
@@ -269,5 +258,6 @@ Rectangle {
269258 anchors.fill: parent
270259 workspace: appContainer
271260 focus: state == "altTab"
261+ altTabPressed: root.altTabPressed
272262 }
273263}
qml/Stages/PhoneStage.qml+21−34
@@ -22,29 +22,15 @@ import Unity.Session 0.1
2222import Utils 0.1
2323import "../Components"
2424
25−Rectangle {
25+AbstractStage {
2626 id: root
2727
28− // Controls to be set from outside
29− property int dragAreaWidth
30− property real maximizedAppTopMargin
31− property bool interactive
32− property bool spreadEnabled: true // If false, animations and right edge will be disabled
33− property real inverseProgress: 0 // This is the progress for left edge drags, in pixels.
3428 property QtObject applicationManager: ApplicationManager
3529 property bool focusFirstApp: true // If false, focused app will appear on right edge like other apps
3630 property bool altTabEnabled: true
3731 property real startScale: 1.1
3832 property real endScale: 0.7
39− property bool keepDashRunning: true
40− property bool suspended: false
41− property int shellOrientationAngle: 0
42− property int shellOrientation
43− property int shellPrimaryOrientation
44− property int nativeOrientation
45− property real nativeWidth
46− property real nativeHeight
47− property bool beingResized: false
33+
4834 onBeingResizedChanged: {
4935 if (beingResized) {
5036 // Brace yourselves for impact!
@@ -56,6 +42,8 @@ Rectangle {
5642 priv.reset();
5743 }
5844 }
45+
46+ // Functions to be called from outside
5947 function updateFocusedAppOrientation() {
6048 if (spreadRepeater.count > 0) {
6149 spreadRepeater.itemAt(0).matchShellOrientation();
@@ -71,7 +59,7 @@ Rectangle {
7159
7260 var supportedOrientations = spreadDelegate.application.supportedOrientations;
7361 if (supportedOrientations === Qt.PrimaryOrientation) {
74− supportedOrientations = spreadDelegate.shellPrimaryOrientation;
62+ supportedOrientations = root.orientations.primary;
7563 }
7664
7765 if (delta === 180 && (supportedOrientations & spreadDelegate.shellOrientation)) {
@@ -85,16 +73,14 @@ Rectangle {
8573 }
8674 }
8775
88− // To be read from outside
89− readonly property var mainApp: applicationManager.focusedApplicationId
76+ mainApp: applicationManager.focusedApplicationId
9077 ? applicationManager.findApplication(applicationManager.focusedApplicationId)
9178 : null
9279
93− property int mainAppWindowOrientationAngle: 0
94− readonly property bool orientationChangesEnabled: priv.focusedAppOrientationChangesEnabled
95− && !priv.focusedAppDelegateIsDislocated
96− && !(priv.focusedAppDelegate && priv.focusedAppDelegate.xBehavior.running)
97− && spreadView.phase === 0
80+ orientationChangesEnabled: priv.focusedAppOrientationChangesEnabled
81+ && !priv.focusedAppDelegateIsDislocated
82+ && !(priv.focusedAppDelegate && priv.focusedAppDelegate.xBehavior.running)
83+ && spreadView.phase === 0
9884
9985 // How far left the stage has been dragged
10086 readonly property real dragProgress: spreadRepeater.count > 0 ? -spreadRepeater.itemAt(0).xTranslate : 0
@@ -106,8 +92,6 @@ Rectangle {
10692
10793 signal opened()
10894
109− color: "#111111"
110−
11195 function select(appId) {
11296 spreadView.snapTo(priv.indexOf(appId));
11397 }
@@ -438,16 +422,21 @@ Rectangle {
438422 dropShadow: spreadView.active || priv.focusedAppDelegateIsDislocated
439423 focusFirstApp: root.focusFirstApp
440424
425+ readonly property bool isDash: model.appId == "unity8-dash"
426+
427+ readonly property bool canSuspend: model.isTouchApp
428+ && !isExemptFromLifecycle(model.appId)
429+
441430 Binding {
442431 target: appDelegate.application
443432 property: "requestedState"
444− value: (isDash && root.keepDashRunning) || (!root.suspended && appDelegate.focus)
445− ? ApplicationInfoInterface.RequestedRunning
446− : ApplicationInfoInterface.RequestedSuspended
433+ value: !canSuspend
434+ || (isDash && root.keepDashRunning)
435+ || (!root.suspended && appDelegate.focus)
436+ ? ApplicationInfoInterface.RequestedRunning
437+ : ApplicationInfoInterface.RequestedSuspended
447438 }
448439
449− readonly property bool isDash: model.appId == "unity8-dash"
450−
451440 z: isDash && !spreadView.active ? -1 : behavioredIndex
452441
453442 x: {
@@ -535,11 +524,9 @@ Rectangle {
535524 visible: (progress >= 0 && progress < 1.7)
536525 || (isDash && priv.focusedAppDelegateIsDislocated)
537526
538−
539527 shellOrientationAngle: root.shellOrientationAngle
540528 shellOrientation: root.shellOrientation
541− shellPrimaryOrientation: root.shellPrimaryOrientation
542− nativeOrientation: root.nativeOrientation
529+ orientations: root.orientations
543530
544531 onClicked: {
545532 if (root.altTabEnabled && spreadView.phase == 2) {
qml/Stages/ShimStage.qml+1−1
@@ -17,7 +17,7 @@
1717import QtQuick 2.3
1818import Ubuntu.Components 0.1
1919
20−Rectangle {
20+AbstractStage {
2121 id: shimStage
2222
2323 anchors.fill: parent
qml/Stages/SpreadDelegate.qml+8−9
@@ -42,8 +42,7 @@ FocusScope {
4242 property alias application: appWindow.application
4343 property int shellOrientationAngle
4444 property int shellOrientation
45− property int shellPrimaryOrientation
46− property int nativeOrientation
45+ property QtObject orientations
4746
4847 function matchShellOrientation() {
4948 if (!root.application)
@@ -157,7 +156,7 @@ FocusScope {
157156 var supportedOrientations = root.application.supportedOrientations;
158157
159158 if (supportedOrientations === Qt.PrimaryOrientation) {
160− supportedOrientations = root.shellPrimaryOrientation;
159+ supportedOrientations = root.orientations.primary;
161160 }
162161
163162 // If it doesn't support shell's current orientation
@@ -166,18 +165,18 @@ FocusScope {
166165 if (supportedOrientations & root.shellOrientation) {
167166 chosenOrientation = root.shellOrientation;
168167 } else if (supportedOrientations & Qt.PortraitOrientation) {
169− chosenOrientation = Qt.PortraitOrientation;
168+ chosenOrientation = root.orientations.portrait;
170169 } else if (supportedOrientations & Qt.LandscapeOrientation) {
171− chosenOrientation = Qt.LandscapeOrientation;
170+ chosenOrientation = root.orientations.landscape;
172171 } else if (supportedOrientations & Qt.InvertedPortraitOrientation) {
173− chosenOrientation = Qt.InvertedPortraitOrientation;
172+ chosenOrientation = root.orientations.invertedPortrait;
174173 } else if (supportedOrientations & Qt.InvertedLandscapeOrientation) {
175− chosenOrientation = Qt.InvertedLandscapeOrientation;
174+ chosenOrientation = root.orientations.invertedLandscape;
176175 } else {
177− chosenOrientation = root.shellPrimaryOrientation;
176+ chosenOrientation = root.orientations.primary;
178177 }
179178
180− return Screen.angleBetween(root.nativeOrientation, chosenOrientation);
179+ return Screen.angleBetween(root.orientations.native_, chosenOrientation);
181180 }
182181
183182 rotation: normalizeAngle(appWindowWithShadow.orientationAngle - root.shellOrientationAngle)
qml/Stages/TabletStage.qml+24−36
@@ -21,29 +21,12 @@ import Unity.Application 0.1
2121import Utils 0.1
2222import "../Components"
2323
24−Rectangle {
24+AbstractStage {
2525 id: root
2626 objectName: "stages"
2727 anchors.fill: parent
28− color: "#111111"
29−
30− // Controls to be set from outside
31− property int dragAreaWidth
32− property real maximizedAppTopMargin
33− property bool interactive
34− property alias beingResized: spreadView.beingResized
35−
36− property bool spreadEnabled: true // If false, animations and right edge will be disabled
37−
38− property real inverseProgress: 0 // This is the progress for left edge drags, in pixels.
39− property bool keepDashRunning: true
40− property bool suspended: false
41− property int shellOrientationAngle: 0
42− property int shellOrientation
43− property int shellPrimaryOrientation
44− property int nativeOrientation
45− property real nativeWidth
46− property real nativeHeight
28+
29+ // Functions to be called from outside
4730 function updateFocusedAppOrientation() {
4831 var mainStageAppIndex = priv.indexOf(priv.mainStageAppId);
4932 if (mainStageAppIndex >= 0 && mainStageAppIndex < spreadRepeater.count) {
@@ -64,7 +47,7 @@ Rectangle {
6447
6548 var supportedOrientations = spreadDelegate.application.supportedOrientations;
6649 if (supportedOrientations === Qt.PrimaryOrientation) {
67− supportedOrientations = spreadDelegate.shellPrimaryOrientation;
50+ supportedOrientations = spreadDelegate.orientations.primary;
6851 }
6952
7053 if (delta === 180 && (supportedOrientations & spreadDelegate.shellOrientation)) {
@@ -86,10 +69,7 @@ Rectangle {
8669 }
8770 }
8871
89− // To be read from outside
90− property var mainApp: null
91− property int mainAppWindowOrientationAngle: 0
92− readonly property bool orientationChangesEnabled: priv.mainAppOrientationChangesEnabled
72+ orientationChangesEnabled: priv.mainAppOrientationChangesEnabled
9373
9474 onWidthChanged: {
9575 spreadView.selectedIndex = -1;
@@ -130,7 +110,7 @@ Rectangle {
130110 property string oldFocusedAppId: ""
131111 property bool mainAppOrientationChangesEnabled: false
132112
133− property real landscapeHeight: root.nativeOrientation == Qt.LandscapeOrientation ?
113+ property real landscapeHeight: root.orientations.native_ == Qt.LandscapeOrientation ?
134114 root.nativeHeight : root.nativeWidth
135115
136116 property bool shellIsLandscape: root.shellOrientation === Qt.LandscapeOrientation
@@ -302,7 +282,7 @@ Rectangle {
302282 }
303283
304284 property bool animateX: true
305− property bool beingResized: false
285+ property bool beingResized: root.beingResized
306286 onBeingResizedChanged: {
307287 if (beingResized) {
308288 // Brace yourselves for impact!
@@ -629,15 +609,18 @@ Rectangle {
629609
630610 readonly property bool isDash: model.appId == "unity8-dash"
631611
612+ readonly property bool canSuspend: model.isTouchApp
613+ && !isExemptFromLifecycle(model.appId)
614+
632615 Binding {
633616 target: spreadTile.application
634617 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
618+ value: !canSuspend
619+ || (isDash && root.keepDashRunning)
620+ || (!root.suspended && (model.appId == priv.mainStageAppId
621+ || model.appId == priv.sideStageAppId))
622+ ? ApplicationInfoInterface.RequestedRunning
623+ : ApplicationInfoInterface.RequestedSuspended
641624 }
642625
643626 // FIXME: A regular binding doesn't update any more after closing an app.
@@ -691,9 +674,14 @@ Rectangle {
691674
692675 shellOrientationAngle: wantsMainStage ? root.shellOrientationAngle : 0
693676 shellOrientation: wantsMainStage ? root.shellOrientation : Qt.PortraitOrientation
694− shellPrimaryOrientation: wantsMainStage ? root.shellPrimaryOrientation : Qt.PortraitOrientation
695− nativeOrientation: wantsMainStage ? root.nativeOrientation : Qt.PortraitOrientation
696−
677+ orientations: Orientations {
678+ primary: spreadTile.wantsMainStage ? root.orientations.primary : Qt.PortraitOrientation
679+ native_: spreadTile.wantsMainStage ? root.orientations.native_ : Qt.PortraitOrientation
680+ portrait: root.orientations.portrait
681+ invertedPortrait: root.orientations.invertedPortrait
682+ landscape: root.orientations.landscape
683+ invertedLandscape: root.orientations.invertedLandscape
684+ }
697685
698686 onClicked: {
699687 if (spreadView.phase == 2) {
qml/Stages/WindowResizeArea.qml+38−7
@@ -36,18 +36,49 @@ MouseArea {
3636 property int minWidth: 0
3737 property int minHeight: 0
3838
39+ QtObject {
40+ id: priv
41+
42+ property int normalX: 0
43+ property int normalY: 0
44+ property int normalWidth: 0
45+ property int normalHeight: 0
46+
47+ function updateNormalGeometry() {
48+ if (root.target.state == "normal") {
49+ normalX = root.target.x
50+ normalY = root.target.y
51+ normalWidth = root.target.width
52+ normalHeight = root.target.height
53+ }
54+ }
55+ }
56+
57+ Connections {
58+ target: root.target
59+ onXChanged: priv.updateNormalGeometry();
60+ onYChanged: priv.updateNormalGeometry();
61+ onWidthChanged: priv.updateNormalGeometry();
62+ onHeightChanged: priv.updateNormalGeometry();
63+ }
64+
3965 Component.onCompleted: {
40− var windowState = windowStateStorage.getGeometry(root.windowId, Qt.rect(target.x, target.y, target.width, target.height))
41− if (windowState !== undefined) {
42− target.x = windowState.x
43− target.y = windowState.y
44− target.width = windowState.width
45− target.height = windowState.height
66+ var windowGeometry = windowStateStorage.getGeometry(root.windowId, Qt.rect(target.x, target.y, target.width, target.height))
67+ if (windowGeometry !== undefined) {
68+ target.x = windowGeometry.x
69+ target.y = windowGeometry.y
70+ target.width = windowGeometry.width
71+ target.height = windowGeometry.height
72+ }
73+ var windowState = windowStateStorage.getState(root.windowId, WindowStateStorage.WindowStateNormal)
74+ if (windowState === WindowStateStorage.WindowStateMaximized) {
75+ target.maximize(false)
4676 }
4777 }
4878
4979 Component.onDestruction: {
50− windowStateStorage.saveGeometry(root.windowId, Qt.rect(target.x, target.y, target.width, target.height))
80+ windowStateStorage.saveState(root.windowId, target.state == "maximized" ? WindowStateStorage.WindowStateMaximized : WindowStateStorage.WindowStateNormal)
81+ windowStateStorage.saveGeometry(root.windowId, Qt.rect(priv.normalX, priv.normalY, priv.normalWidth, priv.normalHeight))
5182 }
5283
5384 QtObject {
qml/graphics/applicationIcons/libreoffice@18.pngnew file

Binary file not shown.

tests/mocks/GSettings.1.0/fake_gsettings.cpp+31
@@ -75,6 +75,19 @@ void GSettingsControllerQml::setLockedOutTime(qint64 timestamp)
7575 }
7676}
7777
78+QStringList GSettingsControllerQml::lifecycleExemptAppids() const
79+{
80+ return m_lifecycleExemptAppids;
81+}
82+
83+void GSettingsControllerQml::setLifecycleExemptAppids(const QStringList &appIds)
84+{
85+ if (m_lifecycleExemptAppids != appIds) {
86+ m_lifecycleExemptAppids = appIds;
87+ Q_EMIT lifecycleExemptAppidsChanged(m_lifecycleExemptAppids);
88+ }
89+}
90+
7891GSettingsSchemaQml::GSettingsSchemaQml(QObject *parent): QObject(parent) {
7992}
8093
@@ -114,6 +127,8 @@ GSettingsQml::GSettingsQml(QObject *parent)
114127 this, &GSettingsQml::usageModeChanged);
115128 connect(GSettingsControllerQml::instance(), &GSettingsControllerQml::lockedOutTimeChanged,
116129 this, &GSettingsQml::lockedOutTimeChanged);
130+ connect(GSettingsControllerQml::instance(), &GSettingsControllerQml::lifecycleExemptAppidsChanged,
131+ this, &GSettingsQml::lifecycleExemptAppidsChanged);
117132}
118133
119134GSettingsSchemaQml * GSettingsQml::schema() const {
@@ -167,3 +182,19 @@ void GSettingsQml::setLockedOutTime(qint64 timestamp)
167182 GSettingsControllerQml::instance()->setLockedOutTime(timestamp);
168183 }
169184}
185+
186+QStringList GSettingsQml::lifecycleExemptAppids() const
187+{
188+ if (m_schema->id() == "com.canonical.qtmir") {
189+ return GSettingsControllerQml::instance()->lifecycleExemptAppids();
190+ } else {
191+ return QStringList();
192+ }
193+}
194+
195+void GSettingsQml::setLifecycleExemptAppids(const QStringList &appIds)
196+{
197+ if (m_schema->id() == "com.canonical.qtmir") {
198+ GSettingsControllerQml::instance()->setLifecycleExemptAppids(appIds);
199+ }
200+}
tests/mocks/GSettings.1.0/fake_gsettings.h+10
@@ -19,6 +19,7 @@
1919
2020#include <QList>
2121#include <QObject>
22+#include <QStringList>
2223
2324class GSettingsSchemaQml: public QObject
2425{
@@ -48,6 +49,7 @@ class GSettingsQml: public QObject
4849 Q_PROPERTY(QString pictureUri READ pictureUri WRITE setPictureUri NOTIFY pictureUriChanged)
4950 Q_PROPERTY(QString usageMode READ usageMode WRITE setUsageMode NOTIFY usageModeChanged)
5051 Q_PROPERTY(qint64 lockedOutTime READ lockedOutTime WRITE setLockedOutTime NOTIFY lockedOutTimeChanged)
52+ Q_PROPERTY(QStringList lifecycleExemptAppids READ lifecycleExemptAppids WRITE setLifecycleExemptAppids NOTIFY lifecycleExemptAppidsChanged)
5153
5254public:
5355 GSettingsQml(QObject *parent = nullptr);
@@ -56,16 +58,19 @@ public:
5658 QString pictureUri() const;
5759 QString usageMode() const;
5860 qint64 lockedOutTime() const;
61+ QStringList lifecycleExemptAppids() const;
5962
6063 void setPictureUri(const QString &str);
6164 void setUsageMode(const QString &usageMode);
6265 void setLockedOutTime(qint64 timestamp);
66+ void setLifecycleExemptAppids(const QStringList &appIds);
6367
6468Q_SIGNALS:
6569 void schemaChanged();
6670 void pictureUriChanged(const QString&);
6771 void usageModeChanged(const QString&);
6872 void lockedOutTimeChanged(qint64);
73+ void lifecycleExemptAppidsChanged(const QStringList &);
6974
7075private:
7176 GSettingsSchemaQml* m_schema;
@@ -90,10 +95,14 @@ public:
9095 qint64 lockedOutTime() const;
9196 Q_INVOKABLE void setLockedOutTime(qint64 timestamp);
9297
98+ QStringList lifecycleExemptAppids() const;
99+ Q_INVOKABLE void setLifecycleExemptAppids(const QStringList &appIds);
100+
93101Q_SIGNALS:
94102 void pictureUriChanged(const QString&);
95103 void usageModeChanged(const QString&);
96104 void lockedOutTimeChanged(qint64 timestamp);
105+ void lifecycleExemptAppidsChanged(const QStringList&);
97106
98107private:
99108 GSettingsControllerQml();
@@ -101,6 +110,7 @@ private:
101110 QString m_pictureUri;
102111 QString m_usageMode;
103112 qint64 m_lockedOutTime;
113+ QStringList m_lifecycleExemptAppids;
104114
105115 static GSettingsControllerQml* s_controllerInstance;
106116 QList<GSettingsQml *> m_registeredGSettings;
tests/mocks/Unity/Application/ApplicationInfo.cpp+12
@@ -41,6 +41,7 @@ ApplicationInfo::ApplicationInfo(const QString &appId, QObject *parent)
4141 Qt::InvertedLandscapeOrientation)
4242 , m_rotatesWindowContents(false)
4343 , m_requestedState(RequestedRunning)
44+ , m_isTouchApp(true)
4445 , m_manualSurfaceCreation(false)
4546{
4647}
@@ -58,6 +59,7 @@ ApplicationInfo::ApplicationInfo(QObject *parent)
5859 Qt::InvertedLandscapeOrientation)
5960 , m_rotatesWindowContents(false)
6061 , m_requestedState(RequestedRunning)
62+ , m_isTouchApp(true)
6163 , m_manualSurfaceCreation(false)
6264{
6365}
@@ -245,6 +247,16 @@ void ApplicationInfo::setRequestedState(RequestedState value)
245247 }
246248}
247249
250+bool ApplicationInfo::isTouchApp() const
251+{
252+ return m_isTouchApp;
253+}
254+
255+void ApplicationInfo::setIsTouchApp(bool isTouchApp)
256+{
257+ m_isTouchApp = isTouchApp;
258+}
259+
248260void ApplicationInfo::onSessionSurfaceChanged(MirSurface* surface)
249261{
250262 if (surface != nullptr && m_state == Starting) {
tests/mocks/Unity/Application/ApplicationInfo.h+4
@@ -89,6 +89,9 @@ public:
8989 bool manualSurfaceCreation() const { return m_manualSurfaceCreation; }
9090 void setManualSurfaceCreation(bool value);
9191
92+ bool isTouchApp() const override;
93+ void setIsTouchApp(bool isTouchApp); // only in mock
94+
9295public:
9396 void setSession(Session* session);
9497 Session* session() const { return m_session; }
@@ -121,6 +124,7 @@ private:
121124 Qt::ScreenOrientations m_supportedOrientations;
122125 bool m_rotatesWindowContents;
123126 RequestedState m_requestedState;
127+ bool m_isTouchApp;
124128
125129 bool m_manualSurfaceCreation;
126130};
tests/mocks/Unity/Application/ApplicationManager.cpp+10
@@ -105,6 +105,8 @@ QVariant ApplicationManager::data(const QModelIndex& index, int role) const {
105105 return app->state();
106106 case RoleFocused:
107107 return app->focused();
108+ case RoleIsTouchApp:
109+ return app->isTouchApp();
108110 case RoleSession:
109111 return QVariant::fromValue(app->session());
110112 case RoleFullscreen:
@@ -458,6 +460,14 @@ void ApplicationManager::buildListOfAvailableApplications()
458460 application->setName("YouTube");
459461 application->setIconId("youtube");
460462 m_availableApplications.append(application);
463+
464+ application = new ApplicationInfo(this);
465+ application->setAppId("libreoffice");
466+ application->setName("LibreOffice");
467+ application->setIconId("libreoffice");
468+ application->setScreenshotId("libreoffice");
469+ application->setIsTouchApp(false);
470+ m_availableApplications.append(application);
461471}
462472
463473
tests/mocks/Unity/Application/ApplicationManager.h+1−1
@@ -43,7 +43,7 @@ class ApplicationManager : public ApplicationManagerInterface {
4343 static ApplicationManager *singleton();
4444
4545 enum MoreRoles {
46− RoleSession = RoleFocused+1,
46+ RoleSession = RoleIsTouchApp+1,
4747 RoleFullscreen,
4848 };
4949 enum Flag {
tests/mocks/Unity/Launcher/MockLauncherModel.cpp+3
@@ -75,6 +75,9 @@ MockLauncherModel::MockLauncherModel(QObject* parent): LauncherModelInterface(pa
7575 item = new MockLauncherItem("calendar-app", "/usr/share/applications/calendar-app.desktop","Calendar", "calendar", this);
7676 item->setPinned(true);
7777 m_list.append(item);
78+ item = new MockLauncherItem("libreoffice", "/usr/share/applications/libreoffice.desktop","Libre Office", "libreoffice", this);
79+ item->setPinned(true);
80+ m_list.append(item);
7881}
7982
8083MockLauncherModel::~MockLauncherModel()
tests/mocks/Utils/CMakeLists.txt+2
@@ -18,6 +18,8 @@ set(QMLPLUGIN_SRC
1818 ${CMAKE_SOURCE_DIR}/plugins/Utils/windowscreenshotprovider.cpp
1919 ${CMAKE_SOURCE_DIR}/plugins/Utils/easingcurve.cpp
2020 ${CMAKE_SOURCE_DIR}/plugins/Utils/inputwatcher.cpp
21+ ${CMAKE_SOURCE_DIR}/plugins/Utils/applicationsfiltermodel.cpp
22+ ${APPLICATION_API_INCLUDEDIR}/unity/shell/application/ApplicationManagerInterface.h
2123 constants.cpp
2224 plugin.cpp
2325 windowstatestorage.cpp
tests/mocks/Utils/plugin.cpp+2
@@ -35,6 +35,7 @@
3535#include <windowkeysfilter.h>
3636#include <windowscreenshotprovider.h>
3737#include <easingcurve.h>
38+#include <applicationsfiltermodel.h>
3839
3940static QObject *createWindowStateStorage(QQmlEngine *engine, QJSEngine *scriptEngine)
4041{
@@ -63,6 +64,7 @@ void FakeUtilsPlugin::registerTypes(const char *uri)
6364 qmlRegisterType<InputWatcher>(uri, 0, 1, "InputWatcher");
6465 qmlRegisterSingletonType<Constants>(uri, 0, 1, "Constants", createConstants);
6566 qmlRegisterType<ActiveFocusLogger>(uri, 0, 1, "ActiveFocusLogger");
67+ qmlRegisterType<ApplicationsFilterModel>(uri, 0, 1, "ApplicationsFilterModel");
6668}
6769
6870void FakeUtilsPlugin::initializeEngine(QQmlEngine *engine, const char *uri)
tests/mocks/Utils/windowstatestorage.cpp+17
@@ -44,3 +44,20 @@ QRect WindowStateStorage::getGeometry(const QString &windowId, const QRect &defa
4444 if (!m_geometry.contains(windowId)) return defaultValue;
4545 return m_geometry.value(windowId).toRect();
4646}
47+
48+void WindowStateStorage::clear()
49+{
50+ m_state.clear();
51+ m_geometry.clear();
52+}
53+
54+void WindowStateStorage::saveState(const QString &windowId, WindowState state)
55+{
56+ m_state[windowId] = state;
57+}
58+
59+WindowStateStorage::WindowState WindowStateStorage::getState(const QString &windowId, WindowStateStorage::WindowState defaultValue)
60+{
61+ if (!m_state.contains(windowId)) return defaultValue;
62+ return m_state.value(windowId);
63+}
tests/mocks/Utils/windowstatestorage.h+12
@@ -22,12 +22,23 @@ class WindowStateStorage: public QObject
2222{
2323 Q_OBJECT
2424 Q_PROPERTY(QVariantMap geometry READ geometry WRITE setGeometry NOTIFY geometryChanged)
25+ Q_ENUMS(WindowState)
2526public:
27+ enum WindowState {
28+ WindowStateNormal,
29+ WindowStateMaximized
30+ };
2631 WindowStateStorage(QObject *parent = 0);
2732
33+ Q_INVOKABLE void saveState(const QString &windowId, WindowState state);
34+ Q_INVOKABLE WindowState getState(const QString &windowId, WindowState defaultValue);
35+
2836 Q_INVOKABLE void saveGeometry(const QString &windowId, const QRect &rect);
2937 Q_INVOKABLE QRect getGeometry(const QString &windowId, const QRect &defaultValue);
3038
39+ // Only in the mock, to easily restore a fresh state
40+ Q_INVOKABLE void clear();
41+
3142Q_SIGNALS:
3243 void geometryChanged(const QVariantMap& geometry);
3344
@@ -35,5 +46,6 @@ private:
3546 void setGeometry(const QVariantMap& geometry);
3647 QVariantMap geometry() const;
3748
49+ QHash<QString, WindowState> m_state;
3850 QVariantMap m_geometry;
3951};
tests/plugins/Unity/Launcher/launchermodeltest.cpp+1
@@ -57,6 +57,7 @@ public:
5757 QColor splashColorFooter() const override { return QColor(0,0,0,0); }
5858 Qt::ScreenOrientations supportedOrientations() const override { return Qt::PortraitOrientation; }
5959 bool rotatesWindowContents() const override { return false; }
60+ bool isTouchApp() const override { return true; }
6061
6162 // Methods used for mocking (not in the interface)
6263 void setFocused(bool focused) { m_focused = focused; Q_EMIT focusedChanged(focused); }
tests/qmltests/CMakeLists.txt+1
@@ -11,6 +11,7 @@ add_unity8_qmltest(Components Dialogs LIGHTDM)
1111add_unity8_qmltest(Components DraggingArea)
1212add_unity8_qmltest(Components LazyImage)
1313add_unity8_qmltest(Components Lockscreen)
14+add_unity8_qmltest(Components ModeSwitchWarningDialog)
1415add_unity8_qmlunittest(Components PhysicalKeysMapper)
1516add_unity8_qmltest(Components Rating)
1617add_unity8_qmltest(Components ResponsiveGridView)
tests/qmltests/Components/tst_ModeSwitchWarningDialog.qmlnew file+75
@@ -0,0 +1,75 @@
1+/*
2+ * Copyright 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 QtTest 1.0
19+import "../../../qml/Components"
20+import Unity.Test 0.1 as UT
21+import Ubuntu.Components 1.2
22+import Ubuntu.Components.Popups 1.2
23+
24+Rectangle {
25+ id: root
26+ width: units.gu(40)
27+ height: units.gu(70)
28+ color: "lightgrey"
29+
30+ ListModel {
31+ id: fakeModel
32+ ListElement { icon: "../graphics/applicationIcons/facebook.png"; name: "Facebook" }
33+ ListElement { icon: "../graphics/applicationIcons/gmail.png"; name: "Mail" }
34+ }
35+
36+ function openWarning() {
37+ return PopupUtils.open(Qt.resolvedUrl("../../../qml/Components/ModeSwitchWarningDialog.qml"), root, { model: fakeModel });
38+ }
39+
40+ Button {
41+ anchors.centerIn: parent
42+ text: "trigger warning"
43+ onClicked: {
44+ openWarning();
45+ }
46+ }
47+
48+ SignalSpy {
49+ id: spy
50+ signalName: "forceClose"
51+ }
52+
53+ UT.UnityTestCase {
54+ name: "ModeSwitchWarning"
55+ when: windowShown
56+
57+ function test_labelChangesOnClick() {
58+ var popup = openWarning();
59+ var reconnectLabel = findChild(popup, "reconnectLabel");
60+ tryCompare(reconnectLabel, "text", reconnectLabel.notClickedText)
61+ mouseClick(reconnectLabel, reconnectLabel.width / 2, reconnectLabel.height / 2)
62+ tryCompare(reconnectLabel, "text", reconnectLabel.clickedText)
63+ }
64+
65+ function test_closeButtonEmitsSignal() {
66+ var popup = openWarning();
67+ spy.target = popup;
68+ spy.clear();
69+ var closeButton = findChild(popup, "forceCloseButton");
70+ mouseClick(closeButton, closeButton.width / 2, closeButton.height / 2)
71+ tryCompare(spy, "count", 1)
72+
73+ }
74+ }
75+}
tests/qmltests/Stages/tst_PhoneStage.qml−2
@@ -34,8 +34,6 @@ Item {
3434 maximizedAppTopMargin: units.gu(3) + units.dp(2)
3535 interactive: true
3636 shellOrientation: Qt.PortraitOrientation
37− shellPrimaryOrientation: Qt.PortraitOrientation
38− nativeOrientation: Qt.PortraitOrientation
3937 }
4038
4139 Binding {
tests/qmltests/Stages/tst_SpreadDelegate.qml+6−2
@@ -18,6 +18,7 @@ import QtQuick 2.0
1818import QtTest 1.0
1919import Unity.Test 0.1 as UT
2020import ".."
21+import "../../../qml/Components"
2122import "../../../qml/Stages"
2223import Ubuntu.Components 0.1
2324import Ubuntu.Components.ListItems 1.0 as ListItem
@@ -73,8 +74,11 @@ Rectangle {
7374 return Qt.LandscapeOrientation;
7475 }
7576 }
76− shellPrimaryOrientation: Qt.PortraitOrientation
77− nativeOrientation: Qt.PortraitOrientation
77+
78+ orientations: Orientations {
79+ // the default values will do
80+ }
81+
7882 maximizedAppTopMargin: units.gu(3)
7983 Component.onDestruction: {
8084 spreadDelegateLoader.itemDestroyed = true;
tests/qmltests/Stages/tst_TabletStage.qml+2−2
@@ -22,6 +22,7 @@ import Unity.Application 0.1
2222import Unity.Test 0.1
2323
2424import "../../../qml/Stages"
25+import "../../../qml/Components"
2526
2627Rectangle {
2728 id: root
@@ -50,10 +51,9 @@ Rectangle {
5051 maximizedAppTopMargin: units.gu(3) + units.dp(2)
5152 interactive: true
5253 shellOrientation: Qt.LandscapeOrientation
53− shellPrimaryOrientation: Qt.LandscapeOrientation
54− nativeOrientation: Qt.LandscapeOrientation
5554 nativeWidth: width
5655 nativeHeight: height
56+ orientations: Orientations{} // Defaults are fine for testing
5757 focus: true
5858 }
5959 }
tests/qmltests/Stages/tst_WindowResizeArea.qml+35−2
@@ -44,8 +44,11 @@ Item {
4444 width: units.gu(20)
4545 property int windowHeight: height
4646 property int windowWidth: width
47− onWindowHeightChanged: height = windowHeight
48− onWindowWidthChanged: width = windowWidth
47+ state: "normal"
48+
49+ function maximize() {
50+ state = "maximized"
51+ }
4952
5053 WindowResizeArea {
5154 id: windowResizeArea
@@ -195,5 +198,35 @@ Item {
195198 tryCompare(fakeWindow, "width", initialWindowWidth);
196199 tryCompare(fakeWindow, "height", initialWindowHeight);
197200 }
201+
202+ function test_saveRestoreMaximized() {
203+ var initialWindowX = fakeWindow.x;
204+ var initialWindowY = fakeWindow.y;
205+
206+ var moveDelta = units.gu(5);
207+
208+ fakeWindow.x = initialWindowX + moveDelta
209+ fakeWindow.y = initialWindowY + moveDelta
210+
211+ // Now change the state to maximized. The window should not keep updating the stored values
212+ fakeWindow.state = "maximized"
213+ fakeWindow.x = 31415 // 0 is too risky to pass the test even when broken
214+ fakeWindow.y = 31415
215+
216+ // This will destroy the window and recreate it
217+ windowLoader.active = false;
218+ waitForRendering(root);
219+ windowLoader.active = true;
220+
221+ // Make sure it's again where we left it in normal state before destroying
222+ tryCompare(fakeWindow, "x", initialWindowX + moveDelta)
223+ tryCompare(fakeWindow, "y", initialWindowX + moveDelta)
224+
225+ // Make sure maximize() has been called after restoring
226+ tryCompare(fakeWindow, "state", "maximized")
227+
228+ // clean up
229+ fakeWindow.state = "normal"
230+ }
198231 }
199232}
tests/qmltests/tst_OrientedShell.qml+1−1
@@ -840,7 +840,7 @@ Rectangle {
840840 // shell should remain in its primery orientation as the app in the main stage
841841 // is the one that dictates its orientation. In this case it's unity8-dash
842842 // which supports only primary orientation
843− compare(shell.orientation, orientedShell.primaryOrientation);
843+ compare(shell.orientation, orientedShell.orientations.primary);
844844 }
845845
846846 function test_sideStageAppsRemainPortraitInSpread_data() {
tests/qmltests/tst_Shell.qml+179−2
@@ -29,8 +29,11 @@ import Unity.Notifications 1.0
2929import Unity.Test 0.1
3030import Powerd 0.1
3131import Wizard 0.1 as Wizard
32+import Utils 0.1
3233
3334import "../../qml"
35+import "../../qml/Components"
36+import "../../qml/Components/PanelState"
3437import "Stages"
3538
3639Rectangle {
@@ -97,8 +100,7 @@ Rectangle {
97100 id: __shell
98101 usageScenario: usageScenarioSelector.model[usageScenarioSelector.selectedIndex]
99102 orientation: Qt.PortraitOrientation
100− primaryOrientation: Qt.PortraitOrientation
101− nativeOrientation: Qt.PortraitOrientation
103+ orientations: Orientations{} // Defaults are fine for testing
102104 Component.onDestruction: {
103105 shellLoader.itemDestroyed = true;
104106 }
@@ -302,6 +304,7 @@ Rectangle {
302304 mouseEmulation.checked = true;
303305 tryCompare(shell, "enabled", true); // make sure greeter didn't leave us in disabled state
304306 tearDown();
307+ WindowStateStorage.clear();
305308 }
306309
307310 function loadShell(formFactor) {
@@ -377,6 +380,8 @@ Rectangle {
377380 LightDM.Greeter.authenticate(""); // reset greeter
378381
379382 sessionSpy.clear();
383+
384+ GSettingsController.setLifecycleExemptAppids([]);
380385 }
381386
382387 function killApps() {
@@ -1707,5 +1712,177 @@ Rectangle {
17071712
17081713 keyRelease(Qt.Key_Control);
17091714 }
1715+
1716+ // regression test for http://pad.lv/1443319
1717+ function test_closeMaximizedAndRestart() {
1718+ loadDesktopShellWithApps();
1719+
1720+ var appRepeater = findChild(shell, "appRepeater")
1721+ var appId = ApplicationManager.get(0).appId;
1722+ var appDelegate = appRepeater.itemAt(0);
1723+ var maximizeButton = findChild(appDelegate, "maximizeWindowButton");
1724+
1725+ wait(5000)
1726+ tryCompare(appDelegate, "state", "normal");
1727+ tryCompare(PanelState, "buttonsVisible", false)
1728+
1729+ wait(5000)
1730+ mouseClick(maximizeButton, maximizeButton.width / 2, maximizeButton.height / 2);
1731+ tryCompare(appDelegate, "state", "maximized");
1732+ tryCompare(PanelState, "buttonsVisible", true)
1733+
1734+ ApplicationManager.stopApplication(appId);
1735+ tryCompare(PanelState, "buttonsVisible", false)
1736+
1737+ ApplicationManager.startApplication(appId);
1738+ tryCompare(PanelState, "buttonsVisible", true)
1739+ }
1740+
1741+ // bug http://pad.lv/1431566
1742+ function test_switchToStagedHidesPanelButtons() {
1743+ loadDesktopShellWithApps();
1744+ var appRepeater = findChild(shell, "appRepeater")
1745+ var appId = ApplicationManager.get(0).appId;
1746+ var appDelegate = appRepeater.itemAt(0);
1747+ var panelButtons = findChild(shell, "panelWindowControlButtons")
1748+
1749+ tryCompare(appDelegate, "state", "normal");
1750+ tryCompare(panelButtons, "visible", false);
1751+
1752+ appDelegate.maximize(false);
1753+ tryCompare(panelButtons, "visible", true);
1754+
1755+ shell.usageScenario = "phone";
1756+ waitForRendering(shell);
1757+ tryCompare(panelButtons, "visible", false);
1758+
1759+ shell.usageScenario = "desktop";
1760+ waitForRendering(shell);
1761+ tryCompare(panelButtons, "visible", true);
1762+ }
1763+
1764+ function test_lockingGreeterHidesPanelButtons() {
1765+ loadDesktopShellWithApps();
1766+ var appRepeater = findChild(shell, "appRepeater")
1767+ var appId = ApplicationManager.get(0).appId;
1768+ var appDelegate = appRepeater.itemAt(0);
1769+ var panelButtons = findChild(shell, "panelWindowControlButtons")
1770+
1771+ tryCompare(appDelegate, "state", "normal");
1772+ tryCompare(panelButtons, "visible", false);
1773+
1774+ appDelegate.maximize(false);
1775+ tryCompare(panelButtons, "visible", true);
1776+
1777+ LightDM.Greeter.showGreeter();
1778+ waitForRendering(shell);
1779+ tryCompare(panelButtons, "visible", false);
1780+
1781+ LightDM.Greeter.hideGreeter();
1782+ waitForRendering(shell);
1783+ tryCompare(panelButtons, "visible", true);
1784+ }
1785+
1786+ function test_lifecyclePolicyForNonTouchApp_data() {
1787+ return [
1788+ {tag: "phone", formFactor: "phone", usageScenario: "phone"},
1789+ {tag: "tablet", formFactor: "tablet", usageScenario: "tablet"}
1790+ ]
1791+ }
1792+
1793+ function test_lifecyclePolicyForNonTouchApp(data) {
1794+ loadShell(data.formFactor);
1795+ shell.usageScenario = data.usageScenario;
1796+
1797+ var app1 = ApplicationManager.startApplication("libreoffice");
1798+ waitUntilAppWindowIsFullyLoaded(app1);
1799+ var app2 = ApplicationManager.startApplication("dialer-app");
1800+ waitUntilAppWindowIsFullyLoaded(app2);
1801+
1802+ // Make sure app1 is unfocused but still running
1803+ compare(app1.session.surface.activeFocus, false);
1804+ compare(app1.isTouchApp, false); // sanity check our mock, which sets this for us
1805+ compare(app1.requestedState, ApplicationInfoInterface.RequestedRunning);
1806+ }
1807+
1808+ function test_lifecyclePolicyExemption_data() {
1809+ return [
1810+ {tag: "phone", formFactor: "phone", usageScenario: "phone", suspendsApps: true},
1811+ {tag: "tablet", formFactor: "tablet", usageScenario: "tablet", suspendsApps: true}
1812+ ]
1813+ }
1814+
1815+ function test_lifecyclePolicyExemption(data) {
1816+ loadShell(data.formFactor);
1817+ shell.usageScenario = data.usageScenario;
1818+
1819+ GSettingsController.setLifecycleExemptAppids(["webbrowser-app"]);
1820+
1821+ var app1 = ApplicationManager.startApplication("webbrowser-app");
1822+ waitUntilAppWindowIsFullyLoaded(app1);
1823+ var app2 = ApplicationManager.startApplication("dialer-app");
1824+ waitUntilAppWindowIsFullyLoaded(app2);
1825+
1826+ // Make sure app1 is unfocused but still running
1827+ compare(app1.session.surface.activeFocus, false);
1828+ compare(app1.requestedState, ApplicationInfoInterface.RequestedRunning);
1829+ }
1830+
1831+ function test_switchToStagedForcesLegacyAppClosing_data() {
1832+ return [
1833+ {tag: "forceClose", replug: false },
1834+ {tag: "replug", replug: true }
1835+ ];
1836+ }
1837+
1838+ function test_switchToStagedForcesLegacyAppClosing(data) {
1839+ loadShell("desktop")
1840+ shell.usageScenario = "desktop"
1841+ waitForRendering(shell);
1842+
1843+ ApplicationManager.startApplication("camera-app")
1844+
1845+ shell.usageScenario = "phone"
1846+ waitForRendering(shell);
1847+
1848+ // No legacy app running yet... Popup must *not* show.
1849+ var popup = findChild(root, "modeSwitchWarningDialog");
1850+ compare(popup, null);
1851+
1852+ shell.usageScenario = "desktop"
1853+ waitForRendering(shell);
1854+
1855+ // Now start a legacy app
1856+ ApplicationManager.startApplication("libreoffice")
1857+
1858+ shell.usageScenario = "phone"
1859+ waitForRendering(shell);
1860+
1861+ // The popup must appear now
1862+ popup = findChild(root, "modeSwitchWarningDialog");
1863+ compare(popup !== null, true);
1864+
1865+ if (data.replug) {
1866+ shell.usageScenario = "desktop"
1867+ waitForRendering(shell);
1868+
1869+ } else {
1870+ var forceCloseButton = findChild(popup, "forceCloseButton");
1871+ mouseClick(forceCloseButton, forceCloseButton.width / 2, forceCloseButton.height / 2);
1872+ waitForRendering(root);
1873+ }
1874+
1875+ // Popup must be gone now
1876+ popup = findChild(root, "modeSwitchWarningDialog");
1877+ compare(popup === null, true);
1878+
1879+ if (data.replug) {
1880+ // Libreoffice must still be running
1881+ compare(ApplicationManager.findApplication("libreoffice") !== null, true);
1882+ } else {
1883+ // Libreoffice must be gone now
1884+ compare(ApplicationManager.findApplication("libreoffice") === null, true);
1885+ }
1886+ }
17101887 }
17111888}