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