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
17import QtQuick 2.12
18import Lomiri.Components 1.3
19import Lomiri.Gestures 0.1
20import "../../Components"
21
22Item {
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 } else {
130 if (root.closeable) {
131 if (value != 0)
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 = d.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}