Lomiri
MoveHandler.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 QtMir.Application 0.1 // For Mir singleton
19 import Lomiri.Components 1.3
20 import Utils 0.1
21 import "../Components"
22 
23 QtObject {
24  id: root
25 
26  property Item target // appDelegate
27  property real buttonsWidth: 0
28  property Item boundsItem
29  property real boundsTopMargin: 0
30 
31  readonly property bool dragging: priv.dragging
32  readonly property bool moving: priv.moving
33 
34  signal fakeMaximizeAnimationRequested(real amount)
35  signal fakeMaximizeLeftAnimationRequested(real amount)
36  signal fakeMaximizeRightAnimationRequested(real amount)
37  signal fakeMaximizeTopLeftAnimationRequested(real amount)
38  signal fakeMaximizeTopRightAnimationRequested(real amount)
39  signal fakeMaximizeBottomLeftAnimationRequested(real amount)
40  signal fakeMaximizeBottomRightAnimationRequested(real amount)
41  signal stopFakeAnimation()
42 
43  property QtObject priv: QtObject {
44  property real distanceX
45  property real distanceY
46  property bool dragging
47  property bool moving
48 
49  readonly property int triggerArea: units.gu(8)
50  property bool nearLeftEdge: target && target.maximizedLeft
51  property bool nearTopEdge: target && target.maximized
52  property bool nearRightEdge: target && target.maximizedRight
53  property bool nearTopLeftCorner: target && target.maximizedTopLeft
54  property bool nearTopRightCorner: target && target.maximizedTopRight
55  property bool nearBottomLeftCorner: target && target.maximizedBottomLeft
56  property bool nearBottomRightCorner: target && target.maximizedBottomRight
57 
58  property Timer mouseDownTimer: Timer {
59  interval: 175
60  onTriggered: Mir.cursorName = "grabbing"
61  }
62 
63  function resetEdges() {
64  nearLeftEdge = false;
65  nearRightEdge = false;
66  nearTopEdge = false;
67  nearTopLeftCorner = false;
68  nearTopRightCorner = false;
69  nearBottomLeftCorner = false;
70  nearBottomRightCorner = false;
71  }
72 
73  // return the progress of mouse pointer movement from 0 to 1 within a corner square of the screen
74  // 0 -> before the mouse enters the square
75  // 1 -> mouse is in the outer corner
76  // a is the corner, b is the mouse pos
77  function progressInCorner(ax, ay, bx, by) {
78  // distance of two points, a and b, in pixels
79  var distance = Math.sqrt(Math.pow(bx-ax, 2) + Math.pow(by-ay, 2));
80  // length of the triggerArea square diagonal
81  var diagLength = Math.sqrt(2 * priv.triggerArea * priv.triggerArea);
82  var ratio = 1 - (distance / diagLength);
83 
84  // everything "outside" of our square from the center is 1
85  var mousePosBoundsCoords = target.mapToItem(root.boundsItem, bx, by);
86  return root.boundsItem.contains(mousePosBoundsCoords) ? ratio : 1;
87  }
88  property real progress: 0
89  }
90 
91  function handlePressedChanged(pressed, pressedButtons, mouseX, mouseY) {
92  if (pressed && pressedButtons === Qt.LeftButton) {
93  var pos = mapToItem(target, mouseX, mouseY);
94  if (target.anyMaximized) {
95  // keep distanceX relative to the normal window width minus the window control buttons (+spacing)
96  // so that dragging it back doesn't make the window jump around to weird positions, away from the mouse pointer
97  priv.distanceX = MathUtils.clampAndProject(pos.x, 0, target.width, buttonsWidth, target.normalWidth);
98  priv.distanceY = MathUtils.clampAndProject(pos.y, 0, target.height, 0, target.normalHeight);
99  } else {
100  priv.distanceX = pos.x;
101  priv.distanceY = pos.y;
102  }
103 
104  priv.dragging = true;
105  priv.mouseDownTimer.start();
106  } else {
107  priv.dragging = false;
108  priv.mouseDownTimer.stop();
109  Mir.cursorName = "";
110  }
111  }
112 
113  function handlePositionChanged(mouse, sensingPoints) {
114  if (priv.dragging) {
115  priv.moving = true;
116  priv.mouseDownTimer.stop();
117  Mir.cursorName = "grabbing";
118 
119  // restore from maximized when dragging away from edges/corners; guard against inadvertent changes when going into maximized state
120  if (target.anyMaximized && !target.windowedTransitionRunning) {
121  priv.progress = 0;
122  target.requestRestore();
123  }
124 
125  var pos = mapToItem(target.parent, mouse.x, mouse.y); // How can that work if we're just a QtObject (not an Item)?
126  var bounds = boundsItem.mapToItem(target.parent, 0, 0, boundsItem.width, boundsItem.height);
127  bounds.y += boundsTopMargin;
128  bounds.height -= boundsTopMargin;
129  // Use integer coordinate values to ensure that target is left in a pixel-aligned
130  // position. Mouse movement could have subpixel precision, yielding a fractional
131  // mouse position.
132  target.windowedX = Math.round(pos.x - priv.distanceX);
133  target.windowedY = Math.round(Math.max(pos.y - priv.distanceY, bounds.top));
134 
135  if (sensingPoints) { // edge/corner detection when dragging via the touch overlay
136  if (sensingPoints.topLeft.x < priv.triggerArea && sensingPoints.topLeft.y < bounds.top + priv.triggerArea
137  && target.canBeCornerMaximized) { // top left
138  priv.progress = priv.progressInCorner(bounds.left, bounds.top, sensingPoints.topLeft.x, sensingPoints.topLeft.y);
139  priv.resetEdges();
140  priv.nearTopLeftCorner = true;
141  root.fakeMaximizeTopLeftAnimationRequested(priv.progress);
142  } else if (sensingPoints.topRight.x > bounds.right - priv.triggerArea && sensingPoints.topRight.y < bounds.top + priv.triggerArea
143  && target.canBeCornerMaximized) { // top right
144  priv.progress = priv.progressInCorner(bounds.right, bounds.top, sensingPoints.topRight.x, sensingPoints.topRight.y);
145  priv.resetEdges();
146  priv.nearTopRightCorner = true;
147  root.fakeMaximizeTopRightAnimationRequested(priv.progress);
148  } else if (sensingPoints.bottomLeft.x < priv.triggerArea && sensingPoints.bottomLeft.y > bounds.bottom - priv.triggerArea
149  && target.canBeCornerMaximized) { // bottom left
150  priv.progress = priv.progressInCorner(bounds.left, bounds.bottom, sensingPoints.bottomLeft.x, sensingPoints.bottomLeft.y);
151  priv.resetEdges();
152  priv.nearBottomLeftCorner = true;
153  root.fakeMaximizeBottomLeftAnimationRequested(priv.progress);
154  } else if (sensingPoints.bottomRight.x > bounds.right - priv.triggerArea && sensingPoints.bottomRight.y > bounds.bottom - priv.triggerArea
155  && target.canBeCornerMaximized) { // bottom right
156  priv.progress = priv.progressInCorner(bounds.right, bounds.bottom, sensingPoints.bottomRight.x, sensingPoints.bottomRight.y);
157  priv.resetEdges();
158  priv.nearBottomRightCorner = true;
159  root.fakeMaximizeBottomRightAnimationRequested(priv.progress);
160  } else if (sensingPoints.left.x < priv.triggerArea && target.canBeMaximizedLeftRight) { // left
161  priv.progress = MathUtils.clampAndProject(sensingPoints.left.x, priv.triggerArea, 0, 0, 1);
162  priv.resetEdges();
163  priv.nearLeftEdge = true;
164  root.fakeMaximizeLeftAnimationRequested(priv.progress);
165  } else if (sensingPoints.right.x > bounds.right - priv.triggerArea && target.canBeMaximizedLeftRight) { // right
166  priv.progress = MathUtils.clampAndProject(sensingPoints.right.x, bounds.right - priv.triggerArea, bounds.right, 0, 1);
167  priv.resetEdges();
168  priv.nearRightEdge = true;
169  root.fakeMaximizeRightAnimationRequested(priv.progress);
170  } else if (sensingPoints.top.y < bounds.top + priv.triggerArea && target.canBeMaximized) { // top
171  priv.progress = MathUtils.clampAndProject(sensingPoints.top.y, bounds.top + priv.triggerArea, 0, 0, 1);
172  priv.resetEdges();
173  priv.nearTopEdge = true;
174  root.fakeMaximizeAnimationRequested(priv.progress);
175  } else if (priv.nearLeftEdge || priv.nearRightEdge || priv.nearTopEdge || priv.nearTopLeftCorner || priv.nearTopRightCorner ||
176  priv.nearBottomLeftCorner || priv.nearBottomRightCorner) {
177  priv.progress = 0;
178  priv.resetEdges();
179  root.stopFakeAnimation();
180  }
181  }
182  }
183  }
184 
185  function handleReleased(touchMode) {
186  priv.moving = false;
187  if (touchMode) {
188  priv.progress = 0;
189  priv.resetEdges();
190  }
191  }
192 
193  function cancelDrag() {
194  priv.dragging = false;
195  root.stopFakeAnimation();
196  priv.mouseDownTimer.stop();
197  Mir.cursorName = "";
198  priv.progress = 0;
199  priv.resetEdges();
200  }
201 }