on xenial help sign in
Initial support for child windows (menus, dialogs, tooltips) (LP: #1543467, #1591384, #1656727)

Approved by: Gerry Boland

commit96917ee23a8250fda577f059785bae1c55c39037 authorDaniel d'Andrada <daniel.dandrada@canonical.com> dateMon 6 Feb 2017, 09:11 (10 years ago) parentafa296df2ff1f0
31 files changed+726−108
CMakeLists.txt+1−1 debian/control+3−3 plugins/WindowManager/TopLevelWindowModel.cpp+36−28 plugins/WindowManager/TopLevelWindowModel.h+2 plugins/WindowManager/Window.h−1 qml/Components/WindowControlButtons.qml+2 qml/Stage/ChildWindow.qml+159 qml/Stage/ChildWindowRepeater.qml+28 qml/Stage/ChildWindowTree.qml+127 qml/Stage/DecoratedWindow.qml+1−2 qml/Stage/MoveHandler.qml+23−18 qml/Stage/Stage.qml+50−9 qml/Stage/WindowControlsOverlay.qml+21−24 qml/Stage/WindowDecoration.qml+1 qml/Stage/WindowResizeArea.qml+3−2 tests/mocks/Unity/Application/ApplicationInfo.cpp+9−1 tests/mocks/Unity/Application/ApplicationInfo.h+3 tests/mocks/Unity/Application/ApplicationManager.cpp+9 tests/mocks/Unity/Application/MirSurface.cpp+43 tests/mocks/Unity/Application/MirSurface.h+11 tests/mocks/Unity/Application/MirSurfaceItem.cpp+19−4 tests/mocks/Unity/Application/MirSurfaceListModel.h−1 tests/mocks/Unity/Application/SurfaceManager.cpp+15−8 tests/mocks/Unity/Application/SurfaceManager.h+3−1 tests/mocks/Unity/Application/VirtualKeyboard.cpp+5−4 tests/mocks/Unity/Application/resources/Kate.qml+49 tests/mocks/Unity/Application/resources/KateDialog.qml+48 tests/mocks/Unity/Application/resources/KateMenu.qml+41 tests/mocks/Unity/Application/resources/surfaces.qrc+3 tests/qmltests/Stage/ApplicationCheckBox.qml+1−1 tests/qmltests/Stage/tst_WindowResizeArea.qml+10
CMakeLists.txt+1−1
@@ -70,7 +70,7 @@ find_package(Qt5DBus 5.6 REQUIRED)
7070find_package(Qt5Concurrent 5.6 REQUIRED)
7171find_package(Qt5Sql 5.6 REQUIRED)
7272
73−pkg_check_modules(APPLICATION_API REQUIRED unity-shell-application=25)
73+pkg_check_modules(APPLICATION_API REQUIRED unity-shell-application=26)
7474pkg_check_modules(GEONAMES REQUIRED geonames>=0.2)
7575pkg_check_modules(GIO REQUIRED gio-2.0>=2.32)
7676pkg_check_modules(GLIB REQUIRED glib-2.0>=2.32)
debian/control+3−3
@@ -38,7 +38,7 @@ Build-Depends: cmake,
3838 libubuntugestures5-private-dev (>= 1.3.2030),
3939 libudev-dev,
4040 libudm-common-dev,
41− libunity-api-dev (>= 8.1),
41+ libunity-api-dev (>= 8.2),
4242 libusermetricsoutput1-dev,
4343# Need those X11 libs touch emulation from mouse events in manual QML tests on a X11 desktop
4444 libx11-dev[!arm64 !armhf],
@@ -162,7 +162,7 @@ Depends: qml-module-qtquick-layouts,
162162 qttranslations5-l10n,
163163 ubuntu-thumbnailer-impl-0,
164164 ubuntu-wallpapers,
165− unity-application-impl-23,
165+ unity-application-impl-26,
166166 unity-notifications-impl-3,
167167 unity-plugin-scopes | unity-scopes-impl,
168168 unity-scopes-impl-12,
@@ -212,7 +212,7 @@ Depends: dbus-test-runner,
212212 ${misc:Depends},
213213 ${shlibs:Depends},
214214Provides: unity-application-impl,
215− unity-application-impl-23,
215+ unity-application-impl-26,
216216 unity8-fake-env,
217217Replaces: unity8-autopilot (<< 8.02+15.04.20150422-0ubuntu1),
218218 unity8-fake-env,
plugins/WindowManager/TopLevelWindowModel.cpp+36−28
@@ -176,18 +176,13 @@ void TopLevelWindowModel::prependSurfaceHelper(unityapi::MirSurfaceInterface *su
176176 // No point in signaling anything if we're resetting the whole model
177177 }
178178
179− int id = generateId();
180− Window *window = new Window(id, this);
181− if (surface) {
182− window->setSurface(surface);
183− }
179+ Window *window = createWindow(surface);
180+
184181 m_windowModel.prepend(ModelEntry(window, application));
185182 if (surface) {
186183 connectSurface(surface);
187184 }
188185
189− connectWindow(window);
190−
191186 if (m_modelState == InsertingState) {
192187 endInsertRows();
193188 Q_EMIT countChanged();
@@ -322,34 +317,47 @@ void TopLevelWindowModel::onSurfaceDestroyed(unityapi::MirSurfaceInterface *surf
322317 }
323318}
324319
320+Window *TopLevelWindowModel::createWindow(unityapi::MirSurfaceInterface *surface)
321+{
322+ int id = generateId();
323+ Window *qmlWindow = new Window(id, this);
324+ connectWindow(qmlWindow);
325+ if (surface) {
326+ qmlWindow->setSurface(surface);
327+ }
328+ return qmlWindow;
329+}
330+
325331void TopLevelWindowModel::onSurfaceCreated(unityapi::MirSurfaceInterface *surface)
326332{
327333 DEBUG_MSG << "(" << surface << ")";
328− if (surface->type() == Mir::InputMethodType) {
329− int id = generateId();
330− Window *qmlWindow = new Window(id, this);
331− connectWindow(qmlWindow);
332− qmlWindow->setSurface(surface);
333− setInputMethodWindow(qmlWindow);
334+
335+ if (surface->parentSurface()) {
336+ // Wrap it in a Window so that we keep focusedWindow() up to date.
337+ Window *window = createWindow(surface);
338+ connect(surface, &QObject::destroyed, window, [=](){
339+ window->setSurface(nullptr);
340+ window->deleteLater();
341+ });
334342 } else {
335− auto application = m_applicationManager->findApplicationWithSurface(surface);
336− if (application) {
337− prependSurface(surface, application);
343+ if (surface->type() == Mir::InputMethodType) {
344+ setInputMethodWindow(createWindow(surface));
338345 } else {
339− // Must be a prompt session. No need to do add it as a prompt surface is not top-level.
340− // It will show up in the ApplicationInfoInterface::promptSurfaceList of some application.
341− // Still wrap it in a Window though, so that we keep focusedWindow() up to date.
342− int id = generateId();
343− Window *promptWindow = new Window(id, this);
344− connectWindow(promptWindow);
345− promptWindow->setSurface(surface);
346− connect(surface, &QObject::destroyed, promptWindow, [=](){
347− promptWindow->setSurface(nullptr);
348− promptWindow->deleteLater();
349− });
346+ auto *application = m_applicationManager->findApplicationWithSurface(surface);
347+ if (application) {
348+ prependSurface(surface, application);
349+ } else {
350+ // Must be a prompt session. No need to do add it as a prompt surface is not top-level.
351+ // It will show up in the ApplicationInfoInterface::promptSurfaceList of some application.
352+ // Still wrap it in a Window though, so that we keep focusedWindow() up to date.
353+ Window *promptWindow = createWindow(surface);
354+ connect(surface, &QObject::destroyed, promptWindow, [=](){
355+ promptWindow->setSurface(nullptr);
356+ promptWindow->deleteLater();
357+ });
358+ }
350359 }
351360 }
352− // TODO: handle surfaces that are neither top-level windows nor input method. eg: child dialogs, popups, menus
353361}
354362
355363void TopLevelWindowModel::removeAt(int index)
plugins/WindowManager/TopLevelWindowModel.h+2
@@ -220,6 +220,8 @@ private:
220220
221221 void activateTopMostWindowWithoutId(int forbiddenId);
222222
223+ Window *createWindow(unity::shell::application::MirSurfaceInterface *surface);
224+
223225 struct ModelEntry {
224226 ModelEntry() {}
225227 ModelEntry(Window *window,
plugins/WindowManager/Window.h−1
@@ -32,7 +32,6 @@ namespace unity {
3232 }
3333}
3434
35−
3635Q_DECLARE_LOGGING_CATEGORY(UNITY_WINDOW)
3736
3837/**
qml/Components/WindowControlButtons.qml+2
@@ -29,6 +29,7 @@ Row {
2929 property bool windowIsMaximized: false
3030 property bool closeButtonShown: true
3131 property bool maximizeButtonShown: true
32+ property bool minimizeButtonVisible: true
3233 property bool overlayShown
3334
3435 signal closeClicked()
@@ -75,6 +76,7 @@ Row {
7576 height: parent.height
7677 width: height
7778 onClicked: root.minimizeClicked()
79+ visible: root.minimizeButtonVisible
7880
7981 // We dont want touch events to fall through to parent,
8082 // otherwise the containsMouse will not work.
qml/Stage/ChildWindow.qmlnew file+159
@@ -0,0 +1,159 @@
1+/*
2+ * Copyright (C) 2016 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 Unity.Application 0.1
20+
21+Item {
22+ id: root
23+
24+ // Set from outside.
25+ property var surface
26+ property Item boundsItem
27+ property Item target
28+ property alias requestedWidth: surfaceContainer.requestedWidth
29+ property alias requestedHeight: surfaceContainer.requestedHeight
30+
31+ width: surface ? surface.size.width : 0
32+ height: surface ? surface.size.height : 0
33+
34+ // Make it get shown and hidden with a fade in/out effect
35+ opacity: surface && surface.state !== Mir.MinimizedState && surface.state !== Mir.HiddenState ? 1.0 : 0.0
36+ Behavior on opacity { UbuntuNumberAnimation {} }
37+ visible: opacity !== 0.0 // make it transparent to input as well
38+
39+ readonly property bool dragging: windowResizeArea.dragging || d.touchOverlayDragging || d.moveHandlerDragging
40+
41+ QtObject {
42+ id: d
43+ readonly property bool decorated: surface ? surface.type === Mir.UtilityType
44+ || surface.type === Mir.DialogType
45+ || surface.type === Mir.NormalType
46+ : false
47+
48+ readonly property bool moveable: decorated
49+ readonly property bool resizeable: decorated
50+
51+ property alias decoration: decorationLoader.item
52+ property alias moveHandler: moveHandlerLoader.item
53+
54+ readonly property bool touchOverlayDragging: touchOverlayLoader.item ? touchOverlayLoader.item.dragging : false
55+ readonly property bool moveHandlerDragging: moveHandlerLoader.item ? moveHandlerLoader.item.dragging : false
56+ }
57+
58+ WindowResizeArea {
59+ id: windowResizeArea
60+ anchors {
61+ top: decorationLoader.top
62+ bottom: parent.bottom
63+ left: parent.left; right: parent.right
64+ }
65+ target: root.target
66+ boundsItem: root.boundsItem
67+ minWidth: units.gu(10)
68+ minHeight: units.gu(10)
69+ borderThickness: units.gu(2)
70+ enabled: d.resizeable
71+ visible: enabled
72+ onPressed: root.surface.activate();
73+ }
74+
75+ BorderImage {
76+ property real shadowThickness: root.surface && root.surface.focused ? units.gu(2) : units.gu(1.5)
77+ anchors {
78+ top: decorationLoader.top
79+ bottom: parent.bottom
80+ left: parent.left; right: parent.right
81+ margins: -shadowThickness
82+ }
83+ source: "../graphics/dropshadow2gu.sci"
84+ opacity: .3
85+ }
86+
87+ Loader {
88+ id: decorationLoader
89+ anchors.bottom: root.top
90+ anchors.left: root.left
91+ anchors.right: root.right
92+
93+ visible: active
94+ active: d.decorated
95+
96+ height: item ? item.height : 0
97+
98+ sourceComponent: Component {
99+ WindowDecoration {
100+ height: units.gu(3)
101+ title: root.surface ? root.surface.name : ""
102+ active: root.surface ? root.surface.focused : false
103+ closeButtonVisible: false
104+ minimizeButtonVisible: false
105+ maximizeButtonShown: false
106+ onPressed: root.surface.activate();
107+ onPressedChanged: if (d.moveHandler) { d.moveHandler.handlePressedChanged(pressed, pressedButtons, mouseX, mouseY); }
108+ onPositionChanged: if (d.moveHandler) {
109+ d.moveHandler.handlePositionChanged(mouse);
110+ }
111+ onReleased: if (d.moveHandler) { d.moveHandler.handleReleased(); }
112+ }
113+ }
114+ }
115+
116+ Loader {
117+ id: moveHandlerLoader
118+ active: d.moveable
119+ sourceComponent: Component {
120+ MoveHandler {
121+ target: root.target
122+ buttonsWidth: d.decoration ? d.decoration.buttonsWidth : 0
123+ boundsItem: root.boundsItem
124+ boundsTopMargin: decorationLoader.height
125+ }
126+ }
127+ }
128+
129+ SurfaceContainer {
130+ id: surfaceContainer
131+
132+ // Do not hold on to a dead surface so that it can be destroyed.
133+ // FIXME It should not be QML's job to release the MirSurface if its backing surface goes away. Instead backing
134+ // MirSurface should go away but the MirSurfaceItem should be able to live on with the last drawn frame
135+ // and properties.
136+ surface: root.surface && root.surface.live ? root.surface : null
137+
138+ requestedWidth: surface ? surface.size.width : 0
139+ requestedHeight: surface ? surface.size.height : 0
140+
141+ // TODO ChildWindow parent will probably want to control those
142+ interactive: true
143+ consumesInput: true
144+ }
145+
146+ Loader {
147+ id: touchOverlayLoader
148+ active: d.resizeable || d.moveable
149+ anchors.top: decorationLoader.top
150+ anchors.bottom: parent.bottom
151+ anchors.left: parent.left
152+ anchors.right: parent.right
153+ sourceComponent: Component { WindowControlsOverlay {
154+ target: root.target
155+ resizeArea: windowResizeArea
156+ boundsItem: root.boundsItem
157+ } }
158+ }
159+}
qml/Stage/ChildWindowRepeater.qmlnew file+28
@@ -0,0 +1,28 @@
1+/*
2+ * Copyright (C) 2016 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+// Meant to be created with a Loader to circumvent the "ChildWindowTree is instantiated recursively" error from the QML engine
20+Repeater {
21+ id: root
22+ property Item boundsItem
23+ delegate: ChildWindowTree {
24+ surface: model.surface
25+ z: root.count - model.index
26+ boundsItem: root.boundsItem
27+ }
28+}
qml/Stage/ChildWindowTree.qmlnew file+127
@@ -0,0 +1,127 @@
1+/*
2+ * Copyright (C) 2016 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 Unity.Application 0.1
20+
21+Item {
22+ id: root
23+
24+ property alias surface: childWindow.surface
25+ property real displacementX: 0
26+ property real displacementY: 0
27+ property alias boundsItem: childWindow.boundsItem
28+
29+ x: surface ? surface.position.x + displacementX : 0
30+ y: surface ? surface.position.y + displacementY : 0
31+ width: childWindow.width
32+ height: childWindow.height
33+
34+ ////
35+ // API expected by MoveHandler (and some by WindowResizeArea as well)
36+ readonly property bool maximized: false
37+ readonly property bool maximizedLeft: false
38+ readonly property bool maximizedRight: false
39+ readonly property bool maximizedHorizontally: false
40+ readonly property bool maximizedVertically: false
41+ readonly property bool maximizedTopLeft: false
42+ readonly property bool maximizedTopRight: false
43+ readonly property bool maximizedBottomLeft: false
44+ readonly property bool maximizedBottomRight: false
45+ readonly property bool anyMaximized: maximized || maximizedLeft || maximizedRight || maximizedHorizontally || maximizedVertically ||
46+ maximizedTopLeft || maximizedTopRight || maximizedBottomLeft || maximizedBottomRight
47+
48+ readonly property bool canBeCornerMaximized: false
49+ readonly property bool canBeMaximizedLeftRight: false
50+ readonly property bool canBeMaximized: false
51+
52+ readonly property var resizeArea: QtObject {
53+ property real normalWidth: units.gu(1)
54+ property real normalHeight: units.gu(1)
55+ }
56+
57+ readonly property bool windowedTransitionRunning: false
58+
59+ // NB: those bindings will be overwritten by MoveHandler when you first move the window
60+ property real windowedX: x
61+ property real windowedY: y
62+
63+
64+ property real restoredX
65+ property real restoredY
66+
67+ state: "restored"
68+ // end of API expected by MoveHandler
69+ ////
70+
71+ ////
72+ // API expected by WindowResizeArea
73+ property real windowedWidth: childWindow.width
74+ property real windowedHeight: childWindow.height
75+ // end of API expected by WindowResizeArea
76+ ////
77+
78+ ////
79+ // API expected by WindowControlsOverlay
80+ function activate() {
81+ surface.activate();
82+ }
83+ // end of API expected by WindowControlsOverlay
84+ ////
85+
86+ Binding {
87+ target: root.surface
88+ when: childWindow.dragging
89+ property: "requestedPosition"
90+ value: Qt.point(root.windowedX - root.displacementX,
91+ root.windowedY - root.displacementY);
92+ }
93+
94+ // It's a separate Item so that a window can be hid independently of its children
95+ ChildWindow {
96+ id: childWindow
97+ target: root
98+ requestedWidth: root.windowedWidth
99+ requestedHeight: root.windowedHeight
100+ }
101+
102+ Connections {
103+ target: root.surface
104+ onFocusRequested: {
105+ root.surface.activate();
106+ }
107+ }
108+
109+ // Using a loader here mainly to circunvent the "ChildWindowTree is instantiated recursively" error from the QML engine
110+ Loader {
111+ id: childRepeaterLoader
112+ source: "ChildWindowRepeater.qml"
113+ active: root.surface && root.surface.childSurfaceList.count > 0
114+ Binding {
115+ target: childRepeaterLoader.item
116+ when: childRepeaterLoader.item
117+ property: "model"
118+ value: root.surface ? root.surface.childSurfaceList : null
119+ }
120+ Binding {
121+ target: childRepeaterLoader.item
122+ when: childRepeaterLoader.item
123+ property: "boundsItem"
124+ value: root.boundsItem
125+ }
126+ }
127+}
qml/Stage/DecoratedWindow.qml+1−2
@@ -67,8 +67,7 @@ FocusScope {
6767 readonly property int heightIncrement: !counterRotate ? applicationWindow.heightIncrement : applicationWindow.widthIncrement
6868
6969 property alias overlayShown: decoration.overlayShown
70− property alias stageWidth: moveHandler.stageWidth
71− property alias stageHeight: moveHandler.stageHeight
70+ property alias boundsItem: moveHandler.boundsItem
7271 readonly property alias dragging: moveHandler.dragging
7372
7473 readonly property Item clientAreaItem: applicationWindow
qml/Stage/MoveHandler.qml+23−18
@@ -19,15 +19,14 @@ import Unity.Application 0.1 // For Mir singleton
1919import Ubuntu.Components 1.3
2020import Utils 0.1
2121import "../Components"
22−import "../Components/PanelState"
2322
2423QtObject {
2524 id: root
2625
2726 property Item target // appDelegate
28− property int stageWidth
29− property int stageHeight
3027 property real buttonsWidth: 0
28+ property Item boundsItem
29+ property real boundsTopMargin: 0
3130
3231 readonly property bool dragging: priv.dragging
3332
@@ -79,7 +78,10 @@ QtObject {
7978 // length of the triggerArea square diagonal
8079 var diagLength = Math.sqrt(2 * priv.triggerArea * priv.triggerArea);
8180 var ratio = 1 - (distance / diagLength);
82− return bx > 0 && bx <= stageWidth && by > 0 && by <= stageHeight ? ratio : 1; // everything "outside" of our square from the center is 1
81+
82+ // everything "outside" of our square from the center is 1
83+ var mousePosBoundsCoords = target.mapToItem(root.boundsItem, bx, by);
84+ return root.boundsItem.contains(mousePosBoundsCoords) ? ratio : 1;
8385 }
8486 property real progress: 0
8587 }
@@ -117,35 +119,38 @@ QtObject {
117119 target.requestRestore();
118120 }
119121
120− var pos = mapToItem(target.parent, mouse.x, mouse.y);
122+ var pos = mapToItem(target.parent, mouse.x, mouse.y); // How can that work if we're just a QtObject (not an Item)?
123+ var bounds = boundsItem.mapToItem(target.parent, 0, 0, boundsItem.width, boundsItem.height);
124+ bounds.y += boundsTopMargin;
125+ bounds.height -= boundsTopMargin;
121126 // Use integer coordinate values to ensure that target is left in a pixel-aligned
122127 // position. Mouse movement could have subpixel precision, yielding a fractional
123128 // mouse position.
124129 target.windowedX = Math.round(pos.x - priv.distanceX);
125− target.windowedY = Math.round(Math.max(pos.y - priv.distanceY, PanelState.panelHeight));
130+ target.windowedY = Math.round(Math.max(pos.y - priv.distanceY, bounds.top));
126131
127132 if (sensingPoints) { // edge/corner detection when dragging via the touch overlay
128− if (sensingPoints.topLeft.x < priv.triggerArea && sensingPoints.topLeft.y < PanelState.panelHeight + priv.triggerArea
133+ if (sensingPoints.topLeft.x < priv.triggerArea && sensingPoints.topLeft.y < bounds.top + priv.triggerArea
129134 && target.canBeCornerMaximized) { // top left
130− priv.progress = priv.progressInCorner(0, PanelState.panelHeight, sensingPoints.topLeft.x, sensingPoints.topLeft.y);
135+ priv.progress = priv.progressInCorner(bounds.left, bounds.top, sensingPoints.topLeft.x, sensingPoints.topLeft.y);
131136 priv.resetEdges();
132137 priv.nearTopLeftCorner = true;
133138 root.fakeMaximizeTopLeftAnimationRequested(priv.progress);
134− } else if (sensingPoints.topRight.x > stageWidth - priv.triggerArea && sensingPoints.topRight.y < PanelState.panelHeight + priv.triggerArea
139+ } else if (sensingPoints.topRight.x > bounds.right - priv.triggerArea && sensingPoints.topRight.y < bounds.top + priv.triggerArea
135140 && target.canBeCornerMaximized) { // top right
136− priv.progress = priv.progressInCorner(stageWidth, PanelState.panelHeight, sensingPoints.topRight.x, sensingPoints.topRight.y);
141+ priv.progress = priv.progressInCorner(bounds.right, bounds.top, sensingPoints.topRight.x, sensingPoints.topRight.y);
137142 priv.resetEdges();
138143 priv.nearTopRightCorner = true;
139144 root.fakeMaximizeTopRightAnimationRequested(priv.progress);
140− } else if (sensingPoints.bottomLeft.x < priv.triggerArea && sensingPoints.bottomLeft.y > stageHeight - priv.triggerArea
145+ } else if (sensingPoints.bottomLeft.x < priv.triggerArea && sensingPoints.bottomLeft.y > bounds.bottom - priv.triggerArea
141146 && target.canBeCornerMaximized) { // bottom left
142− priv.progress = priv.progressInCorner(0, stageHeight, sensingPoints.bottomLeft.x, sensingPoints.bottomLeft.y);
147+ priv.progress = priv.progressInCorner(bounds.left, bounds.bottom, sensingPoints.bottomLeft.x, sensingPoints.bottomLeft.y);
143148 priv.resetEdges();
144149 priv.nearBottomLeftCorner = true;
145150 root.fakeMaximizeBottomLeftAnimationRequested(priv.progress);
146− } else if (sensingPoints.bottomRight.x > stageWidth - priv.triggerArea && sensingPoints.bottomRight.y > stageHeight - priv.triggerArea
151+ } else if (sensingPoints.bottomRight.x > bounds.right - priv.triggerArea && sensingPoints.bottomRight.y > bounds.bottom - priv.triggerArea
147152 && target.canBeCornerMaximized) { // bottom right
148− priv.progress = priv.progressInCorner(stageWidth, stageHeight, sensingPoints.bottomRight.x, sensingPoints.bottomRight.y);
153+ priv.progress = priv.progressInCorner(bounds.right, bounds.bottom, sensingPoints.bottomRight.x, sensingPoints.bottomRight.y);
149154 priv.resetEdges();
150155 priv.nearBottomRightCorner = true;
151156 root.fakeMaximizeBottomRightAnimationRequested(priv.progress);
@@ -154,13 +159,13 @@ QtObject {
154159 priv.resetEdges();
155160 priv.nearLeftEdge = true;
156161 root.fakeMaximizeLeftAnimationRequested(priv.progress);
157− } else if (sensingPoints.right.x > stageWidth - priv.triggerArea && target.canBeMaximizedLeftRight) { // right
158− priv.progress = MathUtils.clampAndProject(sensingPoints.right.x, stageWidth - priv.triggerArea, stageWidth, 0, 1);
162+ } else if (sensingPoints.right.x > bounds.right - priv.triggerArea && target.canBeMaximizedLeftRight) { // right
163+ priv.progress = MathUtils.clampAndProject(sensingPoints.right.x, bounds.right - priv.triggerArea, bounds.right, 0, 1);
159164 priv.resetEdges();
160165 priv.nearRightEdge = true;
161166 root.fakeMaximizeRightAnimationRequested(priv.progress);
162− } else if (sensingPoints.top.y < PanelState.panelHeight + priv.triggerArea && target.canBeMaximized) { // top
163− priv.progress = MathUtils.clampAndProject(sensingPoints.top.y, PanelState.panelHeight + priv.triggerArea, 0, 0, 1);
167+ } else if (sensingPoints.top.y < bounds.top + priv.triggerArea && target.canBeMaximized) { // top
168+ priv.progress = MathUtils.clampAndProject(sensingPoints.top.y, bounds.top + priv.triggerArea, 0, 0, 1);
164169 priv.resetEdges();
165170 priv.nearTopEdge = true;
166171 root.fakeMaximizeAnimationRequested(priv.progress);
qml/Stage/Stage.qml+50−9
@@ -100,7 +100,7 @@ FocusScope {
100100 } else {
101101 // No we didn't, do a quick alt-tab
102102 if (appRepeater.count > 1) {
103− appRepeater.itemAt(1).claimFocus();
103+ appRepeater.itemAt(1).activate();
104104 }
105105 }
106106 }
@@ -665,6 +665,13 @@ FocusScope {
665665 }
666666 }
667667
668+ Item {
669+ id: boundariesForWindowPlacement
670+ anchors.fill: parent
671+ anchors.topMargin: PanelState.panelHeight
672+ visible: false
673+ }
674+
668675 Repeater {
669676 id: appRepeater
670677 model: topLevelSurfaceList
@@ -833,7 +840,6 @@ FocusScope {
833840 readonly property var surface: model.window.surface
834841 readonly property var window: model.window
835842
836− readonly property alias resizeArea: resizeArea
837843 readonly property alias focusedSurface: decoratedWindow.focusedSurface
838844 readonly property bool dragging: touchControls.overlayShown ? touchControls.dragging : decoratedWindow.dragging
839845
@@ -890,7 +896,7 @@ FocusScope {
890896 screenWidth: appContainer.width
891897 screenHeight: appContainer.height
892898 leftMargin: root.leftMargin
893− minimumY: PanelState.panelHeight
899+ minimumY: boundariesForWindowPlacement.y
894900 }
895901
896902 Connections {
@@ -1520,7 +1526,7 @@ FocusScope {
15201526 anchors.margins: touchControls.overlayShown ? borderThickness/2 : -borderThickness
15211527
15221528 target: appDelegate
1523− minimumY: PanelState.panelHeight // disallow resizing up past Panel
1529+ boundsItem: boundariesForWindowPlacement
15241530 minWidth: units.gu(10)
15251531 minHeight: units.gu(10)
15261532 borderThickness: units.gu(2)
@@ -1539,7 +1545,7 @@ FocusScope {
15391545 anchors.top: appDelegate.top
15401546 application: model.application
15411547 surface: model.window.surface
1542− active: appDelegate.focus
1548+ active: model.window.focused
15431549 focus: true
15441550 interactive: root.interactive
15451551 showDecoration: 1
@@ -1548,8 +1554,7 @@ FocusScope {
15481554 width: implicitWidth
15491555 height: implicitHeight
15501556 highlightSize: windowInfoItem.iconMargin / 2
1551− stageWidth: appContainer.width
1552− stageHeight: appContainer.height
1557+ boundsItem: boundariesForWindowPlacement
15531558
15541559 requestedWidth: appDelegate.requestedWidth
15551560 requestedHeight: appDelegate.requestedHeight
@@ -1604,11 +1609,12 @@ FocusScope {
16041609
16051610 WindowControlsOverlay {
16061611 id: touchControls
1612+ anchors.fill: appDelegate
16071613 target: appDelegate
1614+ resizeArea: resizeArea
16081615 enabled: false
16091616 visible: enabled
1610− stageWidth: appContainer.width
1611− stageHeight: appContainer.height
1617+ boundsItem: boundariesForWindowPlacement
16121618
16131619 onFakeMaximizeAnimationRequested: if (!appDelegate.maximized) fakeRectangle.maximize(amount, true)
16141620 onFakeMaximizeLeftAnimationRequested: if (!appDelegate.maximizedLeft) fakeRectangle.maximizeLeft(amount, true)
@@ -1703,6 +1709,41 @@ FocusScope {
17031709 sourceSize.height: height
17041710 }
17051711 }
1712+
1713+ Item {
1714+ // Group all child windows in this item so that we can fade them out together when going to the spread
1715+ // (and fade them in back again when returning from it)
1716+ readonly property bool stageOnProperState: root.state === "windowed"
1717+ || root.state === "staged"
1718+ || root.state === "stagedWithSideStage"
1719+
1720+ // TODO: Is it worth the extra cost of layering to avoid the opacity artifacts of intersecting children?
1721+ // Btw, will involve more than uncommenting the line below as children won't necessarily fit this item's
1722+ // geometry. This is just a reference.
1723+ //layer.enabled: opacity !== 0.0 && opacity !== 1.0
1724+
1725+ opacity: stageOnProperState ? 1.0 : 0.0
1726+ visible: opacity !== 0.0 // make it transparent to input as well
1727+ Behavior on opacity { UbuntuNumberAnimation {} }
1728+
1729+ Repeater {
1730+ id: childWindowRepeater
1731+ model: appDelegate.surface ? appDelegate.surface.childSurfaceList : null
1732+
1733+ delegate: ChildWindowTree {
1734+ surface: model.surface
1735+
1736+ // Account for the displacement caused by window decoration in the top-level surface
1737+ // Ie, the top-level surface is not positioned at (0,0) of this ChildWindow's parent (appDelegate)
1738+ displacementX: appDelegate.clientAreaItem.x
1739+ displacementY: appDelegate.clientAreaItem.y
1740+
1741+ boundsItem: boundariesForWindowPlacement
1742+
1743+ z: childWindowRepeater.count - model.index
1744+ }
1745+ }
1746+ }
17061747 }
17071748 }
17081749 }
qml/Stage/WindowControlsOverlay.qml+21−24
@@ -18,17 +18,14 @@ import QtQuick 2.4
1818import Ubuntu.Components 1.3
1919import Ubuntu.Gestures 0.1
2020import Unity.Application 0.1
21−import "../Components/PanelState"
2221
2322Item {
2423 id: root
25− enabled: target && !target.fullscreen
26− anchors.fill: target
2724
2825 // to be set from outside
2926 property Item target // appDelegate
30− property alias stageWidth: moveHandler.stageWidth
31− property alias stageHeight: moveHandler.stageHeight
27+ property WindowResizeArea resizeArea
28+ property Item boundsItem
3229
3330 // to be read from outside
3431 readonly property alias overlayShown: overlay.visible
@@ -90,9 +87,7 @@ Item {
9087
9188 QtObject {
9289 id: priv
93− readonly property var resizeArea: root.target && root.target.resizeArea ? root.target.resizeArea : null
94− readonly property bool ensureWindow: root.target.state == "normal" || root.target.state == "restored"
95− readonly property bool dragging: moveHandler.dragging || (resizeArea && resizeArea.dragging)
90+ readonly property bool dragging: moveHandler.dragging || (root.resizeArea && root.resizeArea.dragging)
9691
9792 function getSensingPoints() {
9893 var xPoints = [];
@@ -162,6 +157,8 @@ Item {
162157 objectName: "moveHandler"
163158 target: root.target
164159
160+ boundsItem: root.boundsItem
161+
165162 onFakeMaximizeAnimationRequested: root.fakeMaximizeAnimationRequested(amount)
166163 onFakeMaximizeLeftAnimationRequested: root.fakeMaximizeLeftAnimationRequested(amount)
167164 onFakeMaximizeRightAnimationRequested: root.fakeMaximizeRightAnimationRequested(amount)
@@ -193,65 +190,65 @@ Item {
193190 ResizeGrip { // top left
194191 anchors.horizontalCenter: parent.left
195192 anchors.verticalCenter: parent.top
196− visible: priv.ensureWindow || target.maximizedBottomRight
197− resizeTarget: priv.resizeArea
193+ visible: root.enabled || target.maximizedBottomRight
194+ resizeTarget: root.resizeArea
198195 }
199196
200197 ResizeGrip { // top center
201198 anchors.horizontalCenter: parent.horizontalCenter
202199 anchors.verticalCenter: parent.top
203200 rotation: 45
204− visible: priv.ensureWindow || target.maximizedHorizontally || target.maximizedBottomLeft || target.maximizedBottomRight
205− resizeTarget: priv.resizeArea
201+ visible: root.enabled || target.maximizedHorizontally || target.maximizedBottomLeft || target.maximizedBottomRight
202+ resizeTarget: root.resizeArea
206203 }
207204
208205 ResizeGrip { // top right
209206 anchors.horizontalCenter: parent.right
210207 anchors.verticalCenter: parent.top
211208 rotation: 90
212− visible: priv.ensureWindow || target.maximizedBottomLeft
213− resizeTarget: priv.resizeArea
209+ visible: root.enabled || target.maximizedBottomLeft
210+ resizeTarget: root.resizeArea
214211 }
215212
216213 ResizeGrip { // right
217214 anchors.horizontalCenter: parent.right
218215 anchors.verticalCenter: parent.verticalCenter
219216 rotation: 135
220− visible: priv.ensureWindow || target.maximizedVertically || target.maximizedLeft ||
217+ visible: root.enabled || target.maximizedVertically || target.maximizedLeft ||
221218 target.maximizedTopLeft || target.maximizedBottomLeft
222− resizeTarget: priv.resizeArea
219+ resizeTarget: root.resizeArea
223220 }
224221
225222 ResizeGrip { // bottom right
226223 anchors.horizontalCenter: parent.right
227224 anchors.verticalCenter: parent.bottom
228− visible: priv.ensureWindow || target.maximizedTopLeft
229− resizeTarget: priv.resizeArea
225+ visible: root.enabled || target.maximizedTopLeft
226+ resizeTarget: root.resizeArea
230227 }
231228
232229 ResizeGrip { // bottom center
233230 anchors.horizontalCenter: parent.horizontalCenter
234231 anchors.verticalCenter: parent.bottom
235232 rotation: 45
236− visible: priv.ensureWindow || target.maximizedHorizontally || target.maximizedTopLeft || target.maximizedTopRight
237− resizeTarget: priv.resizeArea
233+ visible: root.enabled || target.maximizedHorizontally || target.maximizedTopLeft || target.maximizedTopRight
234+ resizeTarget: root.resizeArea
238235 }
239236
240237 ResizeGrip { // bottom left
241238 anchors.horizontalCenter: parent.left
242239 anchors.verticalCenter: parent.bottom
243240 rotation: 90
244− visible: priv.ensureWindow || target.maximizedTopRight
245− resizeTarget: priv.resizeArea
241+ visible: root.enabled || target.maximizedTopRight
242+ resizeTarget: root.resizeArea
246243 }
247244
248245 ResizeGrip { // left
249246 anchors.horizontalCenter: parent.left
250247 anchors.verticalCenter: parent.verticalCenter
251248 rotation: 135
252− visible: priv.ensureWindow || target.maximizedVertically || target.maximizedRight ||
249+ visible: root.enabled || target.maximizedVertically || target.maximizedRight ||
253250 target.maximizedTopRight || target.maximizedBottomRight
254− resizeTarget: priv.resizeArea
251+ resizeTarget: root.resizeArea
255252 }
256253 }
257254}
qml/Stage/WindowDecoration.qml+1
@@ -28,6 +28,7 @@ MouseArea {
2828 property alias closeButtonVisible: buttons.closeButtonShown
2929 property alias title: titleLabel.text
3030 property alias maximizeButtonShown: buttons.maximizeButtonShown
31+ property alias minimizeButtonVisible: buttons.minimizeButtonVisible
3132 property bool active: false
3233 property alias overlayShown: buttons.overlayShown
3334 property var menu: undefined
qml/Stage/WindowResizeArea.qml+3−2
@@ -32,7 +32,7 @@ MouseArea {
3232 // The area will anchor to it and manage resize events
3333 property Item target: null
3434 property int borderThickness: 0
35− property real minimumY: -100000000 // By default, impose no limit
35+ property Item boundsItem
3636 property int minWidth: 0
3737 property int minHeight: 0
3838
@@ -243,7 +243,8 @@ MouseArea {
243243 }
244244
245245 if (d.topBorder) {
246− var newTargetY = Math.max(d.startY + deltaY, root.minimumY);
246+ var bounds = boundsItem.mapToItem(target.parent, 0, 0, boundsItem.width, boundsItem.height);
247+ var newTargetY = Math.max(d.startY + deltaY, bounds.y);
247248 var bottomBorderY = target.windowedY + target.height;
248249 if (bottomBorderY > newTargetY + d.minimumHeight) {
249250 if (bottomBorderY < newTargetY + d.maximumHeight) {
tests/mocks/Unity/Application/ApplicationInfo.cpp+9−1
@@ -94,6 +94,7 @@ void ApplicationInfo::createPromptSurface()
9494 auto surface = surfaceManager->createSurface(QString("prompt foo"),
9595 Mir::NormalType,
9696 Mir::RestoredState,
97+ nullptr, /* parentSurface */
9798 screenshotUrl);
9899 surfaceManager->notifySurfaceCreated(surface);
99100
@@ -122,7 +123,9 @@ void ApplicationInfo::createSurface()
122123 auto surface = surfaceManager->createSurface(surfaceName,
123124 Mir::NormalType,
124125 fullscreen() ? Mir::FullscreenState : Mir::RestoredState,
125− m_screenshotFileName);
126+ nullptr, /* parentSurface */
127+ m_screenshotFileName,
128+ m_qmlFilePath);
126129
127130 surface->setShellChrome(m_shellChrome);
128131
@@ -201,6 +204,11 @@ void ApplicationInfo::setScreenshotId(const QString &screenshotId)
201204 }
202205}
203206
207+void ApplicationInfo::setQmlFilename(const QString &qmlFilename)
208+{
209+ m_qmlFilePath = QString("qrc:///Unity/Application/%1").arg(qmlFilename);
210+}
211+
204212void ApplicationInfo::setName(const QString &value)
205213{
206214 if (value != m_name) {
tests/mocks/Unity/Application/ApplicationInfo.h+3
@@ -29,6 +29,7 @@ class MirSurface;
2929
3030#include <QList>
3131#include <QTimer>
32+#include <QUrl>
3233
3334using namespace unity::shell::application;
3435
@@ -58,6 +59,7 @@ public:
5859
5960 void setIconId(const QString &iconId);
6061 void setScreenshotId(const QString &screenshotId);
62+ void setQmlFilename(const QString &);
6163
6264 void setAppId(const QString &value) { m_appId = value; }
6365 QString appId() const override { return m_appId; }
@@ -152,6 +154,7 @@ private:
152154 QList<MirSurface*> m_closingSurfaces;
153155 bool m_manualSurfaceCreation{false};
154156 Mir::ShellChrome m_shellChrome{Mir::NormalChrome};
157+ QUrl m_qmlFilePath;
155158};
156159
157160Q_DECLARE_METATYPE(ApplicationInfo*)
tests/mocks/Unity/Application/ApplicationManager.cpp+9
@@ -480,6 +480,15 @@ void ApplicationManager::buildListOfAvailableApplications()
480480 application->setName("Primary Oriented");
481481 application->setSupportedOrientations(Qt::PrimaryOrientation);
482482 m_availableApplications.append(application);
483+
484+ application = new ApplicationInfo(this);
485+ application->setAppId("kate");
486+ application->setName("Kate");
487+ application->setIconId("libreoffice");
488+ application->setScreenshotId("libreoffice");
489+ application->setQmlFilename("Kate.qml");
490+ application->setIsTouchApp(false);
491+ m_availableApplications.append(application);
483492}
484493
485494
tests/mocks/Unity/Application/MirSurface.cpp+43
@@ -74,6 +74,7 @@ using namespace unity::shell::application;
7474MirSurface::MirSurface(const QString& name,
7575 Mir::Type type,
7676 Mir::State state,
77+ MirSurface *parentSurface,
7778 const QUrl& screenshot,
7879 const QUrl &qmlFilePath)
7980 : unity::shell::application::MirSurfaceInterface(nullptr)
@@ -90,6 +91,8 @@ MirSurface::MirSurface(const QString& name,
9091 , m_height(-1)
9192 , m_slowToResize(false)
9293 , m_shellChrome(Mir::NormalChrome)
94+ , m_parentSurface(parentSurface)
95+ , m_childSurfaceList(new MirSurfaceListModel(this))
9396{
9497 DEBUG_MSG("state=" << stateToStr(state));
9598
@@ -493,6 +496,36 @@ void MirSurface::setInputBounds(const QRect &boundsRect)
493496 }
494497}
495498
499+void MirSurface::openMenu(qreal x, qreal y, qreal width, qreal height)
500+{
501+ auto *menu = SurfaceManager::instance()->createSurface("menu", Mir::MenuType, Mir::HiddenState,
502+ this /* parentSurface */,
503+ QUrl() /* screenshot */,
504+ QUrl("qrc:///Unity/Application/KateMenu.qml"));
505+
506+ menu->setRequestedPosition(QPoint(x,y));
507+ menu->resize(width, height);
508+ menu->requestState(Mir::RestoredState);
509+
510+ SurfaceManager::instance()->notifySurfaceCreated(menu);
511+}
512+
513+void MirSurface::openDialog(qreal x, qreal y, qreal width, qreal height)
514+{
515+ auto *dialog = SurfaceManager::instance()->createSurface("dialog", Mir::DialogType, Mir::HiddenState,
516+ this /* parentSurface */,
517+ QUrl() /* screenshot */,
518+ QUrl("qrc:///Unity/Application/KateDialog.qml"));
519+
520+ dialog->setRequestedPosition(QPoint(x,y));
521+ dialog->resize(width, height);
522+ dialog->requestState(Mir::RestoredState);
523+
524+ SurfaceManager::instance()->notifySurfaceCreated(dialog);
525+
526+ dialog->requestFocus();
527+}
528+
496529void MirSurface::setRequestedPosition(const QPoint &value)
497530{
498531 if (value != m_requestedPosition) {
@@ -505,3 +538,13 @@ void MirSurface::setRequestedPosition(const QPoint &value)
505538 Q_EMIT positionChanged(m_position);
506539 }
507540}
541+
542+MirSurfaceInterface* MirSurface::parentSurface() const
543+{
544+ return m_parentSurface;
545+}
546+
547+MirSurfaceListInterface* MirSurface::childSurfaceList() const
548+{
549+ return m_childSurfaceList;
550+}
tests/mocks/Unity/Application/MirSurface.h+11
@@ -45,6 +45,7 @@ public:
4545 MirSurface(const QString& name,
4646 Mir::Type type,
4747 Mir::State state,
48+ MirSurface *parentSurface,
4849 const QUrl& screenshot,
4950 const QUrl &qmlFilePath = QUrl());
5051 virtual ~MirSurface();
@@ -94,6 +95,9 @@ public:
9495 QPoint requestedPosition() const override { return m_requestedPosition; }
9596 void setRequestedPosition(const QPoint &) override;
9697
98+ unity::shell::application::MirSurfaceInterface* parentSurface() const override;
99+ unity::shell::application::MirSurfaceListInterface* childSurfaceList() const override;
100+
97101 Q_INVOKABLE void close() override;
98102 Q_INVOKABLE void activate() override;
99103
@@ -121,6 +125,9 @@ public:
121125
122126 Q_INVOKABLE virtual void setInputBounds(const QRect &boundsRect);
123127
128+ Q_INVOKABLE void openMenu(qreal x, qreal y, qreal width, qreal height);
129+ Q_INVOKABLE void openDialog(qreal x, qreal y, qreal width, qreal height);
130+
124131 /////
125132 // internal mock stuff
126133
@@ -216,6 +223,10 @@ private:
216223
217224 QPoint m_position;
218225 QPoint m_requestedPosition;
226+
227+ unity::shell::application::MirSurfaceInterface* m_parentSurface;
228+
229+ MirSurfaceListModel *m_childSurfaceList;
219230};
220231
221232#endif // MOCK_MIR_SURFACE_H
tests/mocks/Unity/Application/MirSurfaceItem.cpp+19−4
@@ -159,7 +159,9 @@ void MirSurfaceItem::setOrientationAngle(Mir::OrientationAngle angle)
159159
160160 if (m_qmlItem) {
161161 QQmlProperty orientationProp(m_qmlItem, "orientationAngle");
162− orientationProp.write(QVariant::fromValue(orientationAngle()));
162+ if (orientationProp.isValid()) {
163+ orientationProp.write(QVariant::fromValue(orientationAngle()));
164+ }
163165 }
164166}
165167
@@ -167,7 +169,9 @@ void MirSurfaceItem::updateScreenshot(QUrl screenshotUrl)
167169{
168170 if (m_qmlItem) {
169171 QQmlProperty screenshotSource(m_qmlItem, "screenshotSource");
170− screenshotSource.write(QVariant::fromValue(screenshotUrl));
172+ if (screenshotSource.isValid()) {
173+ screenshotSource.write(QVariant::fromValue(screenshotUrl));
174+ }
171175 }
172176}
173177
@@ -193,12 +197,23 @@ void MirSurfaceItem::createQmlContentItem()
193197
194198 {
195199 QQmlProperty screenshotSource(m_qmlItem, "screenshotSource");
196− screenshotSource.write(QVariant::fromValue(m_qmlSurface->screenshotUrl()));
200+ if (screenshotSource.isValid()) {
201+ screenshotSource.write(QVariant::fromValue(m_qmlSurface->screenshotUrl()));
202+ }
197203 }
198204
199205 {
200206 QQmlProperty orientationProp(m_qmlItem, "orientationAngle");
201− orientationProp.write(QVariant::fromValue(orientationAngle()));
207+ if (orientationProp.isValid()) {
208+ orientationProp.write(QVariant::fromValue(orientationAngle()));
209+ }
210+ }
211+
212+ {
213+ QQmlProperty surfaceProperty(m_qmlItem, "surface");
214+ if (surfaceProperty.isValid()) {
215+ surfaceProperty.write(QVariant::fromValue(m_qmlSurface));
216+ }
202217 }
203218}
204219
tests/mocks/Unity/Application/MirSurfaceListModel.h−1
@@ -44,7 +44,6 @@ public:
4444 bool contains(MirSurface *surface) const { return m_surfaceList.contains(surface); }
4545
4646private:
47− void appendSurface(MirSurface *surface);
4847 void raise(MirSurface *surface);
4948 void moveSurface(int from, int to);
5049 void connectSurface(MirSurface *surface);
tests/mocks/Unity/Application/SurfaceManager.cpp+15−8
@@ -61,10 +61,15 @@ SurfaceManager::~SurfaceManager()
6161MirSurface *SurfaceManager::createSurface(const QString& name,
6262 Mir::Type type,
6363 Mir::State state,
64− const QUrl& screenshot)
64+ MirSurface *parentSurface,
65+ const QUrl &screenshot,
66+ const QUrl &qmlFilePath)
6567{
66− MirSurface* surface = new MirSurface(name, type, state, screenshot);
68+ MirSurface* surface = new MirSurface(name, type, state, parentSurface, screenshot, qmlFilePath);
6769 registerSurface(surface);
70+ if (parentSurface) {
71+ static_cast<MirSurfaceListModel*>(parentSurface->childSurfaceList())->addSurface(surface);
72+ }
6873 return surface;
6974}
7075
@@ -72,12 +77,14 @@ void SurfaceManager::registerSurface(MirSurface *surface)
7277{
7378 m_surfaces.prepend(surface);
7479
75− surface->setMinimumWidth(m_newSurfaceMinimumWidth);
76− surface->setMaximumWidth(m_newSurfaceMaximumWidth);
77− surface->setMinimumHeight(m_newSurfaceMinimumHeight);
78− surface->setMaximumHeight(m_newSurfaceMaximumHeight);
79− surface->setWidthIncrement(m_newSurfaceWidthIncrement);
80− surface->setHeightIncrement(m_newSurfaceHeightIncrement);
80+ if (!surface->parentSurface()) {
81+ surface->setMinimumWidth(m_newSurfaceMinimumWidth);
82+ surface->setMaximumWidth(m_newSurfaceMaximumWidth);
83+ surface->setMinimumHeight(m_newSurfaceMinimumHeight);
84+ surface->setMaximumHeight(m_newSurfaceMaximumHeight);
85+ surface->setWidthIncrement(m_newSurfaceWidthIncrement);
86+ surface->setHeightIncrement(m_newSurfaceHeightIncrement);
87+ }
8188
8289 connect(surface, &MirSurface::stateRequested, this, [=](Mir::State state) {
8390 this->onStateRequested(surface, state);
tests/mocks/Unity/Application/SurfaceManager.h+3−1
@@ -49,7 +49,9 @@ public:
4949 Q_INVOKABLE MirSurface* createSurface(const QString& name,
5050 Mir::Type type,
5151 Mir::State state,
52− const QUrl& screenshot);
52+ MirSurface *parentSurface,
53+ const QUrl &screenshot,
54+ const QUrl &qmlFilePath = QUrl());
5355
5456
5557 void notifySurfaceCreated(unity::shell::application::MirSurfaceInterface *);
tests/mocks/Unity/Application/VirtualKeyboard.cpp+5−4
@@ -24,10 +24,11 @@
2424
2525VirtualKeyboard::VirtualKeyboard()
2626 : MirSurface("input-method",
27− Mir::InputMethodType,
28− Mir::MinimizedState,
29− QUrl("qrc:///Unity/Application/vkb_portrait.png"),
30− QUrl("qrc:///Unity/Application/VirtualKeyboard.qml"))
27+ Mir::InputMethodType,
28+ Mir::MinimizedState,
29+ nullptr, /* parentSurface */
30+ QUrl("qrc:///Unity/Application/vkb_portrait.png"),
31+ QUrl("qrc:///Unity/Application/VirtualKeyboard.qml"))
3132{
3233}
3334
tests/mocks/Unity/Application/resources/Kate.qmlnew file+49
@@ -0,0 +1,49 @@
1+/*
2+ * Copyright (C) 2016 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+
20+Rectangle {
21+ id: root
22+ color: "white"
23+ implicitWidth: width
24+ implicitHeight: height
25+
26+ property var surface
27+
28+ Column {
29+ Button {
30+ text: "Open Menu"
31+ property real nextY: y
32+ property real menuHeight: units.gu(5)
33+ onClicked: {
34+ surface.openMenu(x+width, nextY, units.gu(10), menuHeight);
35+ nextY += menuHeight + units.gu(.5)
36+ }
37+ }
38+
39+ Button {
40+ text: "Open Dialog"
41+ property real dialogWidth: units.gu(30)
42+ property real dialogHeight: units.gu(20)
43+ onClicked: {
44+ surface.openDialog(root.x+(root.width/2)-(dialogWidth/2), root.y+(root.height/2)-(dialogHeight/2),
45+ dialogWidth, dialogHeight);
46+ }
47+ }
48+ }
49+}
tests/mocks/Unity/Application/resources/KateDialog.qmlnew file+48
@@ -0,0 +1,48 @@
1+/*
2+ * Copyright (C) 2016 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+
20+Rectangle {
21+ id: root
22+ color: "grey"
23+ implicitWidth: width
24+ implicitHeight: height
25+
26+ property var surface
27+
28+ Text {
29+ anchors.left: parent.left
30+ anchors.right: parent.right
31+ horizontalAlignment: Text.AlignHCenter
32+ verticalAlignment: Text.AlignVCenter
33+ text: "This is a child dialog."
34+ }
35+
36+ Row {
37+ anchors.bottom: parent.bottom
38+ anchors.right: parent.right
39+ Button {
40+ text: "OK"
41+ onClicked: root.surface.close()
42+ }
43+ Button {
44+ text: "Cancel"
45+ onClicked: root.surface.close()
46+ }
47+ }
48+}
tests/mocks/Unity/Application/resources/KateMenu.qmlnew file+41
@@ -0,0 +1,41 @@
1+/*
2+ * Copyright (C) 2016 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+
20+Rectangle {
21+ id: root
22+ color: "grey"
23+ implicitWidth: width
24+ implicitHeight: height
25+
26+ property var surface
27+
28+ Text {
29+ anchors.centerIn: parent
30+ text: "menu"
31+ }
32+ MouseArea {
33+ anchors.fill: parent
34+ property real nextY: 0
35+ onClicked: {
36+ // ensure some overlap with is parent in order to visually check for nested opacity artifacts
37+ root.surface.openMenu(x+(width*0.6), nextY, units.gu(10), root.height);
38+ nextY += root.height
39+ }
40+ }
41+}
tests/mocks/Unity/Application/resources/surfaces.qrc+3
@@ -1,5 +1,8 @@
11<RCC>
22 <qresource prefix="/Unity/Application">
3+ <file>Kate.qml</file>
4+ <file>KateDialog.qml</file>
5+ <file>KateMenu.qml</file>
36 <file>VirtualKeyboard.qml</file>
47 <file>MirSurfaceItem.qml</file>
58 <file>vkb_portrait.png</file>
tests/qmltests/Stage/ApplicationCheckBox.qml+1−1
@@ -162,7 +162,7 @@ Column {
162162 }
163163 }
164164
165− // Rows of application surfaces
165+ // Rows of top-level application surfaces
166166 Repeater {
167167 model: d.application ? d.application.surfaceList : null
168168 RowLayout {
tests/qmltests/Stage/tst_WindowResizeArea.qml+10
@@ -90,6 +90,7 @@ Item {
9090 id: windowResizeArea
9191 anchors.fill: parent
9292 target: fakeWindow
93+ boundsItem: bounds
9394 borderThickness: units.gu(2)
9495 minWidth: units.gu(15)
9596 minHeight: units.gu(10)
@@ -121,7 +122,16 @@ Item {
121122 active: windowLoaderCheckbox.checked
122123 }
123124
125+ Item {
126+ id: bounds
127+ anchors.left: parent.left
128+ anchors.top: parent.top
129+ anchors.right: controls.left
130+ anchors.bottom: parent.bottom
131+ }
132+
124133 Rectangle {
134+ id: controls
125135 anchors.right: parent.right
126136 anchors.top: parent.top
127137 anchors.bottom: parent.bottom