on xenial help sign in
Tapping on a surface gives it active focus

Which needed a big refactoring of the active focus handling of surfaces. Useful in tablet mode, where you may have two running applications: one on the main stage and the other on the side stage. Also needed for the desktop mode.

commitba68d27c2af172073e0ac6d81b3a85ed3e84a047 authorDaniel d'Andrada <daniel.dandrada@canonical.com> dateWed 28 Jan 2015, 10:59 (12 years ago) parentc780bde
22 files changed+443−49
plugins/Ubuntu/Gestures/TouchGate.cpp+2 plugins/Ubuntu/Gestures/TouchGate.h+2 qml/Shell.qml+10−1 qml/Stages/ApplicationWindow.qml+3−1 qml/Stages/PhoneStage.qml+2 qml/Stages/SessionContainer.qml+22−7 qml/Stages/SpreadDelegate.qml+2−1 qml/Stages/SurfaceContainer.qml+11−25 qml/Stages/TabletStage.qml+19 tests/mocks/Unity/Application/MirSurfaceItem.qml+16−1 tests/qmltests/CMakeLists.txt+1 tests/qmltests/Stages/ApplicationCheckBox.qml+45 tests/qmltests/Stages/RecursingChildSessionControl.qml+3 tests/qmltests/Stages/tst_ApplicationWindow.qml+6−1 tests/qmltests/Stages/tst_PhoneStage.qml+4 tests/qmltests/Stages/tst_SessionContainer.qml+17−7 tests/qmltests/Stages/tst_TabletStage.qml+154 tests/qmltests/tst_Shell.qml+25−5 tests/uqmlscene/ActiveFocusLogger.cpp+48 tests/uqmlscene/ActiveFocusLogger.h+37 tests/uqmlscene/CMakeLists.txt+1 tests/uqmlscene/main.cpp+13
plugins/Ubuntu/Gestures/TouchGate.cpp+2
@@ -53,6 +53,8 @@ void TouchGate::touchEvent(QTouchEvent *event)
5353 m_touchInfoMap[touchPoint.id()].ownership = OwnershipRequested;
5454 m_touchInfoMap[touchPoint.id()].ended = false;
5555 TouchRegistry::instance()->requestTouchOwnership(touchPoint.id(), this);
56+
57+ Q_EMIT pressed();
5658 }
5759
5860 goodToGo &= m_touchInfoMap.contains(touchPoint.id())
plugins/Ubuntu/Gestures/TouchGate.h+2
@@ -53,6 +53,8 @@ public:
5353Q_SIGNALS:
5454 void targetItemChanged(QQuickItem *item);
5555
56+ void pressed();
57+
5658protected:
5759 void touchEvent(QTouchEvent *event) override;
5860private:
qml/Shell.qml+10−1
@@ -287,6 +287,15 @@ Item {
287287 source: usageModeSettings.usageMode === "Windowed" ? "Stages/DesktopStage.qml"
288288 : tabletMode ? "Stages/TabletStage.qml" : "Stages/PhoneStage.qml"
289289
290+ property bool interactive: edgeDemo.stagesEnabled
291+ && !greeter.shown
292+ && !lockscreen.shown
293+ && panel.indicators.fullyClosed
294+ && launcher.progress == 0
295+ && !notifications.useModal
296+
297+ onInteractiveChanged: { if (interactive) { focus = true; } }
298+
290299 Binding {
291300 target: applicationsDisplayLoader.item
292301 property: "objectName"
@@ -306,7 +315,7 @@ Item {
306315 Binding {
307316 target: applicationsDisplayLoader.item
308317 property: "interactive"
309− value: edgeDemo.stagesEnabled && !greeter.shown && !lockscreen.shown && panel.indicators.fullyClosed && launcher.progress == 0 && !notifications.useModal
318+ value: applicationsDisplayLoader.interactive
310319 }
311320 Binding {
312321 target: applicationsDisplayLoader.item
qml/Stages/ApplicationWindow.qml+3−1
@@ -18,7 +18,7 @@ import QtQuick 2.0
1818import Ubuntu.Components 1.1
1919import Unity.Application 0.1
2020
21−Item {
21+FocusScope {
2222 id: root
2323
2424 // to be read from outside
@@ -120,6 +120,8 @@ Item {
120120 d.surfaceInitialized = false;
121121 }
122122 }
123+
124+ focus: true
123125 }
124126
125127 StateGroup {
qml/Stages/PhoneStage.qml+2
@@ -109,6 +109,8 @@ Rectangle {
109109
110110 onFocusedAppIdChanged: focusedAppDelegate = spreadRepeater.itemAt(0);
111111
112+ onFocusedAppDelegateChanged: focusedAppDelegate.focus = true;
113+
112114 function indexOf(appId) {
113115 for (var i = 0; i < ApplicationManager.count; i++) {
114116 if (ApplicationManager.get(i).appId == appId) {
qml/Stages/SessionContainer.qml+22−7
@@ -17,7 +17,7 @@
1717import QtQuick 2.0
1818import "Animations"
1919
20−Item {
20+FocusScope {
2121 id: root
2222 objectName: "sessionContainer"
2323 property QtObject session
@@ -34,8 +34,8 @@ Item {
3434 orientation: root.orientation
3535 }
3636
37−
3837 Repeater {
38+ id: childSessionsRepeater
3939 model: root.childSessions
4040
4141 delegate: Loader {
@@ -45,9 +45,17 @@ Item {
4545 // Only way to do recursive qml items.
4646 source: Qt.resolvedUrl("SessionContainer.qml")
4747
48+ z: index
49+
50+ // Since a Loader is a FocusScope, propagate its focus to the loaded Item
51+ Binding {
52+ target: item; when: item
53+ property: "focus"; value: focus
54+ }
55+
4856 Binding {
4957 target: item; when: item
50− property: "interactive"; value: root.interactive
58+ property: "interactive"; value: index == (childSessionsRepeater.count - 1) && root.interactive
5159 }
5260
5361 Binding {
@@ -69,10 +77,6 @@ Item {
6977 target: item; when: item
7078 property: "orientation"; value: root.orientation
7179 }
72−
73− Component.onDestruction: {
74− root.session.surface.forceActiveFocus();
75− }
7680 }
7781 }
7882
@@ -138,5 +142,16 @@ Item {
138142 QtObject {
139143 id: d
140144 property var animations: []
145+
146+ property var focusedChild: {
147+ if (childSessionsRepeater.count == 0) {
148+ return _surfaceContainer;
149+ } else {
150+ return childSessionsRepeater.itemAt(childSessionsRepeater.count - 1);
151+ }
152+ }
153+ onFocusedChildChanged: {
154+ focusedChild.focus = true;
155+ }
141156 }
142157}
qml/Stages/SpreadDelegate.qml+2−1
@@ -21,7 +21,7 @@ import QtQuick 2.0
2121import Ubuntu.Components 1.1
2222import "../Components"
2323
24−Item {
24+FocusScope {
2525 id: root
2626
2727 // to be read from outside
@@ -65,6 +65,7 @@ Item {
6565 ApplicationWindow {
6666 id: appWindow
6767 objectName: application ? "appWindow_" + application.appId : "appWindow_null"
68+ focus: true
6869 anchors {
6970 fill: parent
7071 topMargin: appWindow.fullscreen ? 0 : maximizedAppTopMargin
qml/Stages/SurfaceContainer.qml+11−25
@@ -18,7 +18,7 @@ import QtQuick 2.0
1818import Ubuntu.Components 1.1
1919import Ubuntu.Gestures 0.1 // For TouchGate
2020
21−Item {
21+FocusScope {
2222 id: root
2323 objectName: "surfaceContainer"
2424 property Item surface: null
@@ -26,10 +26,15 @@ Item {
2626 property int orientation
2727 property bool interactive
2828
29+ signal surfacePressed()
30+
2931 onSurfaceChanged: {
3032 if (surface) {
33+ // Set the surface focus *after* it is added to the scene to
34+ // ensure an update to the scene's active focus.
35+ surface.focus = false;
3136 surface.parent = root;
32− d.forceSurfaceActiveFocusIfReady();
37+ surface.focus = true;
3338 } else {
3439 hadSurface = true;
3540 }
@@ -38,7 +43,6 @@ Item {
3843 Binding { target: surface; property: "orientation"; value: root.orientation }
3944 Binding { target: surface; property: "z"; value: 1 }
4045 Binding { target: surface; property: "enabled"; value: root.interactive; when: surface }
41− Binding { target: surface; property: "focus"; value: root.interactive; when: surface }
4246 Binding { target: surface; property: "antialiasing"; value: !root.interactive; when: surface }
4347
4448 TouchGate {
@@ -46,28 +50,10 @@ Item {
4650 anchors.fill: root
4751 enabled: root.surface ? root.surface.enabled : false
4852 z: 2
49− }
50−
51− Connections {
52− target: root.surface
53− // FIXME: I would rather not need to do this, but currently it doesn't get
54− // active focus without it and I don't know why.
55− // Possibly because if an item get focus=true before it has a parent, once
56− // it gets a parent QQuickWindow won't check its focus and update its activeFocus
57− // accordingly. Unlike when you focus=true after the item already has a parent.
58− onFocusChanged: d.forceSurfaceActiveFocusIfReady();
59− onParentChanged: d.forceSurfaceActiveFocusIfReady();
60− onEnabledChanged: d.forceSurfaceActiveFocusIfReady();
61− }
62−
63− QtObject {
64− id: d
65− function forceSurfaceActiveFocusIfReady() {
66− if (root.surface !== null &&
67− root.surface.focus &&
68− root.surface.parent === root &&
69− root.surface.enabled) {
70− root.surface.forceActiveFocus();
53+ onPressed: {
54+ root.focus = true;
55+ if (root.interactive) {
56+ root.forceActiveFocus();
7157 }
7258 }
7359 }
qml/Stages/TabletStage.qml+19
@@ -76,6 +76,25 @@ Rectangle {
7676
7777 appId0 = ApplicationManager.count >= 1 ? ApplicationManager.get(0).appId : "";
7878 appId1 = ApplicationManager.count > 1 ? ApplicationManager.get(1).appId : "";
79+
80+ // Update the QML focus accordingly
81+ updateSpreadDelegateFocus();
82+ }
83+
84+ function updateSpreadDelegateFocus() {
85+ var appIndex = 0;
86+ var found = false;
87+ while (!found && appIndex < ApplicationManager.count) {
88+ if (ApplicationManager.get(appIndex).appId === priv.focusedAppId) {
89+ spreadRepeater.itemAt(appIndex).focus = true;
90+ found = true;
91+ } else {
92+ ++appIndex;
93+ }
94+ }
95+ if (!found) {
96+ console.warn("TabletStage: Failed to find the SpreadDelegate for appID=" + priv.focusedAppId);
97+ }
7998 }
8099
81100 function indexOf(appId) {
tests/mocks/Unity/Application/MirSurfaceItem.qml+16−1
@@ -44,9 +44,24 @@ Rectangle {
4444 }
4545
4646 Text {
47+ text: surfaceText.text
48+ color: "black"
49+ font: surfaceText.font
50+ fontSizeMode: Text.Fit
51+ minimumPixelSize: 10
52+ verticalAlignment: Text.AlignVCenter
53+ x: surfaceText.x
54+ y: surfaceText.y
55+ width: surfaceText.width
56+ height: surfaceText.height
57+
58+ transform: Translate { x: -2; y: -2 }
59+ }
60+ Text {
61+ id: surfaceText
4762 anchors.fill: parent
4863 text: "SURFACE"
49− color: "yellow"
64+ color: root.parent && root.parent.activeFocus ? "yellow" : "blue"
5065 font.bold: true
5166 fontSizeMode: Text.Fit
5267 minimumPixelSize: 10; font.pixelSize: 200
tests/qmltests/CMakeLists.txt+1
@@ -88,6 +88,7 @@ add_qml_test(Stages PhoneStage ENVIRONMENT "LD_LIBRARY_PATH=${CMAKE_BINARY_DIR}/
8888add_qml_test(Stages SpreadDelegate ENVIRONMENT "LD_LIBRARY_PATH=${CMAKE_BINARY_DIR}/tests/mocks/libusermetrics:${CMAKE_BINARY_DIR}/tests/mocks/LightDM/single")
8989add_qml_test(Stages SurfaceContainer ENVIRONMENT "LD_LIBRARY_PATH=${CMAKE_BINARY_DIR}/tests/mocks/libusermetrics:${CMAKE_BINARY_DIR}/tests/mocks/LightDM/single")
9090add_qml_test(Stages SessionContainer ENVIRONMENT "LD_LIBRARY_PATH=${CMAKE_BINARY_DIR}/tests/mocks/libusermetrics:${CMAKE_BINARY_DIR}/tests/mocks/LightDM/single")
91+add_qml_test(Stages TabletStage)
9192add_qml_test(Stages WindowMoveResizeArea)
9293add_qml_test(Stages Splash)
9394add_qml_test(Wizard Wizard)
tests/qmltests/Stages/ApplicationCheckBox.qmlnew file+45
@@ -0,0 +1,45 @@
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 QtQuick.Layouts 1.1
19+import Ubuntu.Components 1.1
20+import Unity.Application 0.1
21+
22+RowLayout {
23+ id: root
24+ property string appId
25+ property alias checked: checkbox.checked
26+
27+ Layout.fillWidth: true
28+ CheckBox {
29+ id: checkbox
30+ checked: false
31+ activeFocusOnPress: false
32+ onCheckedChanged: {
33+ if (checked) {
34+ ApplicationManager.startApplication(root.appId);
35+ } else {
36+ ApplicationManager.stopApplication(root.appId);
37+ }
38+ }
39+ }
40+ Label {
41+ text: root.appId
42+ color: "white"
43+ anchors.verticalCenter: parent.verticalCenter
44+ }
45+}
tests/qmltests/Stages/RecursingChildSessionControl.qml+3
@@ -89,6 +89,7 @@ ColumnLayout {
8989 CheckBox {
9090 id: _surfaceCheckbox;
9191 checked: false;
92+ activeFocusOnPress: false
9293 enabled: root.session
9394 onCheckedChanged: {
9495 if (checked) {
@@ -117,6 +118,7 @@ ColumnLayout {
117118
118119 Button {
119120 enabled: root.session
121+ activeFocusOnPress: false
120122 text: removable ? "Remove" : "Release"
121123 onClicked: {
122124 if (removable) {
@@ -131,6 +133,7 @@ ColumnLayout {
131133
132134 Button {
133135 enabled: root.session !== null
136+ activeFocusOnPress: false
134137 text: "Add Child"
135138 onClicked: {
136139 var screenshot = Math.round(Math.random()*100 % (screenshotIds.length-1));
tests/qmltests/Stages/tst_ApplicationWindow.qml+6−1
@@ -52,10 +52,12 @@ Rectangle {
5252 application: fakeApplication
5353 orientation: Qt.PortraitOrientation
5454 interactive: true
55+ focus: true
5556 }
5657 }
5758 Loader {
5859 id: applicationWindowLoader
60+ focus: true
5961 anchors {
6062 top: parent.top
6163 bottom: parent.bottom
@@ -83,6 +85,7 @@ Rectangle {
8385
8486 CheckBox {
8587 id: sessionCheckbox; checked: false
88+ activeFocusOnPress: false
8689 onCheckedChanged: {
8790 if (applicationWindowLoader.status !== Loader.Ready)
8891 return;
@@ -121,6 +124,7 @@ Rectangle {
121124
122125 ListItem.ItemSelector {
123126 id: appStateSelector
127+ activeFocusOnPress: false
124128 anchors { left: parent.left; right: parent.right }
125129 text: "Application state"
126130 model: ["Starting",
@@ -146,6 +150,7 @@ Rectangle {
146150
147151 Button {
148152 anchors { left: parent.left; right: parent.right }
153+ activeFocusOnPress: false
149154 text: "Rotate device \u27F3"
150155 onClicked: {
151156 var orientation = applicationWindowLoader.item.orientation
@@ -398,7 +403,7 @@ Rectangle {
398403 verify(stateGroup.state === "void");
399404 }
400405
401− function test_forceActiveFocusFollowsInterative() {
406+ function test_surfaceActiveFocusFollowsAppWindowInterative() {
402407 fakeApplication.createSession();
403408 applicationWindowLoader.item.interactive = false;
404409 applicationWindowLoader.item.interactive = true;
tests/qmltests/Stages/tst_PhoneStage.qml+4
@@ -29,6 +29,7 @@ Item {
2929 PhoneStage {
3030 id: phoneStage
3131 anchors { fill: parent; rightMargin: units.gu(30) }
32+ focus: true
3233 dragAreaWidth: units.gu(2)
3334 maximizedAppTopMargin: units.gu(3) + units.dp(2)
3435 interactive: true
@@ -51,6 +52,7 @@ Item {
5152 Button {
5253 anchors { left: parent.left; right: parent.right }
5354 text: "Add App"
55+ activeFocusOnPress: false
5456 onClicked: {
5557 testCase.addApps();
5658 }
@@ -58,6 +60,7 @@ Item {
5860 Button {
5961 anchors { left: parent.left; right: parent.right }
6062 text: "Remove Selected"
63+ activeFocusOnPress: false
6164 onClicked: {
6265 ApplicationManager.stopApplication(ApplicationManager.get(appList.selectedAppIndex).appId);
6366 }
@@ -65,6 +68,7 @@ Item {
6568 Button {
6669 anchors { left: parent.left; right: parent.right }
6770 text: "Stop Selected"
71+ activeFocusOnPress: false
6872 onClicked: {
6973 ApplicationManager.get(appList.selectedAppIndex).setState(ApplicationInfoInterface.Stopped);
7074 }
tests/qmltests/Stages/tst_SessionContainer.qml+17−7
@@ -42,11 +42,14 @@ Rectangle {
4242 id: sessionContainer
4343 anchors.fill: parent
4444 orientation: Qt.PortraitOrientation
45+ focus: true
46+ interactive: true
4547 }
4648 }
4749
4850 Loader {
4951 id: sessionContainerLoader
52+ focus: true
5053 anchors {
5154 top: parent.top
5255 bottom: parent.bottom
@@ -75,6 +78,7 @@ Rectangle {
7578 CheckBox {
7679 id: sessionCheckbox;
7780 checked: false;
81+ activeFocusOnPress: false
7882 onCheckedChanged: {
7983 if (sessionContainerLoader.status !== Loader.Ready)
8084 return;
@@ -190,8 +194,6 @@ Rectangle {
190194 var sessionContainer = sessionContainerLoader.item;
191195 compare(sessionContainer.childSessions.count(), 0);
192196
193− sessionContainer.interactive = true;
194−
195197 var i;
196198 var sessions = [];
197199 // 3 sessions should cover all edge cases
@@ -209,14 +211,22 @@ Rectangle {
209211
210212 var a_session;
211213 while(a_session = sessions.pop()) {
212− a_session.surface.forceActiveFocus();
214+ //a_session.surface.forceActiveFocus();
213215 compare(a_session.surface.activeFocus, true);
214216
215− var parentSession = a_session.parentSession;
216− sessionContainer.session.removeChildSession(a_session);
217− compare(a_session.surface.activeFocus, false);
217+ ApplicationTest.removeSurface(a_session.surface);
218+ ApplicationTest.removeSession(a_session);
219+
220+ //compare(parentSession.surface.activeFocus, true);
218221
219− compare(parentSession.surface.activeFocus, true);
222+ if (sessions.length > 0) {
223+ // active focus should have gone to the yongest remaining sibling
224+ var previousSiblingSurface = sessions[sessions.length - 1].surface
225+ tryCompare(previousSiblingSurface, "activeFocus", true);
226+ } else {
227+ // active focus should have gone to the parent surface
228+ tryCompare(sessionContainer.session.surface, "activeFocus", true);
229+ }
220230 }
221231 }
222232
tests/qmltests/Stages/tst_TabletStage.qmlnew file+154
@@ -0,0 +1,154 @@
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: "grey"
29+ width: tabletStageLoader.width + controls.width
30+ height: tabletStageLoader.height
31+
32+ Loader {
33+ id: tabletStageLoader
34+
35+ x: ((root.width - controls.width) - width) / 2
36+ y: (root.height - height) / 2
37+ width: units.gu(160*0.7)
38+ height: units.gu(100*0.7)
39+
40+ focus: true
41+
42+ property bool itemDestroyed: false
43+ sourceComponent: Component {
44+ TabletStage {
45+ anchors.fill: parent
46+ Component.onDestruction: {
47+ tabletStageLoader.itemDestroyed = true;
48+ }
49+ dragAreaWidth: units.gu(2)
50+ maximizedAppTopMargin: units.gu(3) + units.dp(2)
51+ interactive: true
52+ focus: true
53+ }
54+ }
55+ }
56+
57+ Rectangle {
58+ id: controls
59+ color: "darkgrey"
60+ width: units.gu(30)
61+ anchors {
62+ top: parent.top
63+ bottom: parent.bottom
64+ right: parent.right
65+ }
66+
67+ Column {
68+ anchors { left: parent.left; right: parent.right; top: parent.top; margins: units.gu(1) }
69+ spacing: units.gu(1)
70+ ApplicationCheckBox {
71+ id: webbrowserCheckBox
72+ appId: "webbrowser-app"
73+ }
74+ ApplicationCheckBox {
75+ id: dialerCheckBox
76+ appId: "dialer-app"
77+ }
78+ }
79+ }
80+
81+ UnityTestCase {
82+ id: testCase
83+ name: "TabletStage"
84+ when: windowShown
85+
86+ property Item tabletStage: tabletStageLoader.status === Loader.Ready ? tabletStageLoader.item : null
87+
88+ function init() {
89+ tabletStageLoader.active = true;
90+ tryCompare(tabletStageLoader, "status", Loader.Ready);
91+ }
92+
93+ function cleanup() {
94+ tabletStageLoader.itemDestroyed = false;
95+ tabletStageLoader.active = false;
96+
97+ tryCompare(tabletStageLoader, "status", Loader.Null);
98+ tryCompare(tabletStageLoader, "item", null);
99+ // Loader.status might be Loader.Null and Loader.item might be null but the Loader
100+ // actually took place. Likely because Loader waits until the next event loop
101+ // iteration to do its work. So to ensure the reload, we will wait until the
102+ // Shell instance gets destroyed.
103+ tryCompare(tabletStageLoader, "itemDestroyed", true);
104+
105+ // kill all (fake) running apps
106+ webbrowserCheckBox.checked = false;
107+ dialerCheckBox.checked = false;
108+ }
109+
110+ function waitUntilAppSurfaceShowsUp(appId) {
111+ var appWindow = findChild(tabletStage, "appWindow_" + appId);
112+ verify(appWindow);
113+ var appWindowStates = findInvisibleChild(appWindow, "applicationWindowStateGroup");
114+ verify(appWindowStates);
115+ tryCompare(appWindowStates, "state", "surface");
116+ }
117+
118+ function test_tappingSwitchesFocusBetweenStages() {
119+ webbrowserCheckBox.checked = true;
120+ waitUntilAppSurfaceShowsUp(webbrowserCheckBox.appId);
121+ var webbrowserApp = ApplicationManager.findApplication(webbrowserCheckBox.appId);
122+ compare(webbrowserApp.stage, ApplicationInfoInterface.MainStage);
123+ tryCompare(webbrowserApp.session.surface, "activeFocus", true);
124+
125+ dialerCheckBox.checked = true;
126+ waitUntilAppSurfaceShowsUp(dialerCheckBox.appId);
127+ var dialerApp = ApplicationManager.findApplication(dialerCheckBox.appId);
128+ compare(dialerApp.stage, ApplicationInfoInterface.SideStage);
129+ tryCompare(dialerApp.session.surface, "activeFocus", true);
130+ tryCompare(webbrowserApp.session.surface, "activeFocus", false);
131+
132+ // Tap on the main stage application and check if the focus
133+ // has been passed to it.
134+
135+ var webbrowserWindow = findChild(tabletStage, "appWindow_" + webbrowserApp.appId);
136+ verify(webbrowserWindow);
137+ tap(webbrowserWindow);
138+
139+ tryCompare(dialerApp.session.surface, "activeFocus", false);
140+ tryCompare(webbrowserApp.session.surface, "activeFocus", true);
141+
142+ // Now tap on the side stage application and check if the focus
143+ // has been passed back to it.
144+
145+ var dialerWindow = findChild(tabletStage, "appWindow_" + dialerApp.appId);
146+ verify(dialerWindow);
147+ tap(dialerWindow);
148+
149+ tryCompare(dialerApp.session.surface, "activeFocus", true);
150+ tryCompare(webbrowserApp.session.surface, "activeFocus", false);
151+ }
152+ }
153+
154+}
tests/qmltests/tst_Shell.qml+25−5
@@ -56,6 +56,7 @@ Item {
5656 anchors.fill: parent
5757 Loader {
5858 id: shellLoader
59+ focus: true
5960
6061 property bool itemDestroyed: false
6162 sourceComponent: Component {
@@ -81,6 +82,7 @@ Item {
8182 anchors { left: parent.left; right: parent.right }
8283 Button {
8384 text: "Show Greeter"
85+ activeFocusOnPress: false
8486 onClicked: {
8587 if (shellLoader.status !== Loader.Ready)
8688 return;
@@ -427,11 +429,11 @@ Item {
427429 waitUntilTransitionsEnd(appWindowStateGroup);
428430 }
429431
430− function test_surfaceLosesFocusWhilePanelIsOpen() {
432+ function test_surfaceLosesActiveFocusWhilePanelIsOpen() {
431433 var app = ApplicationManager.startApplication("dialer-app");
432434 waitUntilAppWindowIsFullyLoaded(app);
433435
434− tryCompare(app.session.surface, "focus", true);
436+ tryCompare(app.session.surface, "activeFocus", true);
435437
436438 // Drag the indicators panel half-open
437439 var touchX = shell.width / 2;
@@ -442,7 +444,7 @@ Item {
442444 true /* beginTouch */, false /* endTouch */);
443445 verify(indicators.partiallyOpened);
444446
445− tryCompare(app.session.surface, "focus", false);
447+ tryCompare(app.session.surface, "activeFocus", false);
446448
447449 // And finish getting it open
448450 touchFlick(indicators,
@@ -451,11 +453,21 @@ Item {
451453 false /* beginTouch */, true /* endTouch */);
452454 tryCompare(indicators, "fullyOpened", true);
453455
454− tryCompare(app.session.surface, "focus", false);
456+ tryCompare(app.session.surface, "activeFocus", false);
455457
456458 dragToCloseIndicatorsPanel();
457459
458− tryCompare(app.session.surface, "focus", true);
460+ tryCompare(app.session.surface, "activeFocus", true);
461+ }
462+
463+ function test_launchedAppHasActiveFocus() {
464+ var dialerApp = ApplicationManager.startApplication("dialer-app");
465+ verify(dialerApp);
466+ waitUntilAppSurfaceShowsUp("dialer-app")
467+
468+ verify(dialerApp.session.surface);
469+
470+ tryCompare(dialerApp.session.surface, "activeFocus", true);
459471 }
460472
461473 // Wait for the whole UI to settle down
@@ -470,6 +482,14 @@ Item {
470482 waitForRendering(shell)
471483 }
472484
485+ function waitUntilAppSurfaceShowsUp(appId) {
486+ var appWindow = findChild(shell, "appWindow_" + appId);
487+ verify(appWindow);
488+ var appWindowStates = findInvisibleChild(appWindow, "applicationWindowStateGroup");
489+ verify(appWindowStates);
490+ tryCompare(appWindowStates, "state", "surface");
491+ }
492+
473493 function dragToCloseIndicatorsPanel() {
474494 var indicators = findChild(shell, "indicators");
475495
tests/uqmlscene/ActiveFocusLogger.cppnew file+48
@@ -0,0 +1,48 @@
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+#include "ActiveFocusLogger.h"
18+
19+#include <QDebug>
20+#include <QQuickItem>
21+
22+void ActiveFocusLogger::setWindow(QQuickWindow *window)
23+{
24+ m_window = window;
25+ QObject::connect(window, &QQuickWindow::activeFocusItemChanged,
26+ this, &ActiveFocusLogger::printActiveFocusInfo);
27+}
28+
29+void ActiveFocusLogger::printActiveFocusInfo()
30+{
31+ if (!m_window) {
32+ return;
33+ }
34+
35+ qDebug() << "============== Active focus info START ================";
36+ if (m_window->activeFocusItem()) {
37+ qDebug() << m_window->activeFocusItem();
38+ qDebug() << "Ancestry:";
39+ QQuickItem *item = m_window->activeFocusItem()->parentItem();
40+ while (item != nullptr) {
41+ qDebug() << item << ", isFocusScope =" << item->isFocusScope();
42+ item = item->parentItem();
43+ }
44+ } else {
45+ qDebug() << "NULL";
46+ }
47+ qDebug() << "============== Active focus info END ================";
48+}
tests/uqmlscene/ActiveFocusLogger.hnew file+37
@@ -0,0 +1,37 @@
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 ACTIVE_FOCUS_LOGGER_H
18+#define ACTIVE_FOCUS_LOGGER_H
19+
20+#include <QObject>
21+#include <QQuickWindow>
22+#include <QPointer>
23+
24+class ActiveFocusLogger : public QObject {
25+ Q_OBJECT
26+
27+public:
28+ void setWindow(QQuickWindow *window);
29+
30+private Q_SLOTS:
31+ void printActiveFocusInfo();
32+
33+private:
34+ QPointer<QQuickWindow> m_window;
35+};
36+
37+#endif // ACTIVE_FOCUS_LOGGER_H
tests/uqmlscene/CMakeLists.txt+1
@@ -1,5 +1,6 @@
11add_executable(uqmlscene
22 ${shellapplication_MOC_SRCS}
3+ ActiveFocusLogger.cpp
34 main.cpp
45 ${CMAKE_SOURCE_DIR}/src/MouseTouchAdaptor.cpp
56 )
tests/uqmlscene/main.cpp+13
@@ -68,6 +68,12 @@
6868// UbuntuGestures lib
6969#include <TouchRegistry.h>
7070
71+#define UQMLSCENE_DEBUG_ACTIVE_FOCUS 0
72+
73+#if UQMLSCENE_DEBUG_ACTIVE_FOCUS
74+ #include "ActiveFocusLogger.h"
75+#endif
76+
7177#ifdef QML_RUNTIME_TESTING
7278class RenderStatistics
7379{
@@ -378,6 +384,10 @@ int main(int argc, char ** argv)
378384{
379385 Options options;
380386
387+ #if UQMLSCENE_DEBUG_ACTIVE_FOCUS
388+ ActiveFocusLogger activeFocusLogger;
389+ #endif
390+
381391 QStringList imports;
382392 QList<QPair<QString, QString> > bundles;
383393 for (int i = 1; i < argc; ++i) {
@@ -503,6 +513,9 @@ int main(int argc, char ** argv)
503513 QQuickItem *contentItem = qobject_cast<QQuickItem *>(topLevel);
504514 if (contentItem) {
505515 qxView = new QQuickView(&engine, nullptr);
516+ #if UQMLSCENE_DEBUG_ACTIVE_FOCUS
517+ activeFocusLogger.setWindow(qxView);
518+ #endif
506519 TouchRegistry::instance()->setParent(qxView);
507520 qxView->installEventFilter(TouchRegistry::instance());
508521 window = qxView;