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  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  function loadWindowState() {
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  switch (windowState) {
85  case WindowStateStorage.WindowStateNormal:
86  break;
87  case WindowStateStorage.WindowStateMaximized:
88  target.maximize(false);
89  break;
90  case WindowStateStorage.WindowStateMaximizedLeft:
91  target.maximizeLeft(false);
92  break;
93  case WindowStateStorage.WindowStateMaximizedRight:
94  target.maximizeRight(false);
95  break;
96  case WindowStateStorage.WindowStateMaximizedHorizontally:
97  target.maximizeHorizontally(false);
98  break;
99  case WindowStateStorage.WindowStateMaximizedVertically:
100  target.maximizeVertically(false);
101  break;
102  default:
103  console.warn("Unsupported window state");
104  break;
105  }
106 
107  priv.updateNormalGeometry();
108  }
109 
110  function saveWindowState() {
111  windowStateStorage.saveState(root.windowId, target.windowState & ~WindowStateStorage.WindowStateMinimized); // clear the minimized bit when saving
112  windowStateStorage.saveGeometry(root.windowId, Qt.rect(priv.normalX, priv.normalY, priv.normalWidth, priv.normalHeight));
113  }
114 
115  QtObject {
116  id: d
117 
118  readonly property int maxSafeInt: 2147483647
119  readonly property int maxSizeIncrement: units.gu(40)
120 
121  readonly property int minimumWidth: root.target ? Math.max(root.minWidth, root.target.minimumWidth) : root.minWidth
122  onMinimumWidthChanged: {
123  if (target.requestedWidth < minimumWidth) {
124  target.requestedWidth = minimumWidth;
125  }
126  }
127  readonly property int minimumHeight: root.target ? Math.max(root.minHeight, root.target.minimumHeight) : root.minHeight
128  onMinimumHeightChanged: {
129  if (target.requestedHeight < minimumHeight) {
130  target.requestedHeight = minimumHeight;
131  }
132  }
133  readonly property int maximumWidth: root.target && root.target.maximumWidth >= minimumWidth && root.target.maximumWidth > 0
134  ? root.target.maximumWidth : maxSafeInt
135  onMaximumWidthChanged: {
136  if (target.requestedWidth > maximumWidth) {
137  target.requestedWidth = maximumWidth;
138  }
139  }
140  readonly property int maximumHeight: root.target && root.target.maximumHeight >= minimumHeight && root.target.maximumHeight > 0
141  ? root.target.maximumHeight : maxSafeInt
142  onMaximumHeightChanged: {
143  if (target.requestedHeight > maximumHeight) {
144  target.requestedHeight = maximumHeight;
145  }
146  }
147  readonly property int widthIncrement: {
148  if (!root.target) {
149  return 1;
150  }
151  if (root.target.widthIncrement > 0) {
152  if (root.target.widthIncrement < maxSizeIncrement) {
153  return root.target.widthIncrement;
154  } else {
155  return maxSizeIncrement;
156  }
157  } else {
158  return 1;
159  }
160  }
161  readonly property int heightIncrement: {
162  if (!root.target) {
163  return 1;
164  }
165  if (root.target.heightIncrement > 0) {
166  if (root.target.heightIncrement < maxSizeIncrement) {
167  return root.target.heightIncrement;
168  } else {
169  return maxSizeIncrement;
170  }
171  } else {
172  return 1;
173  }
174  }
175 
176  property bool leftBorder: false
177  property bool rightBorder: false
178  property bool topBorder: false
179  property bool bottomBorder: false
180 
181  // true - A change in surface size will cause the left border of the window to move accordingly.
182  // The window's right border will stay in the same position.
183  // false - a change in surface size will cause the right border of the window to move accordingly.
184  // The window's left border will stay in the same position.
185  property bool moveLeftBorder: false
186 
187  // true - A change in surface size will cause the top border of the window to move accordingly.
188  // The window's bottom border will stay in the same position.
189  // false - a change in surface size will cause the bottom border of the window to move accordingly.
190  // The window's top border will stay in the same position.
191  property bool moveTopBorder: false
192 
193  property bool dragging: false
194  property real startMousePosX
195  property real startMousePosY
196  property real startX
197  property real startY
198  property real startWidth
199  property real startHeight
200  property real currentWidth
201  property real currentHeight
202 
203  property string cursorName: {
204  if (root.containsMouse || root.pressed) {
205  if (leftBorder && !topBorder && !bottomBorder) {
206  return "left_side";
207  } else if (rightBorder && !topBorder && !bottomBorder) {
208  return "right_side";
209  } else if (topBorder && !leftBorder && !rightBorder) {
210  return "top_side";
211  } else if (bottomBorder && !leftBorder && !rightBorder) {
212  return "bottom_side";
213  } else if (leftBorder && topBorder) {
214  return "top_left_corner";
215  } else if (leftBorder && bottomBorder) {
216  return "bottom_left_corner";
217  } else if (rightBorder && topBorder) {
218  return "top_right_corner";
219  } else if (rightBorder && bottomBorder) {
220  return "bottom_right_corner";
221  } else {
222  return "";
223  }
224  } else {
225  return "";
226  }
227  }
228  onCursorNameChanged: {
229  Mir.cursorName = cursorName;
230  }
231 
232  function updateBorders() {
233  leftBorder = mouseX <= borderThickness;
234  rightBorder = mouseX >= width - borderThickness;
235  topBorder = mouseY <= borderThickness;
236  bottomBorder = mouseY >= height - borderThickness;
237  }
238  }
239 
240  Timer {
241  id: resetBordersToMoveTimer
242  interval: 2000
243  onTriggered: {
244  d.moveLeftBorder = false;
245  d.moveTopBorder = false;
246  }
247  }
248 
249  onPressedChanged: {
250  var pos = mapToItem(target.parent, mouseX, mouseY);
251 
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.x;
262  d.startY = target.y;
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.x + 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.y + 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.x += d.currentWidth - target.width;
355  }
356  d.currentWidth = target.width;
357  }
358  onHeightChanged: {
359  if (d.moveTopBorder) {
360  target.y += d.currentHeight - target.height;
361  }
362  d.currentHeight = target.height;
363  }
364  }
365 }