Lomiri
SpreadDelegateInputArea.qml
1 /*
2  * Copyright (C) 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.12
18 import Lomiri.Components 1.3
19 import Lomiri.Gestures 0.1
20 import "../../Components"
21 
22 Item {
23  id: root
24 
25  property bool closeable: true
26  readonly property real minSpeedToClose: units.gu(40)
27  property bool zeroVelocityCounts: false
28 
29  readonly property alias distance: d.distance
30 
31  property var stage: null
32  property var dragDelegate: null
33 
34  signal clicked()
35  signal close()
36 
37  QtObject {
38  id: d
39  property real distance: 0
40  property bool moving: false
41  property var dragEvents: []
42  property real dragVelocity: 0
43  property int threshold: units.gu(2)
44 
45  // Can be replaced with a fake implementation during tests
46  // property var __getCurrentTimeMs: function () { return new Date().getTime() }
47  property var __dateTime: new function() {
48  this.getCurrentTimeMs = function() {return new Date().getTime()}
49  }
50 
51  function pushDragEvent(event) {
52  var currentTime = __dateTime.getCurrentTimeMs()
53  dragEvents.push([currentTime, event.x - event.startX, event.y - event.startY, getEventSpeed(currentTime, event)])
54  cullOldDragEvents(currentTime)
55  updateSpeed()
56  }
57 
58  function cullOldDragEvents(currentTime) {
59  // cull events older than 50 ms but always keep the latest 2 events
60  for (var numberOfCulledEvents = 0; numberOfCulledEvents < dragEvents.length-2; numberOfCulledEvents++) {
61  // dragEvents[numberOfCulledEvents][0] is the dragTime
62  if (currentTime - dragEvents[numberOfCulledEvents][0] <= 50) break
63  }
64 
65  dragEvents.splice(0, numberOfCulledEvents)
66  }
67 
68  function updateSpeed() {
69  var totalSpeed = 0
70  for (var i = 0; i < dragEvents.length; i++) {
71  totalSpeed += dragEvents[i][3]
72  }
73 
74  if (zeroVelocityCounts || Math.abs(totalSpeed) > 0.001) {
75  dragVelocity = totalSpeed / dragEvents.length * 1000
76  }
77  }
78 
79  function getEventSpeed(currentTime, event) {
80  if (dragEvents.length != 0) {
81  var lastDrag = dragEvents[dragEvents.length-1]
82  var duration = Math.max(1, currentTime - lastDrag[0])
83  return (event.y - event.startY - lastDrag[2]) / duration
84  } else {
85  return 0
86  }
87  }
88  }
89 
90  MultiPointTouchArea {
91  anchors.fill: parent
92  maximumTouchPoints: 1
93  property int offset: 0
94 
95  // tp.startY seems to be broken for mouse interaction... lets track it ourselves
96  property int startY: 0
97 
98  touchPoints: [
99  TouchPoint {
100  id: tp
101  }
102  ]
103 
104  onPressed: {
105  startY = tp.y
106  }
107 
108  onTouchUpdated: {
109  if (!tp.pressed) {
110  dragDelegate.Drag.active = false;
111  dragDelegate.surface = null;
112  d.moving = false
113  animation.animate("center");
114  return;
115  } else if (!d.moving) {
116  if (Math.abs(startY - tp.y) > d.threshold) {
117  d.moving = true;
118  d.dragEvents = []
119  offset = tp.y - tp.startY;
120  } else {
121  return;
122  }
123  }
124 
125  var value = tp.y - tp.startY - offset;
126  if (value < 0 && stage.workspaceEnabled) {
127  var coords = mapToItem(stage, tp.x, tp.y);
128  dragDelegate.Drag.hotSpot.x = dragDelegate.width / 2
129  dragDelegate.Drag.hotSpot.y = units.gu(2)
130  dragDelegate.x = coords.x - dragDelegate.Drag.hotSpot.x
131  dragDelegate.y = coords.y - dragDelegate.Drag.hotSpot.y
132  dragDelegate.Drag.active = true;
133  dragDelegate.surface = model.window.surface;
134  } else {
135  if (root.closeable) {
136  if (value != 0)
137  d.distance = value
138  } else {
139  d.distance = Math.sqrt(Math.abs(value)) * (value < 0 ? -1 : 1) * 3
140  }
141  }
142 
143  d.pushDragEvent(tp);
144  }
145 
146  onReleased: {
147  var result = dragDelegate.Drag.drop();
148  dragDelegate.surface = null;
149 
150  if (!d.moving) {
151  root.clicked()
152  }
153 
154  if (!root.closeable) {
155  animation.animate("center")
156  return;
157  }
158 
159  if ((d.dragVelocity < -root.minSpeedToClose && d.distance < -units.gu(8)) || d.distance < -root.height / 2) {
160  animation.animate("up")
161  } else if ((d.dragVelocity > root.minSpeedToClose && d.distance > units.gu(8)) || d.distance > root.height / 2) {
162  animation.animate("down")
163  } else {
164  animation.animate("center")
165  }
166  }
167 
168  onCanceled: {
169  dragDelegate.Drag.active = false;
170  dragDelegate.surface = null;
171  d.moving = false
172  animation.animate("center");
173  }
174  }
175 
176  LomiriNumberAnimation {
177  id: animation
178  objectName: "closeAnimation"
179  target: d
180  property: "distance"
181  property bool requestClose: false
182 
183  function animate(direction) {
184  animation.from = d.distance;
185  switch (direction) {
186  case "up":
187  animation.to = -root.height * 1.5;
188  requestClose = true;
189  break;
190  case "down":
191  animation.to = root.height * 1.5;
192  requestClose = true;
193  break;
194  default:
195  animation.to = 0
196  }
197  animation.start();
198  }
199 
200  onRunningChanged: {
201  if (!running) {
202  d.moving = false;
203  if (requestClose) {
204  root.close();
205  } else {
206  d.distance = 0;
207  }
208  }
209  }
210  }
211 }