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.4
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 (!d.moving || !tp.pressed) {
110  if (Math.abs(startY - tp.y) > d.threshold) {
111  d.moving = true;
112  d.dragEvents = []
113  offset = tp.y - tp.startY;
114  } else {
115  return;
116  }
117  }
118 
119 
120  var value = tp.y - tp.startY - offset;
121  if (value < 0 && stage.workspaceEnabled) {
122  var coords = mapToItem(stage, tp.x, tp.y);
123  dragDelegate.Drag.hotSpot.x = dragDelegate.width / 2
124  dragDelegate.Drag.hotSpot.y = units.gu(2)
125  dragDelegate.x = coords.x - dragDelegate.Drag.hotSpot.x
126  dragDelegate.y = coords.y - dragDelegate.Drag.hotSpot.y
127  dragDelegate.Drag.active = true;
128  dragDelegate.surface = model.window.surface;
129 
130  } else {
131  if (root.closeable) {
132  d.distance = value
133  } else {
134  d.distance = Math.sqrt(Math.abs(value)) * (value < 0 ? -1 : 1) * 3
135  }
136  }
137 
138  d.pushDragEvent(tp);
139  }
140 
141  onReleased: {
142  var result = dragDelegate.Drag.drop();
143  dragDelegate.surface = null;
144 
145  if (!d.moving) {
146  root.clicked()
147  }
148 
149  if (!root.closeable) {
150  animation.animate("center")
151  return;
152  }
153 
154  var touchPoint = touchPoints[0];
155 
156  if ((d.dragVelocity < -root.minSpeedToClose && d.distance < -units.gu(8)) || d.distance < -root.height / 2) {
157  animation.animate("up")
158  } else if ((d.dragVelocity > root.minSpeedToClose && d.distance > units.gu(8)) || d.distance > root.height / 2) {
159  animation.animate("down")
160  } else {
161  animation.animate("center")
162  }
163  }
164 
165  onCanceled: {
166  dragDelegate.Drag.active = false;
167  dragDelegate.surface = null;
168  d.moving = false
169  animation.animate("center");
170  }
171  }
172 
173  LomiriNumberAnimation {
174  id: animation
175  objectName: "closeAnimation"
176  target: d
177  property: "distance"
178  property bool requestClose: false
179 
180  function animate(direction) {
181  animation.from = dragArea.distance;
182  switch (direction) {
183  case "up":
184  animation.to = -root.height * 1.5;
185  requestClose = true;
186  break;
187  case "down":
188  animation.to = root.height * 1.5;
189  requestClose = true;
190  break;
191  default:
192  animation.to = 0
193  }
194  animation.start();
195  }
196 
197  onRunningChanged: {
198  if (!running) {
199  d.moving = false;
200  if (requestClose) {
201  root.close();
202  } else {
203  d.distance = 0;
204  }
205  }
206  }
207  }
208 }