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