|
1 /* This Source Code Form is subject to the terms of the Mozilla Public |
|
2 * License, v. 2.0. If a copy of the MPL was not distributed with this file, |
|
3 * You can obtain one at http://mozilla.org/MPL/2.0/. */ |
|
4 |
|
5 /* global Components, GestureSettings, XPCOMUtils, Utils, Promise, Logger */ |
|
6 /* exported GestureSettings, GestureTracker */ |
|
7 |
|
8 /****************************************************************************** |
|
9 All gestures have the following pathways when being resolved(v)/rejected(x): |
|
10 Tap -> DoubleTap (v) |
|
11 -> Dwell (x) |
|
12 -> Swipe (x) |
|
13 |
|
14 AndroidTap -> TripleTap (v) |
|
15 -> TapHold (x) |
|
16 -> Swipe (x) |
|
17 |
|
18 DoubleTap -> TripleTap (v) |
|
19 -> TapHold (x) |
|
20 -> Explore (x) |
|
21 |
|
22 TripleTap -> DoubleTapHold (x) |
|
23 -> Explore (x) |
|
24 |
|
25 Dwell -> DwellEnd (v) |
|
26 |
|
27 Swipe -> Explore (x) |
|
28 |
|
29 TapHold -> TapHoldEnd (v) |
|
30 |
|
31 DoubleTapHold -> DoubleTapHoldEnd (v) |
|
32 |
|
33 DwellEnd -> Explore (x) |
|
34 |
|
35 TapHoldEnd -> Explore (x) |
|
36 |
|
37 DoubleTapHoldEnd -> Explore (x) |
|
38 |
|
39 ExploreEnd -> Explore (x) |
|
40 |
|
41 Explore -> ExploreEnd (v) |
|
42 ******************************************************************************/ |
|
43 |
|
44 'use strict'; |
|
45 |
|
46 const Ci = Components.interfaces; |
|
47 const Cu = Components.utils; |
|
48 |
|
49 this.EXPORTED_SYMBOLS = ['GestureSettings', 'GestureTracker']; // jshint ignore:line |
|
50 |
|
51 Cu.import('resource://gre/modules/XPCOMUtils.jsm'); |
|
52 |
|
53 XPCOMUtils.defineLazyModuleGetter(this, 'Utils', // jshint ignore:line |
|
54 'resource://gre/modules/accessibility/Utils.jsm'); |
|
55 XPCOMUtils.defineLazyModuleGetter(this, 'Logger', // jshint ignore:line |
|
56 'resource://gre/modules/accessibility/Utils.jsm'); |
|
57 XPCOMUtils.defineLazyModuleGetter(this, 'setTimeout', // jshint ignore:line |
|
58 'resource://gre/modules/Timer.jsm'); |
|
59 XPCOMUtils.defineLazyModuleGetter(this, 'clearTimeout', // jshint ignore:line |
|
60 'resource://gre/modules/Timer.jsm'); |
|
61 XPCOMUtils.defineLazyModuleGetter(this, 'Promise', // jshint ignore:line |
|
62 'resource://gre/modules/Promise.jsm'); |
|
63 |
|
64 // Maximum amount of time allowed for a gesture to be considered a multitouch. |
|
65 const MAX_MULTITOUCH = 250; |
|
66 // Minimal swipe distance in inches |
|
67 const SWIPE_MIN_DISTANCE = 0.4; |
|
68 // Maximum distance the pointer could move during a tap in inches |
|
69 const TAP_MAX_RADIUS = 0.2; |
|
70 // Directness coefficient. It is based on the maximum 15 degree angle between |
|
71 // consequent pointer move lines. |
|
72 const DIRECTNESS_COEFF = 1.44; |
|
73 // An android flag. |
|
74 const IS_ANDROID = Utils.MozBuildApp === 'mobile/android' && |
|
75 Utils.AndroidSdkVersion >= 14; |
|
76 // A single pointer down/up sequence periodically precedes the tripple swipe |
|
77 // gesture on Android. This delay acounts for that. |
|
78 const ANDROID_TRIPLE_SWIPE_DELAY = 50; |
|
79 // The virtual touch ID generated by a mouse event. |
|
80 const MOUSE_ID = 'mouse'; |
|
81 |
|
82 /** |
|
83 * A point object containing distance travelled data. |
|
84 * @param {Object} aPoint A point object that looks like: { |
|
85 * x: x coordinate in pixels, |
|
86 * y: y coordinate in pixels |
|
87 * } |
|
88 */ |
|
89 function Point(aPoint) { |
|
90 this.startX = this.x = aPoint.x; |
|
91 this.startY = this.y = aPoint.y; |
|
92 this.distanceTraveled = 0; |
|
93 this.totalDistanceTraveled = 0; |
|
94 } |
|
95 |
|
96 Point.prototype = { |
|
97 /** |
|
98 * Update the current point coordiates. |
|
99 * @param {Object} aPoint A new point coordinates. |
|
100 */ |
|
101 update: function Point_update(aPoint) { |
|
102 let lastX = this.x; |
|
103 let lastY = this.y; |
|
104 this.x = aPoint.x; |
|
105 this.y = aPoint.y; |
|
106 this.distanceTraveled = this.getDistanceToCoord(lastX, lastY); |
|
107 this.totalDistanceTraveled += this.distanceTraveled; |
|
108 }, |
|
109 |
|
110 reset: function Point_reset() { |
|
111 this.distanceTraveled = 0; |
|
112 this.totalDistanceTraveled = 0; |
|
113 }, |
|
114 |
|
115 /** |
|
116 * Get distance between the current point coordinates and the given ones. |
|
117 * @param {Number} aX A pixel value for the x coordinate. |
|
118 * @param {Number} aY A pixel value for the y coordinate. |
|
119 * @return {Number} A distance between point's current and the given |
|
120 * coordinates. |
|
121 */ |
|
122 getDistanceToCoord: function Point_getDistanceToCoord(aX, aY) { |
|
123 return Math.hypot(this.x - aX, this.y - aY); |
|
124 }, |
|
125 |
|
126 /** |
|
127 * Get the direct distance travelled by the point so far. |
|
128 */ |
|
129 get directDistanceTraveled() { |
|
130 return this.getDistanceToCoord(this.startX, this.startY); |
|
131 } |
|
132 }; |
|
133 |
|
134 /** |
|
135 * An externally accessible collection of settings used in gesture resolition. |
|
136 * @type {Object} |
|
137 */ |
|
138 this.GestureSettings = { // jshint ignore:line |
|
139 /** |
|
140 * Maximum duration of swipe |
|
141 * @type {Number} |
|
142 */ |
|
143 swipeMaxDuration: 400, |
|
144 |
|
145 /** |
|
146 * Maximum consecutive pointer event timeout. |
|
147 * @type {Number} |
|
148 */ |
|
149 maxConsecutiveGestureDelay: 400, |
|
150 |
|
151 /** |
|
152 * Delay before tap turns into dwell |
|
153 * @type {Number} |
|
154 */ |
|
155 dwellThreshold: 500, |
|
156 |
|
157 /** |
|
158 * Minimum distance that needs to be travelled for the pointer move to be |
|
159 * fired. |
|
160 * @type {Number} |
|
161 */ |
|
162 travelThreshold: 0.025 |
|
163 }; |
|
164 |
|
165 /** |
|
166 * An interface that handles the pointer events and calculates the appropriate |
|
167 * gestures. |
|
168 * @type {Object} |
|
169 */ |
|
170 this.GestureTracker = { // jshint ignore:line |
|
171 /** |
|
172 * Reset GestureTracker to its initial state. |
|
173 * @return {[type]} [description] |
|
174 */ |
|
175 reset: function GestureTracker_reset() { |
|
176 if (this.current) { |
|
177 this.current.clearTimer(); |
|
178 } |
|
179 delete this.current; |
|
180 }, |
|
181 |
|
182 /** |
|
183 * Create a new gesture object and attach resolution handler to it as well as |
|
184 * handle the incoming pointer event. |
|
185 * @param {Object} aDetail A new pointer event detail. |
|
186 * @param {Number} aTimeStamp A new pointer event timeStamp. |
|
187 * @param {Function} aGesture A gesture constructor (default: Tap). |
|
188 */ |
|
189 _init: function GestureTracker__init(aDetail, aTimeStamp, aGesture = Tap) { |
|
190 // Only create a new gesture on |pointerdown| event. |
|
191 if (aDetail.type !== 'pointerdown') { |
|
192 return; |
|
193 } |
|
194 let points = aDetail.points; |
|
195 let GestureConstructor = aGesture; |
|
196 if (IS_ANDROID && GestureConstructor === Tap && points.length === 1 && |
|
197 points[0].identifier !== MOUSE_ID) { |
|
198 // Handle Android events when EBT is enabled. Two finger gestures are |
|
199 // translated to one. |
|
200 GestureConstructor = AndroidTap; |
|
201 } |
|
202 this._create(GestureConstructor); |
|
203 this._update(aDetail, aTimeStamp); |
|
204 }, |
|
205 |
|
206 /** |
|
207 * Handle the incoming pointer event with the existing gesture object(if |
|
208 * present) or with the newly created one. |
|
209 * @param {Object} aDetail A new pointer event detail. |
|
210 * @param {Number} aTimeStamp A new pointer event timeStamp. |
|
211 */ |
|
212 handle: function GestureTracker_handle(aDetail, aTimeStamp) { |
|
213 Logger.debug(() => { |
|
214 return ['Pointer event', aDetail.type, 'at:', aTimeStamp, |
|
215 JSON.stringify(aDetail.points)]; |
|
216 }); |
|
217 this[this.current ? '_update' : '_init'](aDetail, aTimeStamp); |
|
218 }, |
|
219 |
|
220 /** |
|
221 * Create a new gesture object and attach resolution handler to it. |
|
222 * @param {Function} aGesture A gesture constructor. |
|
223 * @param {Number} aTimeStamp An original pointer event timeStamp. |
|
224 * @param {Array} aPoints All changed points associated with the new pointer |
|
225 * event. |
|
226 * @param {?String} aLastEvent Last pointer event type. |
|
227 */ |
|
228 _create: function GestureTracker__create(aGesture, aTimeStamp, aPoints, aLastEvent) { |
|
229 this.current = new aGesture(aTimeStamp, aPoints, aLastEvent); /* A constructor name should start with an uppercase letter. */ // jshint ignore:line |
|
230 this.current.then(this._onFulfill.bind(this)); |
|
231 }, |
|
232 |
|
233 /** |
|
234 * Handle the incoming pointer event with the existing gesture object. |
|
235 * @param {Object} aDetail A new pointer event detail. |
|
236 * @param {Number} aTimeStamp A new pointer event timeStamp. |
|
237 */ |
|
238 _update: function GestureTracker_update(aDetail, aTimeStamp) { |
|
239 this.current[aDetail.type](aDetail.points, aTimeStamp); |
|
240 }, |
|
241 |
|
242 /** |
|
243 * A resolution handler function for the current gesture promise. |
|
244 * @param {Object} aResult A resolution payload with the relevant gesture id |
|
245 * and an optional new gesture contructor. |
|
246 */ |
|
247 _onFulfill: function GestureTracker__onFulfill(aResult) { |
|
248 let {id, gestureType} = aResult; |
|
249 let current = this.current; |
|
250 // Do nothing if there's no existing gesture or there's already a newer |
|
251 // gesture. |
|
252 if (!current || current.id !== id) { |
|
253 return; |
|
254 } |
|
255 // Only create a gesture if we got a constructor. |
|
256 if (gestureType) { |
|
257 this._create(gestureType, current.startTime, current.points, |
|
258 current.lastEvent); |
|
259 } else { |
|
260 delete this.current; |
|
261 } |
|
262 } |
|
263 }; |
|
264 |
|
265 /** |
|
266 * Compile a mozAccessFuGesture detail structure. |
|
267 * @param {String} aType A gesture type. |
|
268 * @param {Object} aPoints Gesture's points. |
|
269 * @param {String} xKey A default key for the x coordinate. Default is |
|
270 * 'startX'. |
|
271 * @param {String} yKey A default key for the y coordinate. Default is |
|
272 * 'startY'. |
|
273 * @return {Object} a mozAccessFuGesture detail structure. |
|
274 */ |
|
275 function compileDetail(aType, aPoints, keyMap = {x: 'startX', y: 'startY'}) { |
|
276 let touches = []; |
|
277 let maxDeltaX = 0; |
|
278 let maxDeltaY = 0; |
|
279 for (let identifier in aPoints) { |
|
280 let point = aPoints[identifier]; |
|
281 let touch = {}; |
|
282 for (let key in keyMap) { |
|
283 touch[key] = point[keyMap[key]]; |
|
284 } |
|
285 touches.push(touch); |
|
286 let deltaX = point.x - point.startX; |
|
287 let deltaY = point.y - point.startY; |
|
288 // Determine the maximum x and y travel intervals. |
|
289 if (Math.abs(maxDeltaX) < Math.abs(deltaX)) { |
|
290 maxDeltaX = deltaX; |
|
291 } |
|
292 if (Math.abs(maxDeltaY) < Math.abs(deltaY)) { |
|
293 maxDeltaY = deltaY; |
|
294 } |
|
295 // Since the gesture is resolving, reset the points' distance information |
|
296 // since they are passed to the next potential gesture. |
|
297 point.reset(); |
|
298 } |
|
299 return { |
|
300 type: aType, |
|
301 touches: touches, |
|
302 deltaX: maxDeltaX, |
|
303 deltaY: maxDeltaY |
|
304 }; |
|
305 } |
|
306 |
|
307 /** |
|
308 * A general gesture object. |
|
309 * @param {Number} aTimeStamp An original pointer event's timeStamp that started |
|
310 * the gesture resolution sequence. |
|
311 * @param {Object} aPoints An existing set of points (from previous events). |
|
312 * Default is an empty object. |
|
313 * @param {?String} aLastEvent Last pointer event type. |
|
314 */ |
|
315 function Gesture(aTimeStamp, aPoints = {}, aLastEvent = undefined) { |
|
316 this.startTime = Date.now(); |
|
317 Logger.debug('Creating', this.id, 'gesture.'); |
|
318 this.points = aPoints; |
|
319 this.lastEvent = aLastEvent; |
|
320 this._deferred = Promise.defer(); |
|
321 // Call this._handleResolve or this._handleReject when the promise is |
|
322 // fulfilled with either resolve or reject. |
|
323 this.promise = this._deferred.promise.then(this._handleResolve.bind(this), |
|
324 this._handleReject.bind(this)); |
|
325 this.startTimer(aTimeStamp); |
|
326 } |
|
327 |
|
328 Gesture.prototype = { |
|
329 /** |
|
330 * Get the gesture timeout delay. |
|
331 * @return {Number} |
|
332 */ |
|
333 _getDelay: function Gesture__getDelay() { |
|
334 // If nothing happens withing the |
|
335 // GestureSettings.maxConsecutiveGestureDelay, we should not wait for any |
|
336 // more pointer events and consider them the part of the same gesture - |
|
337 // reject this gesture promise. |
|
338 return GestureSettings.maxConsecutiveGestureDelay; |
|
339 }, |
|
340 |
|
341 /** |
|
342 * Clear the existing timer. |
|
343 */ |
|
344 clearTimer: function Gesture_clearTimer() { |
|
345 clearTimeout(this._timer); |
|
346 delete this._timer; |
|
347 }, |
|
348 |
|
349 /** |
|
350 * Start the timer for gesture timeout. |
|
351 * @param {Number} aTimeStamp An original pointer event's timeStamp that |
|
352 * started the gesture resolution sequence. |
|
353 */ |
|
354 startTimer: function Gesture_startTimer(aTimeStamp) { |
|
355 this.clearTimer(); |
|
356 let delay = this._getDelay(aTimeStamp); |
|
357 let handler = () => { |
|
358 delete this._timer; |
|
359 if (!this._inProgress) { |
|
360 this._deferred.reject(); |
|
361 } else if (this._rejectToOnWait) { |
|
362 this._deferred.reject(this._rejectToOnWait); |
|
363 } |
|
364 }; |
|
365 if (delay <= 0) { |
|
366 handler(); |
|
367 } else { |
|
368 this._timer = setTimeout(handler, delay); |
|
369 } |
|
370 }, |
|
371 |
|
372 /** |
|
373 * Add a gesture promise resolution callback. |
|
374 * @param {Function} aCallback |
|
375 */ |
|
376 then: function Gesture_then(aCallback) { |
|
377 this.promise.then(aCallback); |
|
378 }, |
|
379 |
|
380 /** |
|
381 * Update gesture's points. Test the points set with the optional gesture test |
|
382 * function. |
|
383 * @param {Array} aPoints An array with the changed points from the new |
|
384 * pointer event. |
|
385 * @param {String} aType Pointer event type. |
|
386 * @param {Boolean} aCanCreate A flag that enables including the new points. |
|
387 * Default is false. |
|
388 * @param {Boolean} aNeedComplete A flag that indicates that the gesture is |
|
389 * completing. Default is false. |
|
390 * @return {Boolean} Indicates whether the gesture can be complete (it is |
|
391 * set to true iff the aNeedComplete is true and there was a change to at |
|
392 * least one point that belongs to the gesture). |
|
393 */ |
|
394 _update: function Gesture__update(aPoints, aType, aCanCreate = false, aNeedComplete = false) { |
|
395 let complete; |
|
396 let lastEvent; |
|
397 for (let point of aPoints) { |
|
398 let identifier = point.identifier; |
|
399 let gesturePoint = this.points[identifier]; |
|
400 if (gesturePoint) { |
|
401 gesturePoint.update(point); |
|
402 if (aNeedComplete) { |
|
403 // Since the gesture is completing and at least one of the gesture |
|
404 // points is updated, set the return value to true. |
|
405 complete = true; |
|
406 } |
|
407 lastEvent = lastEvent || aType; |
|
408 } else if (aCanCreate) { |
|
409 // Only create a new point if aCanCreate is true. |
|
410 this.points[identifier] = |
|
411 new Point(point); |
|
412 lastEvent = lastEvent || aType; |
|
413 } |
|
414 } |
|
415 this.lastEvent = lastEvent || this.lastEvent; |
|
416 // If test function is defined test the points. |
|
417 if (this.test) { |
|
418 this.test(complete); |
|
419 } |
|
420 return complete; |
|
421 }, |
|
422 |
|
423 /** |
|
424 * Emit a mozAccessFuGesture (when the gesture is resolved). |
|
425 * @param {Object} aDetail a compiled mozAccessFuGesture detail structure. |
|
426 */ |
|
427 _emit: function Gesture__emit(aDetail) { |
|
428 let evt = new Utils.win.CustomEvent('mozAccessFuGesture', { |
|
429 bubbles: true, |
|
430 cancelable: true, |
|
431 detail: aDetail |
|
432 }); |
|
433 Utils.win.dispatchEvent(evt); |
|
434 }, |
|
435 |
|
436 /** |
|
437 * Handle the pointer down event. |
|
438 * @param {Array} aPoints A new pointer down points. |
|
439 * @param {Number} aTimeStamp A new pointer down timeStamp. |
|
440 */ |
|
441 pointerdown: function Gesture_pointerdown(aPoints, aTimeStamp) { |
|
442 this._inProgress = true; |
|
443 this._update(aPoints, 'pointerdown', |
|
444 aTimeStamp - this.startTime < MAX_MULTITOUCH); |
|
445 }, |
|
446 |
|
447 /** |
|
448 * Handle the pointer move event. |
|
449 * @param {Array} aPoints A new pointer move points. |
|
450 */ |
|
451 pointermove: function Gesture_pointermove(aPoints) { |
|
452 this._update(aPoints, 'pointermove'); |
|
453 }, |
|
454 |
|
455 /** |
|
456 * Handle the pointer up event. |
|
457 * @param {Array} aPoints A new pointer up points. |
|
458 */ |
|
459 pointerup: function Gesture_pointerup(aPoints) { |
|
460 let complete = this._update(aPoints, 'pointerup', false, true); |
|
461 if (complete) { |
|
462 this._deferred.resolve(); |
|
463 } |
|
464 }, |
|
465 |
|
466 /** |
|
467 * A subsequent gesture constructor to resolve the current one to. E.g. |
|
468 * tap->doubletap, dwell->dwellend, etc. |
|
469 * @type {Function} |
|
470 */ |
|
471 resolveTo: null, |
|
472 |
|
473 /** |
|
474 * A unique id for the gesture. Composed of the type + timeStamp. |
|
475 */ |
|
476 get id() { |
|
477 delete this._id; |
|
478 this._id = this.type + this.startTime; |
|
479 return this._id; |
|
480 }, |
|
481 |
|
482 /** |
|
483 * A gesture promise resolve callback. Compile and emit the gesture. |
|
484 * @return {Object} Returns a structure to the gesture handler that looks like |
|
485 * this: { |
|
486 * id: current gesture id, |
|
487 * gestureType: an optional subsequent gesture constructor. |
|
488 * } |
|
489 */ |
|
490 _handleResolve: function Gesture__handleResolve() { |
|
491 if (this.isComplete) { |
|
492 return; |
|
493 } |
|
494 Logger.debug('Resolving', this.id, 'gesture.'); |
|
495 this.isComplete = true; |
|
496 let detail = this.compile(); |
|
497 if (detail) { |
|
498 this._emit(detail); |
|
499 } |
|
500 return { |
|
501 id: this.id, |
|
502 gestureType: this.resolveTo |
|
503 }; |
|
504 }, |
|
505 |
|
506 /** |
|
507 * A gesture promise reject callback. |
|
508 * @return {Object} Returns a structure to the gesture handler that looks like |
|
509 * this: { |
|
510 * id: current gesture id, |
|
511 * gestureType: an optional subsequent gesture constructor. |
|
512 * } |
|
513 */ |
|
514 _handleReject: function Gesture__handleReject(aRejectTo) { |
|
515 if (this.isComplete) { |
|
516 return; |
|
517 } |
|
518 Logger.debug('Rejecting', this.id, 'gesture.'); |
|
519 this.isComplete = true; |
|
520 return { |
|
521 id: this.id, |
|
522 gestureType: aRejectTo |
|
523 }; |
|
524 }, |
|
525 |
|
526 /** |
|
527 * A default compilation function used to build the mozAccessFuGesture event |
|
528 * detail. The detail always includes the type and the touches associated |
|
529 * with the gesture. |
|
530 * @return {Object} Gesture event detail. |
|
531 */ |
|
532 compile: function Gesture_compile() { |
|
533 return compileDetail(this.type, this.points); |
|
534 } |
|
535 }; |
|
536 |
|
537 /** |
|
538 * A mixin for an explore related object. |
|
539 */ |
|
540 function ExploreGesture() { |
|
541 this.compile = () => { |
|
542 // Unlike most of other gestures explore based gestures compile using the |
|
543 // current point position and not the start one. |
|
544 return compileDetail(this.type, this.points, {x: 'x', y: 'y'}); |
|
545 }; |
|
546 } |
|
547 |
|
548 /** |
|
549 * Check the in progress gesture for completion. |
|
550 */ |
|
551 function checkProgressGesture(aGesture) { |
|
552 aGesture._inProgress = true; |
|
553 if (aGesture.lastEvent === 'pointerup') { |
|
554 if (aGesture.test) { |
|
555 aGesture.test(true); |
|
556 } |
|
557 aGesture._deferred.resolve(); |
|
558 } |
|
559 } |
|
560 |
|
561 /** |
|
562 * A common travel gesture. When the travel gesture is created, all subsequent |
|
563 * pointer events' points are tested for their total distance traveled. If that |
|
564 * distance exceeds the _threshold distance, the gesture will be rejected to a |
|
565 * _travelTo gesture. |
|
566 * @param {Number} aTimeStamp An original pointer event's timeStamp that started |
|
567 * the gesture resolution sequence. |
|
568 * @param {Object} aPoints An existing set of points (from previous events). |
|
569 * @param {?String} aLastEvent Last pointer event type. |
|
570 * @param {Function} aTravelTo A contructor for the gesture to reject to when |
|
571 * travelling (default: Explore). |
|
572 * @param {Number} aThreshold Travel threshold (default: |
|
573 * GestureSettings.travelThreshold). |
|
574 */ |
|
575 function TravelGesture(aTimeStamp, aPoints, aLastEvent, aTravelTo = Explore, aThreshold = GestureSettings.travelThreshold) { |
|
576 Gesture.call(this, aTimeStamp, aPoints, aLastEvent); |
|
577 this._travelTo = aTravelTo; |
|
578 this._threshold = aThreshold; |
|
579 } |
|
580 |
|
581 TravelGesture.prototype = Object.create(Gesture.prototype); |
|
582 |
|
583 /** |
|
584 * Test the gesture points for travel. The gesture will be rejected to |
|
585 * this._travelTo gesture iff at least one point crosses this._threshold. |
|
586 */ |
|
587 TravelGesture.prototype.test = function TravelGesture_test() { |
|
588 for (let identifier in this.points) { |
|
589 let point = this.points[identifier]; |
|
590 if (point.totalDistanceTraveled / Utils.dpi > this._threshold) { |
|
591 this._deferred.reject(this._travelTo); |
|
592 return; |
|
593 } |
|
594 } |
|
595 }; |
|
596 |
|
597 /** |
|
598 * DwellEnd gesture. |
|
599 * @param {Number} aTimeStamp An original pointer event's timeStamp that started |
|
600 * the gesture resolution sequence. |
|
601 * @param {Object} aPoints An existing set of points (from previous events). |
|
602 * @param {?String} aLastEvent Last pointer event type. |
|
603 */ |
|
604 function DwellEnd(aTimeStamp, aPoints, aLastEvent) { |
|
605 this._inProgress = true; |
|
606 // If the pointer travels, reject to Explore. |
|
607 TravelGesture.call(this, aTimeStamp, aPoints, aLastEvent); |
|
608 checkProgressGesture(this); |
|
609 } |
|
610 |
|
611 DwellEnd.prototype = Object.create(TravelGesture.prototype); |
|
612 DwellEnd.prototype.type = 'dwellend'; |
|
613 |
|
614 /** |
|
615 * TapHoldEnd gesture. This gesture can be represented as the following diagram: |
|
616 * pointerdown-pointerup-pointerdown-*wait*-pointerup. |
|
617 * @param {Number} aTimeStamp An original pointer event's timeStamp that started |
|
618 * the gesture resolution sequence. |
|
619 * @param {Object} aPoints An existing set of points (from previous events). |
|
620 * @param {?String} aLastEvent Last pointer event type. |
|
621 */ |
|
622 function TapHoldEnd(aTimeStamp, aPoints, aLastEvent) { |
|
623 this._inProgress = true; |
|
624 // If the pointer travels, reject to Explore. |
|
625 TravelGesture.call(this, aTimeStamp, aPoints, aLastEvent); |
|
626 checkProgressGesture(this); |
|
627 } |
|
628 |
|
629 TapHoldEnd.prototype = Object.create(TravelGesture.prototype); |
|
630 TapHoldEnd.prototype.type = 'tapholdend'; |
|
631 |
|
632 /** |
|
633 * DoubleTapHoldEnd gesture. This gesture can be represented as the following |
|
634 * diagram: |
|
635 * pointerdown-pointerup-pointerdown-pointerup-pointerdown-*wait*-pointerup. |
|
636 * @param {Number} aTimeStamp An original pointer event's timeStamp that started |
|
637 * the gesture resolution sequence. |
|
638 * @param {Object} aPoints An existing set of points (from previous events). |
|
639 * @param {?String} aLastEvent Last pointer event type. |
|
640 */ |
|
641 function DoubleTapHoldEnd(aTimeStamp, aPoints, aLastEvent) { |
|
642 this._inProgress = true; |
|
643 // If the pointer travels, reject to Explore. |
|
644 TravelGesture.call(this, aTimeStamp, aPoints, aLastEvent); |
|
645 checkProgressGesture(this); |
|
646 } |
|
647 |
|
648 DoubleTapHoldEnd.prototype = Object.create(TravelGesture.prototype); |
|
649 DoubleTapHoldEnd.prototype.type = 'doubletapholdend'; |
|
650 |
|
651 /** |
|
652 * A common tap gesture object. |
|
653 * @param {Number} aTimeStamp An original pointer event's timeStamp that started |
|
654 * the gesture resolution sequence. |
|
655 * @param {Object} aPoints An existing set of points (from previous events). |
|
656 * @param {?String} aLastEvent Last pointer event type. |
|
657 * @param {Function} aRejectTo A constructor for the next gesture to reject to |
|
658 * in case no pointermove or pointerup happens within the |
|
659 * GestureSettings.dwellThreshold. |
|
660 * @param {Function} aTravelTo An optional constuctor for the next gesture to |
|
661 * reject to in case the the TravelGesture test fails. |
|
662 */ |
|
663 function TapGesture(aTimeStamp, aPoints, aLastEvent, aRejectTo, aTravelTo) { |
|
664 this._rejectToOnWait = aRejectTo; |
|
665 // If the pointer travels, reject to aTravelTo. |
|
666 TravelGesture.call(this, aTimeStamp, aPoints, aLastEvent, aTravelTo, |
|
667 TAP_MAX_RADIUS); |
|
668 } |
|
669 |
|
670 TapGesture.prototype = Object.create(TravelGesture.prototype); |
|
671 TapGesture.prototype._getDelay = function TapGesture__getDelay() { |
|
672 // If, for TapGesture, no pointermove or pointerup happens within the |
|
673 // GestureSettings.dwellThreshold, reject. |
|
674 // Note: the original pointer event's timeStamp is irrelevant here. |
|
675 return GestureSettings.dwellThreshold; |
|
676 }; |
|
677 |
|
678 /** |
|
679 * Tap gesture. |
|
680 * @param {Number} aTimeStamp An original pointer event's timeStamp that started |
|
681 * the gesture resolution sequence. |
|
682 * @param {Object} aPoints An existing set of points (from previous events). |
|
683 * @param {?String} aLastEvent Last pointer event type. |
|
684 */ |
|
685 function Tap(aTimeStamp, aPoints, aLastEvent) { |
|
686 // If the pointer travels, reject to Swipe. |
|
687 TapGesture.call(this, aTimeStamp, aPoints, aLastEvent, Dwell, Swipe); |
|
688 } |
|
689 |
|
690 Tap.prototype = Object.create(TapGesture.prototype); |
|
691 Tap.prototype.type = 'tap'; |
|
692 Tap.prototype.resolveTo = DoubleTap; |
|
693 |
|
694 /** |
|
695 * Tap (multi) gesture on Android. |
|
696 * @param {Number} aTimeStamp An original pointer event's timeStamp that started |
|
697 * the gesture resolution sequence. |
|
698 * @param {Object} aPoints An existing set of points (from previous events). |
|
699 * @param {?String} aLastEvent Last pointer event type. |
|
700 */ |
|
701 function AndroidTap(aTimeStamp, aPoints, aLastEvent) { |
|
702 // If the pointer travels, reject to Swipe. On dwell threshold reject to |
|
703 // TapHold. |
|
704 TapGesture.call(this, aTimeStamp, aPoints, aLastEvent, TapHold, Swipe); |
|
705 } |
|
706 AndroidTap.prototype = Object.create(TapGesture.prototype); |
|
707 // Android double taps are translated to single taps. |
|
708 AndroidTap.prototype.type = 'doubletap'; |
|
709 AndroidTap.prototype.resolveTo = TripleTap; |
|
710 |
|
711 /** |
|
712 * Clear the pointerup handler timer in case of the 3 pointer swipe. |
|
713 */ |
|
714 AndroidTap.prototype.clearThreeFingerSwipeTimer = function AndroidTap_clearThreeFingerSwipeTimer() { |
|
715 clearTimeout(this._threeFingerSwipeTimer); |
|
716 delete this._threeFingerSwipeTimer; |
|
717 }; |
|
718 |
|
719 AndroidTap.prototype.pointerdown = function AndroidTap_pointerdown(aPoints, aTimeStamp) { |
|
720 this.clearThreeFingerSwipeTimer(); |
|
721 TapGesture.prototype.pointerdown.call(this, aPoints, aTimeStamp); |
|
722 }; |
|
723 |
|
724 AndroidTap.prototype.pointermove = function AndroidTap_pointermove(aPoints) { |
|
725 this.clearThreeFingerSwipeTimer(); |
|
726 this._moved = true; |
|
727 TapGesture.prototype.pointermove.call(this, aPoints); |
|
728 }; |
|
729 |
|
730 AndroidTap.prototype.pointerup = function AndroidTap_pointerup(aPoints) { |
|
731 if (this._moved) { |
|
732 // If there was a pointer move - handle the real gesture. |
|
733 TapGesture.prototype.pointerup.call(this, aPoints); |
|
734 } else { |
|
735 // Primptively delay the multi pointer gesture resolution, because Android |
|
736 // sometimes fires a pointerdown/poitnerup sequence before the real events. |
|
737 this._threeFingerSwipeTimer = setTimeout(() => { |
|
738 delete this._threeFingerSwipeTimer; |
|
739 TapGesture.prototype.pointerup.call(this, aPoints); |
|
740 }, ANDROID_TRIPLE_SWIPE_DELAY); |
|
741 } |
|
742 }; |
|
743 |
|
744 /** |
|
745 * Reject an android tap gesture. |
|
746 * @param {?Function} aRejectTo An optional next gesture constructor. |
|
747 * @return {Object} structure that looks like { |
|
748 * id: gesture_id, // Current AndroidTap gesture id. |
|
749 * gestureType: next_gesture // Optional |
|
750 * } |
|
751 */ |
|
752 AndroidTap.prototype._handleReject = function AndroidTap__handleReject(aRejectTo) { |
|
753 let keys = Object.keys(this.points); |
|
754 if (aRejectTo === Swipe && keys.length === 1) { |
|
755 let key = keys[0]; |
|
756 let point = this.points[key]; |
|
757 // Two finger swipe is translated into single swipe. |
|
758 this.points[key + '-copy'] = point; |
|
759 } |
|
760 return TapGesture.prototype._handleReject.call(this, aRejectTo); |
|
761 }; |
|
762 |
|
763 /** |
|
764 * Double Tap gesture. |
|
765 * @param {Number} aTimeStamp An original pointer event's timeStamp that started |
|
766 * the gesture resolution sequence. |
|
767 * @param {Object} aPoints An existing set of points (from previous events). |
|
768 * @param {?String} aLastEvent Last pointer event type. |
|
769 */ |
|
770 function DoubleTap(aTimeStamp, aPoints, aLastEvent) { |
|
771 TapGesture.call(this, aTimeStamp, aPoints, aLastEvent, TapHold); |
|
772 } |
|
773 |
|
774 DoubleTap.prototype = Object.create(TapGesture.prototype); |
|
775 DoubleTap.prototype.type = 'doubletap'; |
|
776 DoubleTap.prototype.resolveTo = TripleTap; |
|
777 |
|
778 /** |
|
779 * Triple Tap gesture. |
|
780 * @param {Number} aTimeStamp An original pointer event's timeStamp that started |
|
781 * the gesture resolution sequence. |
|
782 * @param {Object} aPoints An existing set of points (from previous events). |
|
783 * @param {?String} aLastEvent Last pointer event type. |
|
784 */ |
|
785 function TripleTap(aTimeStamp, aPoints, aLastEvent) { |
|
786 TapGesture.call(this, aTimeStamp, aPoints, aLastEvent, DoubleTapHold); |
|
787 } |
|
788 |
|
789 TripleTap.prototype = Object.create(TapGesture.prototype); |
|
790 TripleTap.prototype.type = 'tripletap'; |
|
791 |
|
792 /** |
|
793 * Common base object for gestures that are created as resolved. |
|
794 * @param {Number} aTimeStamp An original pointer event's timeStamp that started |
|
795 * the gesture resolution sequence. |
|
796 * @param {Object} aPoints An existing set of points (from previous events). |
|
797 * @param {?String} aLastEvent Last pointer event type. |
|
798 */ |
|
799 function ResolvedGesture(aTimeStamp, aPoints, aLastEvent) { |
|
800 Gesture.call(this, aTimeStamp, aPoints, aLastEvent); |
|
801 // Resolve the guesture right away. |
|
802 this._deferred.resolve(); |
|
803 } |
|
804 |
|
805 ResolvedGesture.prototype = Object.create(Gesture.prototype); |
|
806 |
|
807 /** |
|
808 * Dwell gesture |
|
809 * @param {Number} aTimeStamp An original pointer event's timeStamp that started |
|
810 * the gesture resolution sequence. |
|
811 * @param {Object} aPoints An existing set of points (from previous events). |
|
812 * @param {?String} aLastEvent Last pointer event type. |
|
813 */ |
|
814 function Dwell(aTimeStamp, aPoints, aLastEvent) { |
|
815 ResolvedGesture.call(this, aTimeStamp, aPoints, aLastEvent); |
|
816 } |
|
817 |
|
818 Dwell.prototype = Object.create(ResolvedGesture.prototype); |
|
819 Dwell.prototype.type = 'dwell'; |
|
820 Dwell.prototype.resolveTo = DwellEnd; |
|
821 |
|
822 /** |
|
823 * TapHold gesture |
|
824 * @param {Number} aTimeStamp An original pointer event's timeStamp that started |
|
825 * the gesture resolution sequence. |
|
826 * @param {Object} aPoints An existing set of points (from previous events). |
|
827 * @param {?String} aLastEvent Last pointer event type. |
|
828 */ |
|
829 function TapHold(aTimeStamp, aPoints, aLastEvent) { |
|
830 ResolvedGesture.call(this, aTimeStamp, aPoints, aLastEvent); |
|
831 } |
|
832 |
|
833 TapHold.prototype = Object.create(ResolvedGesture.prototype); |
|
834 TapHold.prototype.type = 'taphold'; |
|
835 TapHold.prototype.resolveTo = TapHoldEnd; |
|
836 |
|
837 /** |
|
838 * DoubleTapHold gesture |
|
839 * @param {Number} aTimeStamp An original pointer event's timeStamp that started |
|
840 * the gesture resolution sequence. |
|
841 * @param {Object} aPoints An existing set of points (from previous events). |
|
842 * @param {?String} aLastEvent Last pointer event type. |
|
843 */ |
|
844 function DoubleTapHold(aTimeStamp, aPoints, aLastEvent) { |
|
845 ResolvedGesture.call(this, aTimeStamp, aPoints, aLastEvent); |
|
846 } |
|
847 |
|
848 DoubleTapHold.prototype = Object.create(ResolvedGesture.prototype); |
|
849 DoubleTapHold.prototype.type = 'doubletaphold'; |
|
850 DoubleTapHold.prototype.resolveTo = DoubleTapHoldEnd; |
|
851 |
|
852 /** |
|
853 * Explore gesture |
|
854 * @param {Number} aTimeStamp An original pointer event's timeStamp that started |
|
855 * the gesture resolution sequence. |
|
856 * @param {Object} aPoints An existing set of points (from previous events). |
|
857 * @param {?String} aLastEvent Last pointer event type. |
|
858 */ |
|
859 function Explore(aTimeStamp, aPoints, aLastEvent) { |
|
860 ExploreGesture.call(this); |
|
861 ResolvedGesture.call(this, aTimeStamp, aPoints, aLastEvent); |
|
862 } |
|
863 |
|
864 Explore.prototype = Object.create(ResolvedGesture.prototype); |
|
865 Explore.prototype.type = 'explore'; |
|
866 Explore.prototype.resolveTo = ExploreEnd; |
|
867 |
|
868 /** |
|
869 * ExploreEnd gesture. |
|
870 * @param {Number} aTimeStamp An original pointer event's timeStamp that started |
|
871 * the gesture resolution sequence. |
|
872 * @param {Object} aPoints An existing set of points (from previous events). |
|
873 * @param {?String} aLastEvent Last pointer event type. |
|
874 */ |
|
875 function ExploreEnd(aTimeStamp, aPoints, aLastEvent) { |
|
876 this._inProgress = true; |
|
877 ExploreGesture.call(this); |
|
878 // If the pointer travels, reject to Explore. |
|
879 TravelGesture.call(this, aTimeStamp, aPoints, aLastEvent); |
|
880 checkProgressGesture(this); |
|
881 } |
|
882 |
|
883 ExploreEnd.prototype = Object.create(TravelGesture.prototype); |
|
884 ExploreEnd.prototype.type = 'exploreend'; |
|
885 |
|
886 /** |
|
887 * Swipe gesture. |
|
888 * @param {Number} aTimeStamp An original pointer event's timeStamp that started |
|
889 * the gesture resolution sequence. |
|
890 * @param {Object} aPoints An existing set of points (from previous events). |
|
891 * @param {?String} aLastEvent Last pointer event type. |
|
892 */ |
|
893 function Swipe(aTimeStamp, aPoints, aLastEvent) { |
|
894 this._inProgress = true; |
|
895 this._rejectToOnWait = Explore; |
|
896 Gesture.call(this, aTimeStamp, aPoints, aLastEvent); |
|
897 checkProgressGesture(this); |
|
898 } |
|
899 |
|
900 Swipe.prototype = Object.create(Gesture.prototype); |
|
901 Swipe.prototype.type = 'swipe'; |
|
902 Swipe.prototype._getDelay = function Swipe__getDelay(aTimeStamp) { |
|
903 // Swipe should be completed within the GestureSettings.swipeMaxDuration from |
|
904 // the initial pointer down event. |
|
905 return GestureSettings.swipeMaxDuration - this.startTime + aTimeStamp; |
|
906 }; |
|
907 |
|
908 /** |
|
909 * Determine wither the gesture was Swipe or Explore. |
|
910 * @param {Booler} aComplete A flag that indicates whether the gesture is and |
|
911 * will be complete after the test. |
|
912 */ |
|
913 Swipe.prototype.test = function Swipe_test(aComplete) { |
|
914 if (!aComplete) { |
|
915 // No need to test if the gesture is not completing or can't be complete. |
|
916 return; |
|
917 } |
|
918 let reject = true; |
|
919 // If at least one point travelled for more than SWIPE_MIN_DISTANCE and it was |
|
920 // direct enough, consider it a Swipe. |
|
921 for (let identifier in this.points) { |
|
922 let point = this.points[identifier]; |
|
923 let directDistance = point.directDistanceTraveled; |
|
924 if (directDistance / Utils.dpi >= SWIPE_MIN_DISTANCE || |
|
925 directDistance * DIRECTNESS_COEFF >= point.totalDistanceTraveled) { |
|
926 reject = false; |
|
927 } |
|
928 } |
|
929 if (reject) { |
|
930 this._deferred.reject(Explore); |
|
931 } |
|
932 }; |
|
933 |
|
934 /** |
|
935 * Compile a swipe related mozAccessFuGesture event detail. |
|
936 * @return {Object} A mozAccessFuGesture detail object. |
|
937 */ |
|
938 Swipe.prototype.compile = function Swipe_compile() { |
|
939 let type = this.type; |
|
940 let detail = compileDetail(type, this.points, |
|
941 {x1: 'startX', y1: 'startY', x2: 'x', y2: 'y'}); |
|
942 let deltaX = detail.deltaX; |
|
943 let deltaY = detail.deltaY; |
|
944 if (Math.abs(deltaX) > Math.abs(deltaY)) { |
|
945 // Horizontal swipe. |
|
946 detail.type = type + (deltaX > 0 ? 'right' : 'left'); |
|
947 } else { |
|
948 // Vertival swipe. |
|
949 detail.type = type + (deltaY > 0 ? 'down' : 'up'); |
|
950 } |
|
951 return detail; |
|
952 }; |