on xenial help sign in
save and restore window size and position
f8ff2cfMichael Zanetti
plugins/Utils/CMakeLists.txt+2−1
@@ -15,6 +15,7 @@ set(QMLPLUGIN_SRC
15 unitymenumodelpaths.cpp
15 unitymenumodelpaths.cpp
16 windowkeysfilter.cpp
16 windowkeysfilter.cpp
17 easingcurve.cpp
17 easingcurve.cpp
18+ windowstatestorage.cpp
18 plugin.cpp
19 plugin.cpp
19 )
20 )
20
21
@@ -30,6 +31,6 @@ target_link_libraries(Utils-qml ${GIO_LDFLAGS})
30# files directly in targets.
31# files directly in targets.
31set_target_properties(Utils-qml PROPERTIES COMPILE_FLAGS -fvisibility=default)
32set_target_properties(Utils-qml PROPERTIES COMPILE_FLAGS -fvisibility=default)
32
33
33−qt5_use_modules(Utils-qml Qml Quick DBus Network Gui)
34+qt5_use_modules(Utils-qml Qml Quick DBus Network Gui Sql Concurrent)
34
35
35add_unity8_plugin(Utils 0.1 Utils TARGETS Utils-qml)
36add_unity8_plugin(Utils 0.1 Utils TARGETS Utils-qml)
plugins/Utils/plugin.cpp+9
@@ -33,6 +33,14 @@
33#include "unitymenumodelpaths.h"
33#include "unitymenumodelpaths.h"
34#include "windowkeysfilter.h"
34#include "windowkeysfilter.h"
35#include "easingcurve.h"
35#include "easingcurve.h"
36+#include "windowstatestorage.h"
37+
38+static QObject *createWindowStateStorage(QQmlEngine *engine, QJSEngine *scriptEngine)
39+{
40+ Q_UNUSED(engine)
41+ Q_UNUSED(scriptEngine)
42+ return new WindowStateStorage();
43+}
36
44
37void UtilsPlugin::registerTypes(const char *uri)
45void UtilsPlugin::registerTypes(const char *uri)
38{
46{
@@ -46,6 +54,7 @@ void UtilsPlugin::registerTypes(const char *uri)
46 qmlRegisterType<GDateTimeFormatter>(uri, 0, 1, "GDateTimeFormatter");
54 qmlRegisterType<GDateTimeFormatter>(uri, 0, 1, "GDateTimeFormatter");
47 qmlRegisterType<EasingCurve>(uri, 0, 1, "EasingCurve");
55 qmlRegisterType<EasingCurve>(uri, 0, 1, "EasingCurve");
48 qmlRegisterType<RelativeTimeFormatter>(uri, 0, 1, "RelativeTimeFormatter");
56 qmlRegisterType<RelativeTimeFormatter>(uri, 0, 1, "RelativeTimeFormatter");
57+ qmlRegisterSingletonType<WindowStateStorage>(uri, 0, 1, "WindowStateStorage", createWindowStateStorage);
49}
58}
50
59
51void UtilsPlugin::initializeEngine(QQmlEngine *engine, const char *uri)
60void UtilsPlugin::initializeEngine(QQmlEngine *engine, const char *uri)
plugins/Utils/windowstatestorage.cppnew file+96
@@ -0,0 +1,96 @@
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 Lesser 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 Lesser General Public License for more details.
12+ *
13+ * You should have received a copy of the GNU Lesser General Public License
14+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
15+ */
16+
17+#include "windowstatestorage.h"
18+
19+#include <QtConcurrent>
20+#include <QDebug>
21+#include <QSqlQuery>
22+#include <QSqlError>
23+#include <QSqlResult>
24+#include <QRect>
25+
26+QMutex WindowStateStorage::s_mutex;
27+
28+WindowStateStorage::WindowStateStorage(QObject *parent):
29+ QObject(parent)
30+{
31+ QString dbPath = QDir::homePath() + "/.cache/unity8/";
32+ m_db = QSqlDatabase::addDatabase("QSQLITE");
33+ QDir dir;
34+ dir.mkpath(dbPath);
35+ m_db.setDatabaseName(dbPath + "windowstatestorage.sqlite");
36+ initdb();
37+}
38+
39+void WindowStateStorage::saveGeometry(const QString &windowId, const QRect &rect)
40+{
41+ QString queryString = QString("INSERT OR REPLACE INTO geometry (windowId, x, y, width, height) values ('%1', '%2', '%3', '%4', '%5');")
42+ .arg(windowId)
43+ .arg(rect.x())
44+ .arg(rect.y())
45+ .arg(rect.width())
46+ .arg(rect.height());
47+
48+ QtConcurrent::run(executeAsyncQuery, queryString);
49+}
50+
51+void WindowStateStorage::executeAsyncQuery(const QString &queryString)
52+{
53+ QMutexLocker l(&s_mutex);
54+ QSqlQuery query;
55+
56+ bool ok = query.exec(queryString);
57+ if (!ok) {
58+ qWarning() << "Error executing query" << queryString
59+ << "Driver error:" << query.lastError().driverText()
60+ << "Database error:" << query.lastError().databaseText();
61+ }
62+}
63+
64+QRect WindowStateStorage::getGeometry(const QString &windowId, const QRect &defaultValue)
65+{
66+ QMutexLocker l(&s_mutex);
67+ QString queryString = QString("SELECT * FROM geometry WHERE windowId = '%1';")
68+ .arg(windowId);
69+ QSqlQuery query;
70+
71+ bool ok = query.exec(queryString);
72+ if (!ok) {
73+ qWarning() << "Error retrieving window state for" << windowId
74+ << "Driver error:" << query.lastError().driverText()
75+ << "Database error:" << query.lastError().databaseText();
76+ return defaultValue;
77+ }
78+ if (!query.first()) {
79+ return defaultValue;
80+ }
81+ return QRect(query.value("x").toInt(), query.value("y").toInt(), query.value("width").toInt(), query.value("height").toInt());
82+}
83+
84+void WindowStateStorage::initdb()
85+{
86+ m_db.open();
87+ if (!m_db.open()) {
88+ qWarning() << "Error opening state database:" << m_db.lastError().driverText() << m_db.lastError().databaseText();
89+ return;
90+ }
91+
92+ if (!m_db.tables().contains("geometry")) {
93+ QSqlQuery query;
94+ query.exec("CREATE TABLE geometry(windowId TEXT UNIQUE, x INTEGER, y INTEGER, width INTEGER, height INTEGER);");
95+ }
96+}
plugins/Utils/windowstatestorage.hnew file+38
@@ -0,0 +1,38 @@
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 Lesser 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 Lesser General Public License for more details.
12+ *
13+ * You should have received a copy of the GNU Lesser General Public License
14+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
15+ */
16+
17+#include <QObject>
18+#include <QSqlDatabase>
19+#include <QMutex>
20+
21+class WindowStateStorage: public QObject
22+{
23+ Q_OBJECT
24+public:
25+ WindowStateStorage(QObject *parent = 0);
26+
27+ Q_INVOKABLE void saveGeometry(const QString &windowId, const QRect &rect);
28+ Q_INVOKABLE QRect getGeometry(const QString &windowId, const QRect &defaultValue);
29+
30+private:
31+ void initdb();
32+
33+ static void executeAsyncQuery(const QString &queryString);
34+ static QMutex s_mutex;
35+
36+ // NB: This is accessed from threads. Make sure to mutex it.
37+ QSqlDatabase m_db;
38+};
qml/Stages/DesktopStage.qml+2−2
@@ -38,8 +38,6 @@ Item {
38 Connections {
38 Connections {
39 target: ApplicationManager
39 target: ApplicationManager
40 onApplicationAdded: {
40 onApplicationAdded: {
41− // Initial placement to avoid having the window decoration behind the panel
42− appRepeater.itemAt(ApplicationManager.count-1).y = units.gu(3)
43 ApplicationManager.requestFocusApplication(ApplicationManager.get(ApplicationManager.count-1).appId)
41 ApplicationManager.requestFocusApplication(ApplicationManager.get(ApplicationManager.count-1).appId)
44 }
42 }
45
43
@@ -91,6 +89,7 @@ Item {
91 delegate: Item {
89 delegate: Item {
92 id: appDelegate
90 id: appDelegate
93 z: ApplicationManager.count - index
91 z: ApplicationManager.count - index
92+ y: units.gu(3)
94 width: units.gu(60)
93 width: units.gu(60)
95 height: units.gu(50)
94 height: units.gu(50)
96
95
@@ -121,6 +120,7 @@ Item {
121 minWidth: appDelegate.minWidth
120 minWidth: appDelegate.minWidth
122 minHeight: appDelegate.minHeight
121 minHeight: appDelegate.minHeight
123 resizeHandleWidth: units.gu(0.5)
122 resizeHandleWidth: units.gu(0.5)
123+ windowId: model.appId // FIXME: Change this to point to windowId once we have such a thing
124
124
125 onPressed: ApplicationManager.requestFocusApplication(model.appId)
125 onPressed: ApplicationManager.requestFocusApplication(model.appId)
126 }
126 }
qml/Stages/WindowMoveResizeArea.qml+17−1
@@ -18,6 +18,7 @@
18
18
19import QtQuick 2.3
19import QtQuick 2.3
20import Ubuntu.Components 1.1
20import Ubuntu.Components 1.1
21+import Utils 0.1
21
22
22MouseArea {
23MouseArea {
23 id: root
24 id: root
@@ -26,7 +27,8 @@ MouseArea {
26
27
27 // The target item managed by this. Must be a parent or a sibling
28 // The target item managed by this. Must be a parent or a sibling
28 // The area will anchor to it and manage move and resize events
29 // The area will anchor to it and manage move and resize events
29− property var target: null
30+ property Item target: null
31+ property string windowId: ""
30 property int resizeHandleWidth: 0
32 property int resizeHandleWidth: 0
31 property int minWidth: 0
33 property int minWidth: 0
32 property int minHeight: 0
34 property int minHeight: 0
@@ -47,6 +49,16 @@ MouseArea {
47
49
48 }
50 }
49
51
52+ Component.onCompleted: {
53+ var windowState = WindowStateStorage.getGeometry(root.windowId, Qt.rect(target.x, target.y, target.width, target.height))
54+ if (windowState !== undefined) {
55+ target.x = windowState.x
56+ target.y = windowState.y
57+ target.width = windowState.width
58+ target.height = windowState.height
59+ }
60+ }
61+
50 onPressed: {
62 onPressed: {
51 priv.startPoint = mapToItem(null, Qt.point(mouse.x, mouse.y)).x;
63 priv.startPoint = mapToItem(null, Qt.point(mouse.x, mouse.y)).x;
52 priv.startWidth = root.width;
64 priv.startWidth = root.width;
@@ -95,4 +107,8 @@ MouseArea {
95 target.y += mouseDiff.y;
107 target.y += mouseDiff.y;
96 }
108 }
97 }
109 }
110+
111+ Component.onDestruction: {
112+ WindowStateStorage.saveGeometry(root.windowId, Qt.rect(target.x, target.y, target.width, target.height))
113+ }
98}
114}
tests/qmltests/Stages/tst_WindowMoveResizeArea.qml+92−18
@@ -1,5 +1,5 @@
1/*
1/*
2− * Copyright 2014 Canonical Ltd.
2+ * Copyright 2014-2015 Canonical Ltd.
3 *
3 *
4 * This program is free software; you can redistribute it and/or modify
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
5 * it under the terms of the GNU General Public License as published by
@@ -29,20 +29,46 @@ Item {
29 height: units.gu(60)
29 height: units.gu(60)
30 width: units.gu(60)
30 width: units.gu(60)
31
31
32− Rectangle {
32+ property var fakeWindow: windowLoader.item
33+
34+ Component {
35+ id: fakeWindowComponent
36+
37+ Item {
33− id: fakeWindow
38+ id: fakeWindow
39+ property alias minWidth: moveResizeArea.minWidth
40+ property alias minHeight: moveResizeArea.minHeight
41+ x: units.gu(20)
42+ y: units.gu(20)
34− height: units.gu(20)
43+ height: units.gu(20)
35− width: units.gu(20)
44+ width: units.gu(20)
36− color: "khaki"
45+
37− WindowMoveResizeArea {
46+ WindowMoveResizeArea {
38− id: moveResizeArea
47+ id: moveResizeArea
39− target: fakeWindow
48+ target: fakeWindow
40− resizeHandleWidth: units.gu(0.5)
49+ resizeHandleWidth: units.gu(0.5)
41− minWidth: units.gu(15)
50+ minWidth: units.gu(15)
42− minHeight: units.gu(10)
51+ minHeight: units.gu(10)
52+ windowId: "test-window-id"
53+ }
54+
55+ Rectangle {
56+ anchors.fill: moveResizeArea
57+ color: "red"
58+ }
59+
60+ Rectangle {
61+ anchors.fill: fakeWindow
62+ color: "blue"
63+ }
43 }
64 }
44 }
65 }
45
66
67+ Loader {
68+ id: windowLoader
69+ sourceComponent: fakeWindowComponent
70+ }
71+
46 UT.UnityTestCase {
72 UT.UnityTestCase {
47 when: windowShown
73 when: windowShown
48
74
@@ -100,8 +126,8 @@ Item {
100 var startDragY = initialWindowY + initialWindowHeight + 1
126 var startDragY = initialWindowY + initialWindowHeight + 1
101 mouseFlick(root, startDragX, startDragY, startDragX + data.dx, startDragY + data.dy, true, true, units.gu(.5), 10);
127 mouseFlick(root, startDragX, startDragY, startDragX + data.dx, startDragY + data.dy, true, true, units.gu(.5), 10);
102
128
103− tryCompare(fakeWindow, "width", Math.max(initialWindowWidth + data.dx, moveResizeArea.minWidth));
129+ tryCompare(fakeWindow, "width", Math.max(initialWindowWidth + data.dx, fakeWindow.minWidth));
104− tryCompare(fakeWindow, "height", Math.max(initialWindowHeight + data.dy, moveResizeArea.minHeight));
130+ tryCompare(fakeWindow, "height", Math.max(initialWindowHeight + data.dy, fakeWindow.minHeight));
105
131
106 compare(fakeWindow.x, initialWindowX);
132 compare(fakeWindow.x, initialWindowX);
107 compare(fakeWindow.y, initialWindowY);
133 compare(fakeWindow.y, initialWindowY);
@@ -126,13 +152,61 @@ Item {
126 var startDragY = initialWindowY - 1
152 var startDragY = initialWindowY - 1
127 mouseFlick(root, startDragX, startDragY, startDragX + data.dx, startDragY + data.dy, true, true, units.gu(.5), 10);
153 mouseFlick(root, startDragX, startDragY, startDragX + data.dx, startDragY + data.dy, true, true, units.gu(.5), 10);
128
154
129− tryCompare(fakeWindow, "width", Math.max(initialWindowWidth - data.dx, moveResizeArea.minWidth));
155+ tryCompare(fakeWindow, "width", Math.max(initialWindowWidth - data.dx, fakeWindow.minWidth));
130− tryCompare(fakeWindow, "height", Math.max(initialWindowHeight - data.dy, moveResizeArea.minHeight));
156+ tryCompare(fakeWindow, "height", Math.max(initialWindowHeight - data.dy, fakeWindow.minHeight));
131
157
132− var maxMoveX = initialWindowWidth - moveResizeArea.minWidth;
158+ var maxMoveX = initialWindowWidth - fakeWindow.minWidth;
133− var maxMoveY = initialWindowHeight - moveResizeArea.minHeight;
159+ var maxMoveY = initialWindowHeight - fakeWindow.minHeight;
134 compare(fakeWindow.x, Math.min(initialWindowX + data.dx, initialWindowX + maxMoveX));
160 compare(fakeWindow.x, Math.min(initialWindowX + data.dx, initialWindowX + maxMoveX));
135 compare(fakeWindow.y, Math.min(initialWindowY + data.dy, initialWindowY + maxMoveY));
161 compare(fakeWindow.y, Math.min(initialWindowY + data.dy, initialWindowY + maxMoveY));
136 }
162 }
163+
164+ function test_saveRestorePosition() {
165+ var initialWindowX = fakeWindow.x;
166+ var initialWindowY = fakeWindow.y;
167+ var initialWindowWidth = fakeWindow.width;
168+ var initialWindowHeight = fakeWindow.height;
169+
170+ var moveDelta = units.gu(5);
171+ var startDragX = initialWindowX + fakeWindow.width / 2;
172+ var startDragY = initialWindowY + fakeWindow.height / 2;
173+ mouseFlick(root, startDragX, startDragY, startDragX + moveDelta, startDragY + moveDelta, true, true, units.gu(.5), 10)
174+
175+ tryCompare(fakeWindow, "x", initialWindowX + moveDelta)
176+ tryCompare(fakeWindow, "y", initialWindowX + moveDelta)
177+
178+ // This will destroy the window and recreate it
179+ windowLoader.active = false;
180+ waitForRendering(root);
181+ windowLoader.active = true;
182+
183+ // Make sure it's again where we left it before destroying
184+ tryCompare(fakeWindow, "x", initialWindowX + moveDelta)
185+ tryCompare(fakeWindow, "y", initialWindowX + moveDelta)
186+ }
187+
188+ function test_saveRestoreSize() {
189+ var initialWindowX = fakeWindow.x;
190+ var initialWindowY = fakeWindow.y;
191+ var initialWindowWidth = fakeWindow.width
192+ var initialWindowHeight = fakeWindow.height
193+
194+ var resizeDelta = units.gu(5)
195+ var startDragX = initialWindowX + initialWindowWidth + 1
196+ var startDragY = initialWindowY + initialWindowHeight + 1
197+ mouseFlick(root, startDragX, startDragY, startDragX + resizeDelta, startDragY + resizeDelta, true, true, units.gu(.5), 10);
198+
199+ tryCompare(fakeWindow, "width", Math.max(initialWindowWidth + resizeDelta, fakeWindow.minWidth));
200+ tryCompare(fakeWindow, "height", Math.max(initialWindowHeight + resizeDelta, fakeWindow.minHeight));
201+
202+ // This will destroy the window and recreate it
203+ windowLoader.active = false;
204+ waitForRendering(root);
205+ windowLoader.active = true;
206+
207+ // Make sure its size is again the same as before
208+ tryCompare(fakeWindow, "width", Math.max(initialWindowWidth + resizeDelta, fakeWindow.minWidth));
209+ tryCompare(fakeWindow, "height", Math.max(initialWindowHeight + resizeDelta, fakeWindow.minHeight));
210+ }
137 }
211 }
138}
212}