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