Unity 8
WindowResizeArea.qml
1 /*
2  * Copyright (C) 2014-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.4
18 import Ubuntu.Components 1.3
19 import Utils 0.1
20 import Unity.Application 0.1 // for Mir.cursorName
21 import "../Components/PanelState"
22 
23 MouseArea {
24  id: root
25  anchors.fill: target
26  anchors.margins: -borderThickness
27 
28  hoverEnabled: target && !target.maximized // don't grab the resize under the panel
29 
30  property var windowStateStorage: WindowStateStorage
31 
32  // The target item managed by this. Must be a parent or a sibling
33  // The area will anchor to it and manage move and resize events
34  property Item target: null
35  property string windowId: ""
36  property int borderThickness: 0
37  property int minWidth: 0
38  property int minHeight: 0
39  property int defaultWidth: units.gu(60)
40  property int defaultHeight: units.gu(50)
41  property int screenWidth: 0
42  property int screenHeight: 0
43  property int leftMargin: 0
44 
45  QtObject {
46  id: priv
47  objectName: "priv"
48 
49  property int normalX: 0
50  property int normalY: 0
51  property int normalWidth: 0
52  property int normalHeight: 0
53 
54  function updateNormalGeometry() {
55  if (root.target.state == "normal") {
56  normalX = root.target.x
57  normalY = root.target.y
58  normalWidth = root.target.width
59  normalHeight = root.target.height
60  }
61  }
62  }
63 
64  Connections {
65  target: root.target
66  onXChanged: priv.updateNormalGeometry();
67  onYChanged: priv.updateNormalGeometry();
68  onWidthChanged: priv.updateNormalGeometry();
69  onHeightChanged: priv.updateNormalGeometry();
70  }
71 
72  Component.onCompleted: {
73  var windowGeometry = windowStateStorage.getGeometry(root.windowId,
74  Qt.rect(target.x, target.y, defaultWidth, defaultHeight));
75 
76  target.requestedWidth = Qt.binding(function() { return Math.min(Math.max(windowGeometry.width, d.minimumWidth), screenWidth - root.leftMargin); });
77  target.requestedHeight = Qt.binding(function() { return Math.min(Math.max(windowGeometry.height, d.minimumHeight),
78  root.screenHeight - (target.fullscreen ? 0 : PanelState.panelHeight)); });
79  target.x = Qt.binding(function() { return Math.max(Math.min(windowGeometry.x, root.screenWidth - root.leftMargin - target.requestedWidth),
80  (target.fullscreen ? 0 : root.leftMargin)); });
81  target.y = Qt.binding(function() { return Math.max(Math.min(windowGeometry.y, root.screenHeight - target.requestedHeight), PanelState.panelHeight); });
82 
83  var windowState = windowStateStorage.getState(root.windowId, WindowStateStorage.WindowStateNormal)
84  if (windowState === WindowStateStorage.WindowStateMaximized) {
85  target.maximize(false)
86  }
87  priv.updateNormalGeometry();
88  }
89 
90  Component.onDestruction: {
91  windowStateStorage.saveState(root.windowId, target.state == "maximized" ? WindowStateStorage.WindowStateMaximized : WindowStateStorage.WindowStateNormal)
92  windowStateStorage.saveGeometry(root.windowId, Qt.rect(priv.normalX, priv.normalY, priv.normalWidth, priv.normalHeight))
93  }
94 
95  QtObject {
96  id: d
97 
98  readonly property int maxSafeInt: 2147483647
99  readonly property int maxSizeIncrement: units.gu(40)
100 
101  readonly property int minimumWidth: root.target ? Math.max(root.minWidth, root.target.minimumWidth) : root.minWidth
102  onMinimumWidthChanged: {
103  if (target.requestedWidth < minimumWidth) {
104  target.requestedWidth = minimumWidth;
105  }
106  }
107  readonly property int minimumHeight: root.target ? Math.max(root.minHeight, root.target.minimumHeight) : root.minHeight
108  onMinimumHeightChanged: {
109  if (target.requestedHeight < minimumHeight) {
110  target.requestedHeight = minimumHeight;
111  }
112  }
113  readonly property int maximumWidth: root.target && root.target.maximumWidth >= minimumWidth && root.target.maximumWidth > 0
114  ? root.target.maximumWidth : maxSafeInt
115  onMaximumWidthChanged: {
116  if (target.requestedWidth > maximumWidth) {
117  target.requestedWidth = maximumWidth;
118  }
119  }
120  readonly property int maximumHeight: root.target && root.target.maximumHeight >= minimumHeight && root.target.maximumHeight > 0
121  ? root.target.maximumHeight : maxSafeInt
122  onMaximumHeightChanged: {
123  if (target.requestedHeight > maximumHeight) {
124  target.requestedHeight = maximumHeight;
125  }
126  }
127  readonly property int widthIncrement: {
128  if (!root.target) {
129  return 1;
130  }
131  if (root.target.widthIncrement > 0) {
132  if (root.target.widthIncrement < maxSizeIncrement) {
133  return root.target.widthIncrement;
134  } else {
135  return maxSizeIncrement;
136  }
137  } else {
138  return 1;
139  }
140  }
141  readonly property int heightIncrement: {
142  if (!root.target) {
143  return 1;
144  }
145  if (root.target.heightIncrement > 0) {
146  if (root.target.heightIncrement < maxSizeIncrement) {
147  return root.target.heightIncrement;
148  } else {
149  return maxSizeIncrement;
150  }
151  } else {
152  return 1;
153  }
154  }
155 
156  property bool leftBorder: false
157  property bool rightBorder: false
158  property bool topBorder: false
159  property bool bottomBorder: false
160 
161  // true - A change in surface size will cause the left border of the window to move accordingly.
162  // The window's right border will stay in the same position.
163  // false - a change in surface size will cause the right border of the window to move accordingly.
164  // The window's left border will stay in the same position.
165  property bool moveLeftBorder: false
166 
167  // true - A change in surface size will cause the top border of the window to move accordingly.
168  // The window's bottom border will stay in the same position.
169  // false - a change in surface size will cause the bottom border of the window to move accordingly.
170  // The window's top border will stay in the same position.
171  property bool moveTopBorder: false
172 
173  property bool dragging: false
174  property real startMousePosX
175  property real startMousePosY
176  property real startX
177  property real startY
178  property real startWidth
179  property real startHeight
180  property real currentWidth
181  property real currentHeight
182 
183  property string cursorName: {
184  if (root.containsMouse || root.pressed) {
185  if (leftBorder && !topBorder && !bottomBorder) {
186  return "left_side";
187  } else if (rightBorder && !topBorder && !bottomBorder) {
188  return "right_side";
189  } else if (topBorder && !leftBorder && !rightBorder) {
190  return "top_side";
191  } else if (bottomBorder && !leftBorder && !rightBorder) {
192  return "bottom_side";
193  } else if (leftBorder && topBorder) {
194  return "top_left_corner";
195  } else if (leftBorder && bottomBorder) {
196  return "bottom_left_corner";
197  } else if (rightBorder && topBorder) {
198  return "top_right_corner";
199  } else if (rightBorder && bottomBorder) {
200  return "bottom_right_corner";
201  } else {
202  return "";
203  }
204  } else {
205  return "";
206  }
207  }
208  onCursorNameChanged: {
209  Mir.cursorName = cursorName;
210  }
211 
212  function updateBorders() {
213  leftBorder = mouseX <= borderThickness;
214  rightBorder = mouseX >= width - borderThickness;
215  topBorder = mouseY <= borderThickness;
216  bottomBorder = mouseY >= height - borderThickness;
217  }
218  }
219 
220  Timer {
221  id: resetBordersToMoveTimer
222  interval: 2000
223  onTriggered: {
224  d.moveLeftBorder = false;
225  d.moveTopBorder = false;
226  }
227  }
228 
229  onPressedChanged: {
230  var pos = mapToItem(target.parent, mouseX, mouseY);
231 
232  if (pressed) {
233  d.updateBorders();
234  resetBordersToMoveTimer.stop();
235  d.moveLeftBorder = d.leftBorder;
236  d.moveTopBorder = d.topBorder;
237 
238  var pos = mapToItem(root.target.parent, mouseX, mouseY);
239  d.startMousePosX = pos.x;
240  d.startMousePosY = pos.y;
241  d.startX = target.x;
242  d.startY = target.y;
243  d.startWidth = target.width;
244  d.startHeight = target.height;
245  d.currentWidth = target.width;
246  d.currentHeight = target.height;
247  d.dragging = true;
248  } else {
249  resetBordersToMoveTimer.start();
250  d.dragging = false;
251  if (containsMouse) {
252  d.updateBorders();
253  }
254  }
255  }
256 
257  onEntered: {
258  if (!pressed) {
259  d.updateBorders();
260  }
261  }
262 
263  onPositionChanged: {
264  if (!pressed) {
265  d.updateBorders();
266  }
267 
268  if (!d.dragging) {
269  return;
270  }
271 
272  var pos = mapToItem(target.parent, mouse.x, mouse.y);
273 
274  var deltaX = Math.floor((pos.x - d.startMousePosX) / d.widthIncrement) * d.widthIncrement;
275  var deltaY = Math.floor((pos.y - d.startMousePosY) / d.heightIncrement) * d.heightIncrement;
276 
277  if (d.leftBorder) {
278  var newTargetX = d.startX + deltaX;
279  var rightBorderX = target.x + target.width;
280  if (rightBorderX > newTargetX + d.minimumWidth) {
281  if (rightBorderX < newTargetX + d.maximumWidth) {
282  target.requestedWidth = rightBorderX - newTargetX;
283  } else {
284  target.requestedWidth = d.maximumWidth;
285  }
286  } else {
287  target.requestedWidth = d.minimumWidth;
288  }
289 
290  } else if (d.rightBorder) {
291  var newWidth = d.startWidth + deltaX;
292  if (newWidth > d.minimumWidth) {
293  if (newWidth < d.maximumWidth) {
294  target.requestedWidth = newWidth;
295  } else {
296  target.requestedWidth = d.maximumWidth;
297  }
298  } else {
299  target.requestedWidth = d.minimumWidth;
300  }
301  }
302 
303  if (d.topBorder) {
304  var newTargetY = Math.max(d.startY + deltaY, PanelState.panelHeight); // disallow resizing up past Panel
305  var bottomBorderY = target.y + target.height;
306  if (bottomBorderY > newTargetY + d.minimumHeight) {
307  if (bottomBorderY < newTargetY + d.maximumHeight) {
308  target.requestedHeight = bottomBorderY - newTargetY;
309  } else {
310  target.requestedHeight = d.maximumHeight;
311  }
312  } else {
313  target.requestedHeight = d.minimumHeight;
314  }
315 
316  } else if (d.bottomBorder) {
317  var newHeight = d.startHeight + deltaY;
318  if (newHeight > d.minimumHeight) {
319  if (newHeight < d.maximumHeight) {
320  target.requestedHeight = newHeight;
321  } else {
322  target.requestedHeight = d.maximumHeight;
323  }
324  } else {
325  target.requestedHeight = d.minimumHeight;
326  }
327  }
328  }
329 
330  Connections {
331  target: root.target
332  onWidthChanged: {
333  if (d.moveLeftBorder) {
334  target.x += d.currentWidth - target.width;
335  }
336  d.currentWidth = target.width;
337  }
338  onHeightChanged: {
339  if (d.moveTopBorder) {
340  target.y += d.currentHeight - target.height;
341  }
342  d.currentHeight = target.height;
343  }
344  }
345 }