on xenial help sign in
DesktopStage - fix focus switch when user taps on window

Also fixes a threading issue in WindowStateStorage where async SQL queries could be run when WindowStateStorage was already destroyed.

commitdf432508012ff854d6dae7cae3cda61421d0f647 authorDaniel d'Andrada <daniel.dandrada@canonical.com> dateFri 13 Mar 2015, 16:01 (12 years ago) parentfe062bb
7 files changed+223−11
plugins/Utils/windowstatestorage.cpp+20−1 plugins/Utils/windowstatestorage.h+4 qml/Stages/DecoratedWindow.qml+5−4 qml/Stages/DesktopStage.qml+14−3 qml/Stages/WindowMoveResizeArea.qml+5−3 tests/qmltests/CMakeLists.txt+1 tests/qmltests/Stages/tst_DesktopStage.qml+174
plugins/Utils/windowstatestorage.cpp+20−1
@@ -18,6 +18,7 @@
1818
1919#include <QtConcurrent>
2020#include <QDebug>
21+#include <QFutureSynchronizer>
2122#include <QSqlQuery>
2223#include <QSqlError>
2324#include <QSqlResult>
@@ -36,6 +37,16 @@ WindowStateStorage::WindowStateStorage(QObject *parent):
3637 initdb();
3738}
3839
40+WindowStateStorage::~WindowStateStorage()
41+{
42+ QFutureSynchronizer<void> futureSync;
43+ for (int i = 0; i < m_asyncQueries.count(); ++i) {
44+ futureSync.addFuture(m_asyncQueries[i]);
45+ }
46+ futureSync.waitForFinished();
47+ m_db.close();
48+}
49+
3950void WindowStateStorage::saveGeometry(const QString &windowId, const QRect &rect)
4051{
4152 QString queryString = QString("INSERT OR REPLACE INTO geometry (windowId, x, y, width, height) values ('%1', '%2', '%3', '%4', '%5');")
@@ -45,7 +56,15 @@ void WindowStateStorage::saveGeometry(const QString &windowId, const QRect &rect
4556 .arg(rect.width())
4657 .arg(rect.height());
4758
48− QtConcurrent::run(executeAsyncQuery, queryString);
59+ QFuture<void> future = QtConcurrent::run(executeAsyncQuery, queryString);
60+ m_asyncQueries.append(future);
61+
62+ QFutureWatcher<void> *futureWatcher = new QFutureWatcher<void>();
63+ futureWatcher->setFuture(future);
64+ connect(futureWatcher, &QFutureWatcher<void>::finished,
65+ this,
66+ [=](){ m_asyncQueries.removeAll(futureWatcher->future());
67+ futureWatcher->deleteLater(); });
4968}
5069
5170void WindowStateStorage::executeAsyncQuery(const QString &queryString)
plugins/Utils/windowstatestorage.h+4
@@ -17,12 +17,14 @@
1717#include <QObject>
1818#include <QSqlDatabase>
1919#include <QMutex>
20+#include <QFuture>
2021
2122class WindowStateStorage: public QObject
2223{
2324 Q_OBJECT
2425public:
2526 WindowStateStorage(QObject *parent = 0);
27+ virtual ~WindowStateStorage();
2628
2729 Q_INVOKABLE void saveGeometry(const QString &windowId, const QRect &rect);
2830 Q_INVOKABLE QRect getGeometry(const QString &windowId, const QRect &defaultValue);
@@ -35,4 +37,6 @@ private:
3537
3638 // NB: This is accessed from threads. Make sure to mutex it.
3739 QSqlDatabase m_db;
40+
41+ QList< QFuture<void> > m_asyncQueries;
3842};
qml/Stages/DecoratedWindow.qml+5−4
@@ -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
@@ -20,13 +20,12 @@ import QtQuick 2.3
2020import Ubuntu.Components 1.1
2121import Unity.Application 0.1
2222
23−Item {
23+FocusScope {
2424 id: root
2525
2626 property alias application: applicationWindow.application
2727 property alias active: decoration.active
2828
29− signal requestFocus();
3029 signal close();
3130 signal maximize();
3231 signal minimize();
@@ -53,8 +52,10 @@ Item {
5352
5453 ApplicationWindow {
5554 id: applicationWindow
55+ objectName: application ? "appWindow_" + application.appId : "appWindow_null"
5656 anchors.fill: parent
5757 anchors.topMargin: units.gu(3)
58− interactive: index == 0
58+ interactive: true
59+ focus: true
5960 }
6061}
qml/Stages/DesktopStage.qml+14−3
@@ -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
@@ -20,14 +20,17 @@ import QtQuick 2.3
2020import Ubuntu.Components 1.1
2121import Unity.Application 0.1
2222import "../Components/PanelState"
23+import Utils 0.1
2324
24−Item {
25+FocusScope {
2526 id: root
2627
2728 anchors.fill: parent
2829
2930 property alias background: wallpaper.source
3031
32+ property var windowStateStorage: WindowStateStorage
33+
3134 CrossFadeImage {
3235 id: wallpaper
3336 anchors.fill: parent
@@ -116,20 +119,28 @@ Item {
116119 ]
117120
118121 WindowMoveResizeArea {
122+ windowStateStorage: root.windowStateStorage
119123 target: appDelegate
120124 minWidth: appDelegate.minWidth
121125 minHeight: appDelegate.minHeight
122126 resizeHandleWidth: units.gu(0.5)
123127 windowId: model.appId // FIXME: Change this to point to windowId once we have such a thing
124128
125− onPressed: ApplicationManager.requestFocusApplication(model.appId)
129+ onPressed: decoratedWindow.focus = true;
126130 }
127131
128132 DecoratedWindow {
133+ id: decoratedWindow
129134 anchors.fill: parent
130135 application: ApplicationManager.get(index)
131136 active: ApplicationManager.focusedApplicationId === model.appId
132137
138+ onFocusChanged: {
139+ if (focus) {
140+ ApplicationManager.requestFocusApplication(model.appId);
141+ }
142+ }
143+
133144 onClose: ApplicationManager.stopApplication(model.appId)
134145 onMaximize: appDelegate.state = (appDelegate.state == "maximized" ? "normal" : "maximized")
135146 onMinimize: appDelegate.state = "minimized"
qml/Stages/WindowMoveResizeArea.qml+5−3
@@ -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
@@ -25,6 +25,8 @@ MouseArea {
2525 anchors.fill: target
2626 anchors.margins: -resizeHandleWidth
2727
28+ property var windowStateStorage: WindowStateStorage
29+
2830 // The target item managed by this. Must be a parent or a sibling
2931 // The area will anchor to it and manage move and resize events
3032 property Item target: null
@@ -48,7 +50,7 @@ MouseArea {
4850 }
4951
5052 Component.onCompleted: {
51− var windowState = WindowStateStorage.getGeometry(root.windowId, Qt.rect(target.x, target.y, target.width, target.height))
53+ var windowState = windowStateStorage.getGeometry(root.windowId, Qt.rect(target.x, target.y, target.width, target.height))
5254 if (windowState !== undefined) {
5355 target.x = windowState.x
5456 target.y = windowState.y
@@ -102,6 +104,6 @@ MouseArea {
102104 }
103105
104106 Component.onDestruction: {
105− WindowStateStorage.saveGeometry(root.windowId, Qt.rect(target.x, target.y, target.width, target.height))
107+ windowStateStorage.saveGeometry(root.windowId, Qt.rect(target.x, target.y, target.width, target.height))
106108 }
107109}
tests/qmltests/CMakeLists.txt+1
@@ -85,6 +85,7 @@ add_qml_test(Panel/Indicators IndicatorsLight ENVIRONMENT "LD_LIBRARY_PATH=${CMA
8585add_qml_test(Panel/Indicators MenuItemFactory IMPORT_PATHS ${CMAKE_BINARY_DIR}/tests/mocks ${qmltest_DEFAULT_IMPORT_PATHS})
8686add_qml_test(Panel/Indicators MessageMenuItemFactory IMPORT_PATHS ${CMAKE_BINARY_DIR}/tests/mocks ${qmltest_DEFAULT_IMPORT_PATHS})
8787add_qml_test(Stages ApplicationWindow)
88+add_qml_test(Stages DesktopStage)
8889add_qml_test(Stages PhoneStage ENVIRONMENT)
8990add_qml_test(Stages SpreadDelegate ENVIRONMENT)
9091add_qml_test(Stages SurfaceContainer ENVIRONMENT)
tests/qmltests/Stages/tst_DesktopStage.qmlnew file+174
@@ -0,0 +1,174 @@
1+/*
2+ * Copyright (C) 2015 Canonical, Ltd.
3+ *
4+ * This program is free software; you can redistribute it and/or modify
5+ * it under the terms of the GNU General Public License as published by
6+ * the Free Software Foundation; version 3.
7+ *
8+ * This program is distributed in the hope that it will be useful,
9+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
10+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+ * GNU General Public License for more details.
12+ *
13+ * You should have received a copy of the GNU General Public License
14+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
15+ */
16+
17+import QtQuick 2.0
18+import QtTest 1.0
19+import Ubuntu.Components 1.1
20+import Ubuntu.Components.ListItems 1.0 as ListItem
21+import Unity.Application 0.1
22+import Unity.Test 0.1
23+
24+import "../../../qml/Stages"
25+
26+Rectangle {
27+ id: root
28+ color: "darkblue"
29+ width: desktopStageLoader.width + controls.width
30+ height: desktopStageLoader.height
31+
32+ QtObject {
33+ id: fakeWindowStateStorage
34+
35+ property var storedGeom: [
36+ ["unity8-dash", Qt.rect(units.gu(2), units.gu(2), units.gu(50), units.gu(50))],
37+ ["webbrowser-app", Qt.rect(units.gu(60), units.gu(2), units.gu(50), units.gu(50))]
38+ ]
39+
40+ function saveGeometry(windowId, geometry) {
41+ for (var i = 0; i < storedGeom.length; ++i) {
42+ if (storedGeom[i][0] === windowId) {
43+ storedGeom[i][1] = geometry;
44+ return;
45+ }
46+ }
47+ // if new
48+ storedGeom[storedGeom.length] = [windowId, geometry];
49+ }
50+ function getGeometry(windowId, defaultGeometry) {
51+ for (var i = 0; i < storedGeom.length; ++i) {
52+ if (storedGeom[i][0] === windowId) {
53+ return storedGeom[i][1];
54+ }
55+ }
56+ return defaultGeometry;
57+ }
58+ }
59+
60+ Loader {
61+ id: desktopStageLoader
62+ x: ((root.width - controls.width) - width) / 2
63+ y: (root.height - height) / 2
64+ width: units.gu(160*0.9)
65+ height: units.gu(100*0.9)
66+
67+ focus: true
68+
69+ property bool itemDestroyed: false
70+ sourceComponent: Component {
71+ DesktopStage {
72+ anchors.fill: parent
73+ windowStateStorage: fakeWindowStateStorage
74+ Component.onDestruction: {
75+ desktopStageLoader.itemDestroyed = true;
76+ }
77+ focus: true
78+ }
79+ }
80+ }
81+
82+ Rectangle {
83+ id: controls
84+ color: "darkgrey"
85+ width: units.gu(30)
86+ anchors {
87+ top: parent.top
88+ bottom: parent.bottom
89+ right: parent.right
90+ }
91+
92+ Column {
93+ anchors { left: parent.left; right: parent.right; top: parent.top; margins: units.gu(1) }
94+ spacing: units.gu(1)
95+ ApplicationCheckBox {
96+ id: webbrowserCheckBox
97+ appId: "webbrowser-app"
98+ }
99+ ApplicationCheckBox {
100+ id: dialerCheckBox
101+ appId: "dialer-app"
102+ }
103+ }
104+ }
105+
106+ UnityTestCase {
107+ id: testCase
108+ name: "DesktopStage"
109+ when: windowShown
110+
111+ property Item desktopStage: desktopStageLoader.status === Loader.Ready ? desktopStageLoader.item : null
112+
113+ function init() {
114+ }
115+
116+ function cleanup() {
117+ desktopStageLoader.itemDestroyed = false;
118+ desktopStageLoader.active = false;
119+
120+ tryCompare(desktopStageLoader, "status", Loader.Null);
121+ tryCompare(desktopStageLoader, "item", null);
122+ // Loader.status might be Loader.Null and Loader.item might be null but the Loader
123+ // actually took place. Likely because Loader waits until the next event loop
124+ // iteration to do its work. So to ensure the reload, we will wait until the
125+ // Shell instance gets destroyed.
126+ tryCompare(desktopStageLoader, "itemDestroyed", true);
127+
128+ // kill all (fake) running apps
129+ webbrowserCheckBox.checked = false;
130+ dialerCheckBox.checked = false;
131+
132+ desktopStageLoader.active = true;
133+ tryCompare(desktopStageLoader, "status", Loader.Ready);
134+ }
135+
136+ function waitUntilAppSurfaceShowsUp(appId) {
137+ var appWindow = findChild(desktopStage, "appWindow_" + appId);
138+ verify(appWindow);
139+ var appWindowStates = findInvisibleChild(appWindow, "applicationWindowStateGroup");
140+ verify(appWindowStates);
141+ tryCompare(appWindowStates, "state", "surface");
142+ }
143+
144+ function rectsOverlap(aLocal, bLocal) {
145+
146+ var a = aLocal.mapToItem(null, 0, 0, aLocal.width, aLocal.height);
147+ var b = bLocal.mapToItem(null, 0, 0, bLocal.width, bLocal.height);
148+
149+ return !((a.y+a.height) < b.y
150+ || a.y > (b.y+b.height)
151+ || (a.x+a.width) < b.x
152+ || a.x > (b.x+b.width));
153+ }
154+
155+ function test_tappingOnWindowChangesFocusedApp() {
156+ webbrowserCheckBox.checked = true;
157+ waitUntilAppSurfaceShowsUp(webbrowserCheckBox.appId);
158+
159+ var webbrowserWindow = findChild(desktopStage, "appWindow_" + webbrowserCheckBox.appId);
160+ verify(webbrowserWindow);
161+ var dashWindow = findChild(desktopStage, "appWindow_unity8-dash");
162+ verify(dashWindow);
163+
164+ // some sanity check
165+ compare(rectsOverlap(dashWindow, webbrowserWindow), false);
166+
167+ tap(dashWindow);
168+ compare(dashWindow.application.session.surface.activeFocus, true);
169+
170+ tap(webbrowserWindow);
171+ compare(webbrowserWindow.application.session.surface.activeFocus, true);
172+ }
173+ }
174+}