on xenial help sign in
Add a WindowMoveResizeArea and tests for it

Makes moving/resizing windows work properly on all edges and allows to set min/max width/height on windows Approved by: Daniel d'Andrada, PS Jenkins bot

commit7564d82d877af2c8e514d6bb250ce617bf0c294c authorMichael Zanetti <michael.zanetti@canonical.com> dateWed 21 Jan 2015, 18:32 (12 years ago) parentbaa416252fd40b
4 files changed+245−44
qml/Stages/DesktopStage.qml+8−44 qml/Stages/WindowMoveResizeArea.qml+98 tests/qmltests/CMakeLists.txt+1 tests/qmltests/Stages/tst_WindowMoveResizeArea.qml+138
qml/Stages/DesktopStage.qml+8−44
@@ -94,6 +94,8 @@ Item {
9494 width: units.gu(60)
9595 height: units.gu(50)
9696
97+ readonly property int minWidth: units.gu(10)
98+ readonly property int minHeight: units.gu(10)
9799
98100 states: [
99101 State {
@@ -114,51 +116,13 @@ Item {
114116 }
115117 ]
116118
117− MouseArea {
118− anchors.fill: parent
119− anchors.margins: -units.gu(0.5)
120−
121− property bool resizeWidth: false
122− property bool resizeHeight: false
123−
124− property int startX: 0
125− property int startWidth: 0
126− property int startY: 0
127− property int startHeight: 0
128−
129− onPressed: {
130− ApplicationManager.requestFocusApplication(model.appId)
131− if (mouseX > width - units.gu(1)) {
132− resizeWidth = true;
133− startX = mouseX;
134− startWidth = appDelegate.width;
135− }
136− if (mouseY > height - units.gu(1)) {
137− resizeHeight = true;
138− startY = mouseY;
139− startHeight = appDelegate.height;
140− }
141− if (!resizeHeight && !resizeWidth) {
142− drag.target = appDelegate;
143− }
144− }
145−
146− onMouseXChanged: {
147− if (resizeWidth) {
148− appDelegate.width = startWidth + (mouseX - startX)
149− }
150− }
151− onMouseYChanged: {
152− if (resizeHeight) {
153− appDelegate.height = startHeight + (mouseY - startY)
154− }
155− }
119+ WindowMoveResizeArea {
120+ target: appDelegate
121+ minWidth: appDelegate.minWidth
122+ minHeight: appDelegate.minHeight
123+ resizeHandleWidth: units.gu(0.5)
156124
157− onReleased: {
158− resizeWidth = false;
159− resizeHeight = false;
160− drag.target = undefined;
161− }
125+ onPressed: ApplicationManager.requestFocusApplication(model.appId)
162126 }
163127
164128 DecoratedWindow {
qml/Stages/WindowMoveResizeArea.qmlnew file+98
@@ -0,0 +1,98 @@
1+/*
2+ * Copyright (C) 2014 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+ * Authors: Michael Zanetti <michael.zanetti@canonical.com>
17+ */
18+
19+import QtQuick 2.3
20+import Ubuntu.Components 1.1
21+
22+MouseArea {
23+ id: root
24+ anchors.fill: target
25+ anchors.margins: -resizeHandleWidth
26+
27+ // The target item managed by this. Must be a parent or a sibling
28+ // The area will anchor to it and manage move and resize events
29+ property var target: null
30+ property int resizeHandleWidth: 0
31+ property int minWidth: 0
32+ property int minHeight: 0
33+
34+ QtObject {
35+ id: priv
36+ readonly property int windowWidth: root.width - root.resizeHandleWidth * 2
37+ readonly property int windowHeight: root.height - resizeHandleWidth * 2
38+
39+ property var startPoint
40+ property int startWidth
41+ property int startHeight
42+
43+ property bool resizeTop: false
44+ property bool resizeBottom: false
45+ property bool resizeLeft: false
46+ property bool resizeRight: false
47+
48+ }
49+
50+ onPressed: {
51+ priv.startPoint = mapToItem(null, Qt.point(mouse.x, mouse.y)).x;
52+ priv.startWidth = root.width;
53+ priv.startHeight = root.height;
54+ priv.resizeTop = mouseY < root.resizeHandleWidth;
55+ priv.resizeBottom = mouseY > (root.height - root.resizeHandleWidth);
56+ priv.resizeLeft = mouseX < root.resizeHandleWidth;
57+ priv.resizeRight = mouseX > (root.width - root.resizeHandleWidth);
58+ }
59+
60+ onPositionChanged: {
61+ var currentPoint = mapToItem(null, Qt.point(mouse.x, mouse.y)).x;
62+ var mouseDiff = Qt.point(currentPoint.x - priv.startPoint.x, currentPoint.y - priv.startPoint.y);
63+ var moveDiff = Qt.point(0, 0);
64+ var sizeDiff = Qt.point(0, 0);
65+ if (priv.resizeTop || priv.resizeBottom || priv.resizeLeft || priv.resizeRight) {
66+ if (priv.resizeTop) {
67+ moveDiff.y += mouseDiff.y;
68+ sizeDiff.y += -mouseDiff.y;
69+ } else if (priv.resizeBottom) {
70+ sizeDiff.y += mouse.y - priv.startPoint.y;
71+ priv.startPoint.y = mouse.y
72+ }
73+ if (priv.resizeLeft) {
74+ moveDiff.x += mouseDiff.x;
75+ sizeDiff.x += -mouseDiff.x;
76+ } else if (priv.resizeRight) {
77+ sizeDiff.x += mouse.x - priv.startPoint.x;
78+ priv.startPoint.x = mouse.x
79+ }
80+ if (priv.windowWidth + sizeDiff.x < root.minWidth) {
81+ sizeDiff.x = root.minWidth - priv.windowWidth;
82+ moveDiff.x = moveDiff.x == 0 ? 0 : -sizeDiff.x;
83+ }
84+ if (priv.windowHeight + sizeDiff.y < root.minHeight) {
85+ sizeDiff.y = root.minHeight - priv.windowHeight;
86+ moveDiff.y = moveDiff.y == 0 ? 0 : -sizeDiff.y;
87+ }
88+
89+ target.x += moveDiff.x;
90+ target.y += moveDiff.y;
91+ target.width += sizeDiff.x;
92+ target.height += sizeDiff.y;
93+ } else {
94+ target.x += mouseDiff.x;
95+ target.y += mouseDiff.y;
96+ }
97+ }
98+}
tests/qmltests/CMakeLists.txt+1
@@ -88,5 +88,6 @@ 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 WindowMoveResizeArea)
9192add_qml_test(Stages Splash)
9293add_qml_test(Wizard Wizard)
tests/qmltests/Stages/tst_WindowMoveResizeArea.qmlnew file+138
@@ -0,0 +1,138 @@
1+/*
2+ * Copyright 2014 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.1
18+import QtQuick.Layouts 1.1
19+import QtTest 1.0
20+import Unity.Test 0.1 as UT
21+import ".."
22+import "../../../qml/Stages"
23+import Ubuntu.Components 0.1
24+import Ubuntu.Components.ListItems 1.0 as ListItem
25+import Unity.Application 0.1
26+
27+Item {
28+ id: root
29+ height: units.gu(60)
30+ width: units.gu(60)
31+
32+ Rectangle {
33+ id: fakeWindow
34+ height: units.gu(20)
35+ width: units.gu(20)
36+ color: "khaki"
37+ WindowMoveResizeArea {
38+ id: moveResizeArea
39+ target: fakeWindow
40+ resizeHandleWidth: units.gu(0.5)
41+ minWidth: units.gu(15)
42+ minHeight: units.gu(10)
43+ }
44+ }
45+
46+ UT.UnityTestCase {
47+ when: windowShown
48+
49+ function init() {
50+ fakeWindow.x = units.gu(20)
51+ fakeWindow.y = units.gu(20)
52+ fakeWindow.width = units.gu(20)
53+ fakeWindow.height = units.gu(20)
54+ }
55+
56+ function test_dragWindow_data() {
57+ return [
58+ { tag: "up", dx: 0, dy: units.gu(-10) },
59+ { tag: "down", dx: 0, dy: units.gu(10) },
60+ { tag: "left", dx: units.gu(-10), dy: 0 },
61+ { tag: "right", dx: units.gu(10), dy: 0 },
62+ { tag: "right/down", dx: units.gu(10), dy: units.gu(10) },
63+ { tag: "left/down", dx: units.gu(-10), dy: units.gu(10) }
64+ ]
65+ }
66+
67+ function test_dragWindow(data) {
68+ var initialWindowX = fakeWindow.x;
69+ var initialWindowY = fakeWindow.y;
70+ var initialWindowWidth = fakeWindow.width
71+ var initialWindowHeight = fakeWindow.height
72+
73+ var startDragX = initialWindowX + fakeWindow.width / 2;
74+ var startDragY = initialWindowY + fakeWindow.height / 2;
75+ mouseFlick(root, startDragX, startDragY, startDragX + data.dx, startDragY + data.dy, true, true, units.gu(.5), 10)
76+
77+ tryCompare(fakeWindow, "x", initialWindowX + data.dx)
78+ tryCompare(fakeWindow, "y", initialWindowX + data.dy)
79+
80+ compare(fakeWindow.height, initialWindowHeight);
81+ compare(fakeWindow.width, initialWindowWidth);
82+ }
83+
84+ function test_resizeWindowRightBottom_data() {
85+ return [
86+ { tag: "width", dx: units.gu(10), dy: 0 },
87+ { tag: "height", dx: 0, dy: units.gu(10) },
88+ { tag: "both", dx: units.gu(10), dy: units.gu(10) },
89+ { tag: "smaller than minimum size", dx: -units.gu(15), dy: -units.gu(15) }
90+ ]
91+ }
92+
93+ function test_resizeWindowRightBottom(data) {
94+ var initialWindowX = fakeWindow.x;
95+ var initialWindowY = fakeWindow.y;
96+ var initialWindowWidth = fakeWindow.width
97+ var initialWindowHeight = fakeWindow.height
98+
99+ var startDragX = initialWindowX + initialWindowWidth + 1
100+ var startDragY = initialWindowY + initialWindowHeight + 1
101+ mouseFlick(root, startDragX, startDragY, startDragX + data.dx, startDragY + data.dy, true, true, units.gu(.5), 10);
102+
103+ tryCompare(fakeWindow, "width", Math.max(initialWindowWidth + data.dx, moveResizeArea.minWidth));
104+ tryCompare(fakeWindow, "height", Math.max(initialWindowHeight + data.dy, moveResizeArea.minHeight));
105+
106+ compare(fakeWindow.x, initialWindowX);
107+ compare(fakeWindow.y, initialWindowY);
108+ }
109+
110+ function test_resizeWindowLeftTop_data() {
111+ return [
112+ { tag: "width", dx: -units.gu(10), dy: 0 },
113+ { tag: "height", dx: 0, dy: -units.gu(10) },
114+ { tag: "both", dx: -units.gu(10), dy: -units.gu(10) },
115+ { tag: "smaller than minimum size", dx: units.gu(15), dy: units.gu(15) }
116+ ]
117+ }
118+
119+ function test_resizeWindowLeftTop(data) {
120+ var initialWindowX = fakeWindow.x;
121+ var initialWindowY = fakeWindow.y;
122+ var initialWindowWidth = fakeWindow.width
123+ var initialWindowHeight = fakeWindow.height
124+
125+ var startDragX = initialWindowX - 1
126+ var startDragY = initialWindowY - 1
127+ mouseFlick(root, startDragX, startDragY, startDragX + data.dx, startDragY + data.dy, true, true, units.gu(.5), 10);
128+
129+ tryCompare(fakeWindow, "width", Math.max(initialWindowWidth - data.dx, moveResizeArea.minWidth));
130+ tryCompare(fakeWindow, "height", Math.max(initialWindowHeight - data.dy, moveResizeArea.minHeight));
131+
132+ var maxMoveX = initialWindowWidth - moveResizeArea.minWidth;
133+ var maxMoveY = initialWindowHeight - moveResizeArea.minHeight;
134+ compare(fakeWindow.x, Math.min(initialWindowX + data.dx, initialWindowX + maxMoveX));
135+ compare(fakeWindow.y, Math.min(initialWindowY + data.dy, initialWindowY + maxMoveY));
136+ }
137+ }
138+}