on xenial help sign in
DesktopStage - fix focus switch when user taps on window
0533cecDaniel d'Andrada
plugins/Utils/windowstatestorage.cpp+22−1
@@ -18,6 +18,7 @@
18
18
19#include <QtConcurrent>
19#include <QtConcurrent>
20#include <QDebug>
20#include <QDebug>
21+#include <QFutureSynchronizer>
21#include <QSqlQuery>
22#include <QSqlQuery>
22#include <QSqlError>
23#include <QSqlError>
23#include <QSqlResult>
24#include <QSqlResult>
@@ -36,8 +37,20 @@ WindowStateStorage::WindowStateStorage(QObject *parent):
36 initdb();
37 initdb();
37}
38}
38
39
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+
39void WindowStateStorage::saveGeometry(const QString &windowId, const QRect &rect)
50void WindowStateStorage::saveGeometry(const QString &windowId, const QRect &rect)
40{
51{
52+ QMutexLocker mutexLocker(&s_mutex);
53+
41 QString queryString = QString("INSERT OR REPLACE INTO geometry (windowId, x, y, width, height) values ('%1', '%2', '%3', '%4', '%5');")
54 QString queryString = QString("INSERT OR REPLACE INTO geometry (windowId, x, y, width, height) values ('%1', '%2', '%3', '%4', '%5');")
42 .arg(windowId)
55 .arg(windowId)
43 .arg(rect.x())
56 .arg(rect.x())
@@ -45,7 +58,15 @@ void WindowStateStorage::saveGeometry(const QString &windowId, const QRect &rect
45 .arg(rect.width())
58 .arg(rect.width())
46 .arg(rect.height());
59 .arg(rect.height());
47
60
48− QtConcurrent::run(executeAsyncQuery, queryString);
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(); });
49}
70}
50
71
51void WindowStateStorage::executeAsyncQuery(const QString &queryString)
72void WindowStateStorage::executeAsyncQuery(const QString &queryString)
plugins/Utils/windowstatestorage.h+4
@@ -17,12 +17,14 @@
17#include <QObject>
17#include <QObject>
18#include <QSqlDatabase>
18#include <QSqlDatabase>
19#include <QMutex>
19#include <QMutex>
20+#include <QFuture>
20
21
21class WindowStateStorage: public QObject
22class WindowStateStorage: public QObject
22{
23{
23 Q_OBJECT
24 Q_OBJECT
24public:
25public:
25 WindowStateStorage(QObject *parent = 0);
26 WindowStateStorage(QObject *parent = 0);
27+ virtual ~WindowStateStorage();
26
28
27 Q_INVOKABLE void saveGeometry(const QString &windowId, const QRect &rect);
29 Q_INVOKABLE void saveGeometry(const QString &windowId, const QRect &rect);
28 Q_INVOKABLE QRect getGeometry(const QString &windowId, const QRect &defaultValue);
30 Q_INVOKABLE QRect getGeometry(const QString &windowId, const QRect &defaultValue);
@@ -35,4 +37,6 @@ private:
35
37
36 // NB: This is accessed from threads. Make sure to mutex it.
38 // NB: This is accessed from threads. Make sure to mutex it.
37 QSqlDatabase m_db;
39 QSqlDatabase m_db;
40+
41+ QList< QFuture<void> > m_asyncQueries;
38};
42};
qml/Stages/DecoratedWindow.qml+5−4
@@ -1,5 +1,5 @@
1/*
1/*
2− * Copyright (C) 2014 Canonical, Ltd.
2+ * Copyright (C) 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
@@ -20,13 +20,12 @@ import QtQuick 2.3
20import Ubuntu.Components 1.1
20import Ubuntu.Components 1.1
21import Unity.Application 0.1
21import Unity.Application 0.1
22
22
23−Item {
23+FocusScope {
24 id: root
24 id: root
25
25
26 property alias application: applicationWindow.application
26 property alias application: applicationWindow.application
27 property alias active: decoration.active
27 property alias active: decoration.active
28
28
29− signal requestFocus();
30 signal close();
29 signal close();
31 signal maximize();
30 signal maximize();
32 signal minimize();
31 signal minimize();
@@ -53,8 +52,10 @@ Item {
53
52
54 ApplicationWindow {
53 ApplicationWindow {
55 id: applicationWindow
54 id: applicationWindow
55+ objectName: application ? "appWindow_" + application.appId : "appWindow_null"
56 anchors.fill: parent
56 anchors.fill: parent
57 anchors.topMargin: units.gu(3)
57 anchors.topMargin: units.gu(3)
58− interactive: index == 0
58+ interactive: true
59+ focus: true
59 }
60 }
60}
61}
qml/Stages/DesktopStage.qml+15−3
@@ -1,5 +1,5 @@
1/*
1/*
2− * Copyright (C) 2014 Canonical, Ltd.
2+ * Copyright (C) 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
@@ -20,14 +20,17 @@ import QtQuick 2.3
20import Ubuntu.Components 1.1
20import Ubuntu.Components 1.1
21import Unity.Application 0.1
21import Unity.Application 0.1
22import "../Components/PanelState"
22import "../Components/PanelState"
23+import Utils 0.1
23
24
24−Item {
25+FocusScope {
25 id: root
26 id: root
26
27
27 anchors.fill: parent
28 anchors.fill: parent
28
29
29 property alias background: wallpaper.source
30 property alias background: wallpaper.source
30
31
32+ property var windowStateStorage: WindowStateStorage
33+
31 CrossFadeImage {
34 CrossFadeImage {
32 id: wallpaper
35 id: wallpaper
33 anchors.fill: parent
36 anchors.fill: parent
@@ -116,20 +119,29 @@ Item {
116 ]
119 ]
117
120
118 WindowMoveResizeArea {
121 WindowMoveResizeArea {
122+ windowStateStorage: root.windowStateStorage
119 target: appDelegate
123 target: appDelegate
120 minWidth: appDelegate.minWidth
124 minWidth: appDelegate.minWidth
121 minHeight: appDelegate.minHeight
125 minHeight: appDelegate.minHeight
122 resizeHandleWidth: units.gu(0.5)
126 resizeHandleWidth: units.gu(0.5)
123 windowId: model.appId // FIXME: Change this to point to windowId once we have such a thing
127 windowId: model.appId // FIXME: Change this to point to windowId once we have such a thing
124
128
125− onPressed: ApplicationManager.requestFocusApplication(model.appId)
129+ onPressed: decoratedWindow.focus = true;
126 }
130 }
127
131
128 DecoratedWindow {
132 DecoratedWindow {
133+ id: decoratedWindow
134+ objectName: "decoratedWindow_" + appId
129 anchors.fill: parent
135 anchors.fill: parent
130 application: ApplicationManager.get(index)
136 application: ApplicationManager.get(index)
131 active: ApplicationManager.focusedApplicationId === model.appId
137 active: ApplicationManager.focusedApplicationId === model.appId
132
138
139+ onFocusChanged: {
140+ if (focus) {
141+ ApplicationManager.requestFocusApplication(model.appId);
142+ }
143+ }
144+
133 onClose: ApplicationManager.stopApplication(model.appId)
145 onClose: ApplicationManager.stopApplication(model.appId)
134 onMaximize: appDelegate.state = (appDelegate.state == "maximized" ? "normal" : "maximized")
146 onMaximize: appDelegate.state = (appDelegate.state == "maximized" ? "normal" : "maximized")
135 onMinimize: appDelegate.state = "minimized"
147 onMinimize: appDelegate.state = "minimized"
qml/Stages/WindowDecoration.qml+1
@@ -55,6 +55,7 @@ Item {
55
55
56 Label {
56 Label {
57 id: titleLabel
57 id: titleLabel
58+ objectName: "windowDecorationTitle"
58 color: "#DFDBD2"
59 color: "#DFDBD2"
59 height: parent.height
60 height: parent.height
60 verticalAlignment: Text.AlignVCenter
61 verticalAlignment: Text.AlignVCenter
qml/Stages/WindowMoveResizeArea.qml+5−3
@@ -1,5 +1,5 @@
1/*
1/*
2− * Copyright (C) 2014 Canonical, Ltd.
2+ * Copyright (C) 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
@@ -25,6 +25,8 @@ MouseArea {
25 anchors.fill: target
25 anchors.fill: target
26 anchors.margins: -resizeHandleWidth
26 anchors.margins: -resizeHandleWidth
27
27
28+ property var windowStateStorage: WindowStateStorage
29+
28 // The target item managed by this. Must be a parent or a sibling
30 // The target item managed by this. Must be a parent or a sibling
29 // The area will anchor to it and manage move and resize events
31 // The area will anchor to it and manage move and resize events
30 property Item target: null
32 property Item target: null
@@ -48,7 +50,7 @@ MouseArea {
48 }
50 }
49
51
50 Component.onCompleted: {
52 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))
52 if (windowState !== undefined) {
54 if (windowState !== undefined) {
53 target.x = windowState.x
55 target.x = windowState.x
54 target.y = windowState.y
56 target.y = windowState.y
@@ -102,6 +104,6 @@ MouseArea {
102 }
104 }
103
105
104 Component.onDestruction: {
106 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))
106 }
108 }
107}
109}
tests/mocks/Unity/Application/ApplicationManager.h+3−1
@@ -34,6 +34,7 @@ class ApplicationManager : public ApplicationManagerInterface {
34 Q_FLAGS(ExecFlags)
34 Q_FLAGS(ExecFlags)
35
35
36 Q_PROPERTY(bool empty READ isEmpty NOTIFY emptyChanged)
36 Q_PROPERTY(bool empty READ isEmpty NOTIFY emptyChanged)
37+ Q_PROPERTY(QStringList availableApplications READ availableApplications NOTIFY availableApplicationsChanged)
37
38
38 public:
39 public:
39 ApplicationManager(QObject *parent = nullptr);
40 ApplicationManager(QObject *parent = nullptr);
@@ -75,7 +76,7 @@ class ApplicationManager : public ApplicationManagerInterface {
75 void setForceDashActive(bool forceDashActive) override;
76 void setForceDashActive(bool forceDashActive) override;
76
77
77 // Only for testing
78 // Only for testing
78− Q_INVOKABLE QStringList availableApplications();
79+ QStringList availableApplications();
79 Q_INVOKABLE ApplicationInfo* add(QString appId);
80 Q_INVOKABLE ApplicationInfo* add(QString appId);
80
81
81 QModelIndex findIndex(ApplicationInfo* application);
82 QModelIndex findIndex(ApplicationInfo* application);
@@ -85,6 +86,7 @@ class ApplicationManager : public ApplicationManagerInterface {
85 Q_SIGNALS:
86 Q_SIGNALS:
86 void focusRequested(const QString &appId);
87 void focusRequested(const QString &appId);
87 void emptyChanged(bool empty);
88 void emptyChanged(bool empty);
89+ void availableApplicationsChanged(QStringList list);
88
90
89 private Q_SLOTS:
91 private Q_SLOTS:
90 void onWindowCreatedTimerTimeout();
92 void onWindowCreatedTimerTimeout();
tests/qmltests/CMakeLists.txt+1
@@ -83,6 +83,7 @@ add_qml_test(Panel/Indicators IndicatorsLight ENVIRONMENT "LD_LIBRARY_PATH=${CMA
83add_qml_test(Panel/Indicators MenuItemFactory IMPORT_PATHS ${CMAKE_BINARY_DIR}/tests/mocks ${qmltest_DEFAULT_IMPORT_PATHS})
83add_qml_test(Panel/Indicators MenuItemFactory IMPORT_PATHS ${CMAKE_BINARY_DIR}/tests/mocks ${qmltest_DEFAULT_IMPORT_PATHS})
84add_qml_test(Panel/Indicators MessageMenuItemFactory IMPORT_PATHS ${CMAKE_BINARY_DIR}/tests/mocks ${qmltest_DEFAULT_IMPORT_PATHS})
84add_qml_test(Panel/Indicators MessageMenuItemFactory IMPORT_PATHS ${CMAKE_BINARY_DIR}/tests/mocks ${qmltest_DEFAULT_IMPORT_PATHS})
85add_qml_test(Stages ApplicationWindow)
85add_qml_test(Stages ApplicationWindow)
86+add_qml_test(Stages DesktopStage)
86add_qml_test(Stages PhoneStage ENVIRONMENT)
87add_qml_test(Stages PhoneStage ENVIRONMENT)
87add_qml_test(Stages SpreadDelegate ENVIRONMENT)
88add_qml_test(Stages SpreadDelegate ENVIRONMENT)
88add_qml_test(Stages SurfaceContainer ENVIRONMENT)
89add_qml_test(Stages SurfaceContainer ENVIRONMENT)
tests/qmltests/Stages/ApplicationCheckBox.qml+20−1
@@ -24,15 +24,23 @@ RowLayout {
24 property string appId
24 property string appId
25 property alias checked: checkbox.checked
25 property alias checked: checkbox.checked
26
26
27+ enabled: appId !== "unity8-dash"
28+
27 Layout.fillWidth: true
29 Layout.fillWidth: true
28 CheckBox {
30 CheckBox {
29 id: checkbox
31 id: checkbox
30 checked: false
32 checked: false
31 activeFocusOnPress: false
33 activeFocusOnPress: false
34+ property bool noop: false
32 onCheckedChanged: {
35 onCheckedChanged: {
36+ if (noop) {
37+ return;
38+ }
33 if (checked) {
39 if (checked) {
34− ApplicationManager.startApplication(root.appId);
40+ var application = ApplicationManager.startApplication(root.appId);
41+ appConnections.target = application;
35 } else {
42 } else {
43+ appConnections.target = null;
36 ApplicationManager.stopApplication(root.appId);
44 ApplicationManager.stopApplication(root.appId);
37 }
45 }
38 }
46 }
@@ -42,4 +50,15 @@ RowLayout {
42 color: "white"
50 color: "white"
43 anchors.verticalCenter: parent.verticalCenter
51 anchors.verticalCenter: parent.verticalCenter
44 }
52 }
53+
54+ Connections {
55+ id: appConnections
56+ onStateChanged: {
57+ if (target.state == ApplicationInfoInterface.Stopped) {
58+ checkbox.noop = true;
59+ checkbox.checked = false;
60+ checkbox.noop = false;
61+ }
62+ }
63+ }
45}
64}
tests/qmltests/Stages/tst_DesktopStage.qmlnew file+201
@@ -0,0 +1,201 @@
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+ Repeater {
96+ model: ApplicationManager.availableApplications
97+ ApplicationCheckBox {
98+ appId: modelData
99+ }
100+ }
101+ }
102+ }
103+
104+ UnityTestCase {
105+ id: testCase
106+ name: "DesktopStage"
107+ when: windowShown
108+
109+ property Item desktopStage: desktopStageLoader.status === Loader.Ready ? desktopStageLoader.item : null
110+
111+ function init() {
112+ }
113+
114+ function cleanup() {
115+ desktopStageLoader.itemDestroyed = false;
116+ desktopStageLoader.active = false;
117+
118+ tryCompare(desktopStageLoader, "status", Loader.Null);
119+ tryCompare(desktopStageLoader, "item", null);
120+ // Loader.status might be Loader.Null and Loader.item might be null but the Loader
121+ // actually took place. Likely because Loader waits until the next event loop
122+ // iteration to do its work. So to ensure the reload, we will wait until the
123+ // Shell instance gets destroyed.
124+ tryCompare(desktopStageLoader, "itemDestroyed", true);
125+
126+ killAllRunningApps();
127+
128+ desktopStageLoader.active = true;
129+ tryCompare(desktopStageLoader, "status", Loader.Ready);
130+ }
131+
132+ function killAllRunningApps() {
133+ while (ApplicationManager.count > 1) {
134+ var appIndex = ApplicationManager.get(0).appId == "unity8-dash" ? 1 : 0
135+ ApplicationManager.stopApplication(ApplicationManager.get(appIndex).appId);
136+ }
137+ compare(ApplicationManager.count, 1)
138+ }
139+
140+ function waitUntilAppSurfaceShowsUp(appId) {
141+ var appWindow = findChild(desktopStage, "appWindow_" + appId);
142+ verify(appWindow);
143+ var appWindowStates = findInvisibleChild(appWindow, "applicationWindowStateGroup");
144+ verify(appWindowStates);
145+ tryCompare(appWindowStates, "state", "surface");
146+ }
147+
148+ function rectsIntersect(aLocal, bLocal) {
149+
150+ var a = aLocal.mapToItem(null, 0, 0, aLocal.width, aLocal.height);
151+ var b = bLocal.mapToItem(null, 0, 0, bLocal.width, bLocal.height);
152+
153+ return !((a.y+a.height) < b.y
154+ || a.y > (b.y+b.height)
155+ || (a.x+a.width) < b.x
156+ || a.x > (b.x+b.width));
157+ }
158+
159+ function test_tappingOnWindowChangesFocusedApp() {
160+ ApplicationManager.startApplication("webbrowser-app");
161+ waitUntilAppSurfaceShowsUp("webbrowser-app");
162+
163+ var webbrowserWindow = findChild(desktopStage, "appWindow_webbrowser-app");
164+ verify(webbrowserWindow);
165+ var dashWindow = findChild(desktopStage, "appWindow_unity8-dash");
166+ verify(dashWindow);
167+
168+ // some sanity check
169+ compare(rectsIntersect(dashWindow, webbrowserWindow), false);
170+
171+ tap(dashWindow);
172+ compare(dashWindow.application.session.surface.activeFocus, true);
173+
174+ tap(webbrowserWindow);
175+ compare(webbrowserWindow.application.session.surface.activeFocus, true);
176+ }
177+
178+ function test_tappingOnWindowTitleChangesFocusedApp() {
179+ ApplicationManager.startApplication("webbrowser-app");
180+ waitUntilAppSurfaceShowsUp("webbrowser-app");
181+
182+ var webbrowserWindow = findChild(desktopStage, "decoratedWindow_webbrowser-app");
183+ verify(webbrowserWindow);
184+ var webbrowserWindowTitle = findChild(webbrowserWindow, "windowDecorationTitle");
185+ verify(webbrowserWindowTitle);
186+ var dashWindow = findChild(desktopStage, "decoratedWindow_unity8-dash");
187+ verify(dashWindow);
188+ var dashWindowTitle = findChild(dashWindow, "windowDecorationTitle");
189+ verify(dashWindowTitle);
190+
191+ // some sanity check
192+ compare(rectsIntersect(dashWindow, webbrowserWindow), false);
193+
194+ tap(dashWindowTitle);
195+ compare(dashWindow.application.session.surface.activeFocus, true);
196+
197+ tap(webbrowserWindowTitle);
198+ compare(webbrowserWindow.application.session.surface.activeFocus, true);
199+ }
200+ }
201+}
tests/qmltests/Stages/tst_PhoneStage.qml+1−1
@@ -116,7 +116,7 @@ Item {
116 function addApps(count) {
116 function addApps(count) {
117 if (count == undefined) count = 1;
117 if (count == undefined) count = 1;
118 for (var i = 0; i < count; i++) {
118 for (var i = 0; i < count; i++) {
119− var app = ApplicationManager.startApplication(ApplicationManager.availableApplications()[ApplicationManager.count])
119+ var app = ApplicationManager.startApplication(ApplicationManager.availableApplications[ApplicationManager.count])
120 tryCompare(app, "state", ApplicationInfoInterface.Running)
120 tryCompare(app, "state", ApplicationInfoInterface.Running)
121 var spreadView = findChild(phoneStage, "spreadView");
121 var spreadView = findChild(phoneStage, "spreadView");
122 tryCompare(spreadView, "contentX", -spreadView.shift);
122 tryCompare(spreadView, "contentX", -spreadView.shift);