1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/accessible/src/jsat/Gestures.jsm Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,952 @@ 1.4 +/* This Source Code Form is subject to the terms of the Mozilla Public 1.5 + * License, v. 2.0. If a copy of the MPL was not distributed with this file, 1.6 + * You can obtain one at http://mozilla.org/MPL/2.0/. */ 1.7 + 1.8 +/* global Components, GestureSettings, XPCOMUtils, Utils, Promise, Logger */ 1.9 +/* exported GestureSettings, GestureTracker */ 1.10 + 1.11 +/****************************************************************************** 1.12 + All gestures have the following pathways when being resolved(v)/rejected(x): 1.13 + Tap -> DoubleTap (v) 1.14 + -> Dwell (x) 1.15 + -> Swipe (x) 1.16 + 1.17 + AndroidTap -> TripleTap (v) 1.18 + -> TapHold (x) 1.19 + -> Swipe (x) 1.20 + 1.21 + DoubleTap -> TripleTap (v) 1.22 + -> TapHold (x) 1.23 + -> Explore (x) 1.24 + 1.25 + TripleTap -> DoubleTapHold (x) 1.26 + -> Explore (x) 1.27 + 1.28 + Dwell -> DwellEnd (v) 1.29 + 1.30 + Swipe -> Explore (x) 1.31 + 1.32 + TapHold -> TapHoldEnd (v) 1.33 + 1.34 + DoubleTapHold -> DoubleTapHoldEnd (v) 1.35 + 1.36 + DwellEnd -> Explore (x) 1.37 + 1.38 + TapHoldEnd -> Explore (x) 1.39 + 1.40 + DoubleTapHoldEnd -> Explore (x) 1.41 + 1.42 + ExploreEnd -> Explore (x) 1.43 + 1.44 + Explore -> ExploreEnd (v) 1.45 +******************************************************************************/ 1.46 + 1.47 +'use strict'; 1.48 + 1.49 +const Ci = Components.interfaces; 1.50 +const Cu = Components.utils; 1.51 + 1.52 +this.EXPORTED_SYMBOLS = ['GestureSettings', 'GestureTracker']; // jshint ignore:line 1.53 + 1.54 +Cu.import('resource://gre/modules/XPCOMUtils.jsm'); 1.55 + 1.56 +XPCOMUtils.defineLazyModuleGetter(this, 'Utils', // jshint ignore:line 1.57 + 'resource://gre/modules/accessibility/Utils.jsm'); 1.58 +XPCOMUtils.defineLazyModuleGetter(this, 'Logger', // jshint ignore:line 1.59 + 'resource://gre/modules/accessibility/Utils.jsm'); 1.60 +XPCOMUtils.defineLazyModuleGetter(this, 'setTimeout', // jshint ignore:line 1.61 + 'resource://gre/modules/Timer.jsm'); 1.62 +XPCOMUtils.defineLazyModuleGetter(this, 'clearTimeout', // jshint ignore:line 1.63 + 'resource://gre/modules/Timer.jsm'); 1.64 +XPCOMUtils.defineLazyModuleGetter(this, 'Promise', // jshint ignore:line 1.65 + 'resource://gre/modules/Promise.jsm'); 1.66 + 1.67 +// Maximum amount of time allowed for a gesture to be considered a multitouch. 1.68 +const MAX_MULTITOUCH = 250; 1.69 +// Minimal swipe distance in inches 1.70 +const SWIPE_MIN_DISTANCE = 0.4; 1.71 +// Maximum distance the pointer could move during a tap in inches 1.72 +const TAP_MAX_RADIUS = 0.2; 1.73 +// Directness coefficient. It is based on the maximum 15 degree angle between 1.74 +// consequent pointer move lines. 1.75 +const DIRECTNESS_COEFF = 1.44; 1.76 +// An android flag. 1.77 +const IS_ANDROID = Utils.MozBuildApp === 'mobile/android' && 1.78 + Utils.AndroidSdkVersion >= 14; 1.79 +// A single pointer down/up sequence periodically precedes the tripple swipe 1.80 +// gesture on Android. This delay acounts for that. 1.81 +const ANDROID_TRIPLE_SWIPE_DELAY = 50; 1.82 +// The virtual touch ID generated by a mouse event. 1.83 +const MOUSE_ID = 'mouse'; 1.84 + 1.85 +/** 1.86 + * A point object containing distance travelled data. 1.87 + * @param {Object} aPoint A point object that looks like: { 1.88 + * x: x coordinate in pixels, 1.89 + * y: y coordinate in pixels 1.90 + * } 1.91 + */ 1.92 +function Point(aPoint) { 1.93 + this.startX = this.x = aPoint.x; 1.94 + this.startY = this.y = aPoint.y; 1.95 + this.distanceTraveled = 0; 1.96 + this.totalDistanceTraveled = 0; 1.97 +} 1.98 + 1.99 +Point.prototype = { 1.100 + /** 1.101 + * Update the current point coordiates. 1.102 + * @param {Object} aPoint A new point coordinates. 1.103 + */ 1.104 + update: function Point_update(aPoint) { 1.105 + let lastX = this.x; 1.106 + let lastY = this.y; 1.107 + this.x = aPoint.x; 1.108 + this.y = aPoint.y; 1.109 + this.distanceTraveled = this.getDistanceToCoord(lastX, lastY); 1.110 + this.totalDistanceTraveled += this.distanceTraveled; 1.111 + }, 1.112 + 1.113 + reset: function Point_reset() { 1.114 + this.distanceTraveled = 0; 1.115 + this.totalDistanceTraveled = 0; 1.116 + }, 1.117 + 1.118 + /** 1.119 + * Get distance between the current point coordinates and the given ones. 1.120 + * @param {Number} aX A pixel value for the x coordinate. 1.121 + * @param {Number} aY A pixel value for the y coordinate. 1.122 + * @return {Number} A distance between point's current and the given 1.123 + * coordinates. 1.124 + */ 1.125 + getDistanceToCoord: function Point_getDistanceToCoord(aX, aY) { 1.126 + return Math.hypot(this.x - aX, this.y - aY); 1.127 + }, 1.128 + 1.129 + /** 1.130 + * Get the direct distance travelled by the point so far. 1.131 + */ 1.132 + get directDistanceTraveled() { 1.133 + return this.getDistanceToCoord(this.startX, this.startY); 1.134 + } 1.135 +}; 1.136 + 1.137 +/** 1.138 + * An externally accessible collection of settings used in gesture resolition. 1.139 + * @type {Object} 1.140 + */ 1.141 +this.GestureSettings = { // jshint ignore:line 1.142 + /** 1.143 + * Maximum duration of swipe 1.144 + * @type {Number} 1.145 + */ 1.146 + swipeMaxDuration: 400, 1.147 + 1.148 + /** 1.149 + * Maximum consecutive pointer event timeout. 1.150 + * @type {Number} 1.151 + */ 1.152 + maxConsecutiveGestureDelay: 400, 1.153 + 1.154 + /** 1.155 + * Delay before tap turns into dwell 1.156 + * @type {Number} 1.157 + */ 1.158 + dwellThreshold: 500, 1.159 + 1.160 + /** 1.161 + * Minimum distance that needs to be travelled for the pointer move to be 1.162 + * fired. 1.163 + * @type {Number} 1.164 + */ 1.165 + travelThreshold: 0.025 1.166 +}; 1.167 + 1.168 +/** 1.169 + * An interface that handles the pointer events and calculates the appropriate 1.170 + * gestures. 1.171 + * @type {Object} 1.172 + */ 1.173 +this.GestureTracker = { // jshint ignore:line 1.174 + /** 1.175 + * Reset GestureTracker to its initial state. 1.176 + * @return {[type]} [description] 1.177 + */ 1.178 + reset: function GestureTracker_reset() { 1.179 + if (this.current) { 1.180 + this.current.clearTimer(); 1.181 + } 1.182 + delete this.current; 1.183 + }, 1.184 + 1.185 + /** 1.186 + * Create a new gesture object and attach resolution handler to it as well as 1.187 + * handle the incoming pointer event. 1.188 + * @param {Object} aDetail A new pointer event detail. 1.189 + * @param {Number} aTimeStamp A new pointer event timeStamp. 1.190 + * @param {Function} aGesture A gesture constructor (default: Tap). 1.191 + */ 1.192 + _init: function GestureTracker__init(aDetail, aTimeStamp, aGesture = Tap) { 1.193 + // Only create a new gesture on |pointerdown| event. 1.194 + if (aDetail.type !== 'pointerdown') { 1.195 + return; 1.196 + } 1.197 + let points = aDetail.points; 1.198 + let GestureConstructor = aGesture; 1.199 + if (IS_ANDROID && GestureConstructor === Tap && points.length === 1 && 1.200 + points[0].identifier !== MOUSE_ID) { 1.201 + // Handle Android events when EBT is enabled. Two finger gestures are 1.202 + // translated to one. 1.203 + GestureConstructor = AndroidTap; 1.204 + } 1.205 + this._create(GestureConstructor); 1.206 + this._update(aDetail, aTimeStamp); 1.207 + }, 1.208 + 1.209 + /** 1.210 + * Handle the incoming pointer event with the existing gesture object(if 1.211 + * present) or with the newly created one. 1.212 + * @param {Object} aDetail A new pointer event detail. 1.213 + * @param {Number} aTimeStamp A new pointer event timeStamp. 1.214 + */ 1.215 + handle: function GestureTracker_handle(aDetail, aTimeStamp) { 1.216 + Logger.debug(() => { 1.217 + return ['Pointer event', aDetail.type, 'at:', aTimeStamp, 1.218 + JSON.stringify(aDetail.points)]; 1.219 + }); 1.220 + this[this.current ? '_update' : '_init'](aDetail, aTimeStamp); 1.221 + }, 1.222 + 1.223 + /** 1.224 + * Create a new gesture object and attach resolution handler to it. 1.225 + * @param {Function} aGesture A gesture constructor. 1.226 + * @param {Number} aTimeStamp An original pointer event timeStamp. 1.227 + * @param {Array} aPoints All changed points associated with the new pointer 1.228 + * event. 1.229 + * @param {?String} aLastEvent Last pointer event type. 1.230 + */ 1.231 + _create: function GestureTracker__create(aGesture, aTimeStamp, aPoints, aLastEvent) { 1.232 + this.current = new aGesture(aTimeStamp, aPoints, aLastEvent); /* A constructor name should start with an uppercase letter. */ // jshint ignore:line 1.233 + this.current.then(this._onFulfill.bind(this)); 1.234 + }, 1.235 + 1.236 + /** 1.237 + * Handle the incoming pointer event with the existing gesture object. 1.238 + * @param {Object} aDetail A new pointer event detail. 1.239 + * @param {Number} aTimeStamp A new pointer event timeStamp. 1.240 + */ 1.241 + _update: function GestureTracker_update(aDetail, aTimeStamp) { 1.242 + this.current[aDetail.type](aDetail.points, aTimeStamp); 1.243 + }, 1.244 + 1.245 + /** 1.246 + * A resolution handler function for the current gesture promise. 1.247 + * @param {Object} aResult A resolution payload with the relevant gesture id 1.248 + * and an optional new gesture contructor. 1.249 + */ 1.250 + _onFulfill: function GestureTracker__onFulfill(aResult) { 1.251 + let {id, gestureType} = aResult; 1.252 + let current = this.current; 1.253 + // Do nothing if there's no existing gesture or there's already a newer 1.254 + // gesture. 1.255 + if (!current || current.id !== id) { 1.256 + return; 1.257 + } 1.258 + // Only create a gesture if we got a constructor. 1.259 + if (gestureType) { 1.260 + this._create(gestureType, current.startTime, current.points, 1.261 + current.lastEvent); 1.262 + } else { 1.263 + delete this.current; 1.264 + } 1.265 + } 1.266 +}; 1.267 + 1.268 +/** 1.269 + * Compile a mozAccessFuGesture detail structure. 1.270 + * @param {String} aType A gesture type. 1.271 + * @param {Object} aPoints Gesture's points. 1.272 + * @param {String} xKey A default key for the x coordinate. Default is 1.273 + * 'startX'. 1.274 + * @param {String} yKey A default key for the y coordinate. Default is 1.275 + * 'startY'. 1.276 + * @return {Object} a mozAccessFuGesture detail structure. 1.277 + */ 1.278 +function compileDetail(aType, aPoints, keyMap = {x: 'startX', y: 'startY'}) { 1.279 + let touches = []; 1.280 + let maxDeltaX = 0; 1.281 + let maxDeltaY = 0; 1.282 + for (let identifier in aPoints) { 1.283 + let point = aPoints[identifier]; 1.284 + let touch = {}; 1.285 + for (let key in keyMap) { 1.286 + touch[key] = point[keyMap[key]]; 1.287 + } 1.288 + touches.push(touch); 1.289 + let deltaX = point.x - point.startX; 1.290 + let deltaY = point.y - point.startY; 1.291 + // Determine the maximum x and y travel intervals. 1.292 + if (Math.abs(maxDeltaX) < Math.abs(deltaX)) { 1.293 + maxDeltaX = deltaX; 1.294 + } 1.295 + if (Math.abs(maxDeltaY) < Math.abs(deltaY)) { 1.296 + maxDeltaY = deltaY; 1.297 + } 1.298 + // Since the gesture is resolving, reset the points' distance information 1.299 + // since they are passed to the next potential gesture. 1.300 + point.reset(); 1.301 + } 1.302 + return { 1.303 + type: aType, 1.304 + touches: touches, 1.305 + deltaX: maxDeltaX, 1.306 + deltaY: maxDeltaY 1.307 + }; 1.308 +} 1.309 + 1.310 +/** 1.311 + * A general gesture object. 1.312 + * @param {Number} aTimeStamp An original pointer event's timeStamp that started 1.313 + * the gesture resolution sequence. 1.314 + * @param {Object} aPoints An existing set of points (from previous events). 1.315 + * Default is an empty object. 1.316 + * @param {?String} aLastEvent Last pointer event type. 1.317 + */ 1.318 +function Gesture(aTimeStamp, aPoints = {}, aLastEvent = undefined) { 1.319 + this.startTime = Date.now(); 1.320 + Logger.debug('Creating', this.id, 'gesture.'); 1.321 + this.points = aPoints; 1.322 + this.lastEvent = aLastEvent; 1.323 + this._deferred = Promise.defer(); 1.324 + // Call this._handleResolve or this._handleReject when the promise is 1.325 + // fulfilled with either resolve or reject. 1.326 + this.promise = this._deferred.promise.then(this._handleResolve.bind(this), 1.327 + this._handleReject.bind(this)); 1.328 + this.startTimer(aTimeStamp); 1.329 +} 1.330 + 1.331 +Gesture.prototype = { 1.332 + /** 1.333 + * Get the gesture timeout delay. 1.334 + * @return {Number} 1.335 + */ 1.336 + _getDelay: function Gesture__getDelay() { 1.337 + // If nothing happens withing the 1.338 + // GestureSettings.maxConsecutiveGestureDelay, we should not wait for any 1.339 + // more pointer events and consider them the part of the same gesture - 1.340 + // reject this gesture promise. 1.341 + return GestureSettings.maxConsecutiveGestureDelay; 1.342 + }, 1.343 + 1.344 + /** 1.345 + * Clear the existing timer. 1.346 + */ 1.347 + clearTimer: function Gesture_clearTimer() { 1.348 + clearTimeout(this._timer); 1.349 + delete this._timer; 1.350 + }, 1.351 + 1.352 + /** 1.353 + * Start the timer for gesture timeout. 1.354 + * @param {Number} aTimeStamp An original pointer event's timeStamp that 1.355 + * started the gesture resolution sequence. 1.356 + */ 1.357 + startTimer: function Gesture_startTimer(aTimeStamp) { 1.358 + this.clearTimer(); 1.359 + let delay = this._getDelay(aTimeStamp); 1.360 + let handler = () => { 1.361 + delete this._timer; 1.362 + if (!this._inProgress) { 1.363 + this._deferred.reject(); 1.364 + } else if (this._rejectToOnWait) { 1.365 + this._deferred.reject(this._rejectToOnWait); 1.366 + } 1.367 + }; 1.368 + if (delay <= 0) { 1.369 + handler(); 1.370 + } else { 1.371 + this._timer = setTimeout(handler, delay); 1.372 + } 1.373 + }, 1.374 + 1.375 + /** 1.376 + * Add a gesture promise resolution callback. 1.377 + * @param {Function} aCallback 1.378 + */ 1.379 + then: function Gesture_then(aCallback) { 1.380 + this.promise.then(aCallback); 1.381 + }, 1.382 + 1.383 + /** 1.384 + * Update gesture's points. Test the points set with the optional gesture test 1.385 + * function. 1.386 + * @param {Array} aPoints An array with the changed points from the new 1.387 + * pointer event. 1.388 + * @param {String} aType Pointer event type. 1.389 + * @param {Boolean} aCanCreate A flag that enables including the new points. 1.390 + * Default is false. 1.391 + * @param {Boolean} aNeedComplete A flag that indicates that the gesture is 1.392 + * completing. Default is false. 1.393 + * @return {Boolean} Indicates whether the gesture can be complete (it is 1.394 + * set to true iff the aNeedComplete is true and there was a change to at 1.395 + * least one point that belongs to the gesture). 1.396 + */ 1.397 + _update: function Gesture__update(aPoints, aType, aCanCreate = false, aNeedComplete = false) { 1.398 + let complete; 1.399 + let lastEvent; 1.400 + for (let point of aPoints) { 1.401 + let identifier = point.identifier; 1.402 + let gesturePoint = this.points[identifier]; 1.403 + if (gesturePoint) { 1.404 + gesturePoint.update(point); 1.405 + if (aNeedComplete) { 1.406 + // Since the gesture is completing and at least one of the gesture 1.407 + // points is updated, set the return value to true. 1.408 + complete = true; 1.409 + } 1.410 + lastEvent = lastEvent || aType; 1.411 + } else if (aCanCreate) { 1.412 + // Only create a new point if aCanCreate is true. 1.413 + this.points[identifier] = 1.414 + new Point(point); 1.415 + lastEvent = lastEvent || aType; 1.416 + } 1.417 + } 1.418 + this.lastEvent = lastEvent || this.lastEvent; 1.419 + // If test function is defined test the points. 1.420 + if (this.test) { 1.421 + this.test(complete); 1.422 + } 1.423 + return complete; 1.424 + }, 1.425 + 1.426 + /** 1.427 + * Emit a mozAccessFuGesture (when the gesture is resolved). 1.428 + * @param {Object} aDetail a compiled mozAccessFuGesture detail structure. 1.429 + */ 1.430 + _emit: function Gesture__emit(aDetail) { 1.431 + let evt = new Utils.win.CustomEvent('mozAccessFuGesture', { 1.432 + bubbles: true, 1.433 + cancelable: true, 1.434 + detail: aDetail 1.435 + }); 1.436 + Utils.win.dispatchEvent(evt); 1.437 + }, 1.438 + 1.439 + /** 1.440 + * Handle the pointer down event. 1.441 + * @param {Array} aPoints A new pointer down points. 1.442 + * @param {Number} aTimeStamp A new pointer down timeStamp. 1.443 + */ 1.444 + pointerdown: function Gesture_pointerdown(aPoints, aTimeStamp) { 1.445 + this._inProgress = true; 1.446 + this._update(aPoints, 'pointerdown', 1.447 + aTimeStamp - this.startTime < MAX_MULTITOUCH); 1.448 + }, 1.449 + 1.450 + /** 1.451 + * Handle the pointer move event. 1.452 + * @param {Array} aPoints A new pointer move points. 1.453 + */ 1.454 + pointermove: function Gesture_pointermove(aPoints) { 1.455 + this._update(aPoints, 'pointermove'); 1.456 + }, 1.457 + 1.458 + /** 1.459 + * Handle the pointer up event. 1.460 + * @param {Array} aPoints A new pointer up points. 1.461 + */ 1.462 + pointerup: function Gesture_pointerup(aPoints) { 1.463 + let complete = this._update(aPoints, 'pointerup', false, true); 1.464 + if (complete) { 1.465 + this._deferred.resolve(); 1.466 + } 1.467 + }, 1.468 + 1.469 + /** 1.470 + * A subsequent gesture constructor to resolve the current one to. E.g. 1.471 + * tap->doubletap, dwell->dwellend, etc. 1.472 + * @type {Function} 1.473 + */ 1.474 + resolveTo: null, 1.475 + 1.476 + /** 1.477 + * A unique id for the gesture. Composed of the type + timeStamp. 1.478 + */ 1.479 + get id() { 1.480 + delete this._id; 1.481 + this._id = this.type + this.startTime; 1.482 + return this._id; 1.483 + }, 1.484 + 1.485 + /** 1.486 + * A gesture promise resolve callback. Compile and emit the gesture. 1.487 + * @return {Object} Returns a structure to the gesture handler that looks like 1.488 + * this: { 1.489 + * id: current gesture id, 1.490 + * gestureType: an optional subsequent gesture constructor. 1.491 + * } 1.492 + */ 1.493 + _handleResolve: function Gesture__handleResolve() { 1.494 + if (this.isComplete) { 1.495 + return; 1.496 + } 1.497 + Logger.debug('Resolving', this.id, 'gesture.'); 1.498 + this.isComplete = true; 1.499 + let detail = this.compile(); 1.500 + if (detail) { 1.501 + this._emit(detail); 1.502 + } 1.503 + return { 1.504 + id: this.id, 1.505 + gestureType: this.resolveTo 1.506 + }; 1.507 + }, 1.508 + 1.509 + /** 1.510 + * A gesture promise reject callback. 1.511 + * @return {Object} Returns a structure to the gesture handler that looks like 1.512 + * this: { 1.513 + * id: current gesture id, 1.514 + * gestureType: an optional subsequent gesture constructor. 1.515 + * } 1.516 + */ 1.517 + _handleReject: function Gesture__handleReject(aRejectTo) { 1.518 + if (this.isComplete) { 1.519 + return; 1.520 + } 1.521 + Logger.debug('Rejecting', this.id, 'gesture.'); 1.522 + this.isComplete = true; 1.523 + return { 1.524 + id: this.id, 1.525 + gestureType: aRejectTo 1.526 + }; 1.527 + }, 1.528 + 1.529 + /** 1.530 + * A default compilation function used to build the mozAccessFuGesture event 1.531 + * detail. The detail always includes the type and the touches associated 1.532 + * with the gesture. 1.533 + * @return {Object} Gesture event detail. 1.534 + */ 1.535 + compile: function Gesture_compile() { 1.536 + return compileDetail(this.type, this.points); 1.537 + } 1.538 +}; 1.539 + 1.540 +/** 1.541 + * A mixin for an explore related object. 1.542 + */ 1.543 +function ExploreGesture() { 1.544 + this.compile = () => { 1.545 + // Unlike most of other gestures explore based gestures compile using the 1.546 + // current point position and not the start one. 1.547 + return compileDetail(this.type, this.points, {x: 'x', y: 'y'}); 1.548 + }; 1.549 +} 1.550 + 1.551 +/** 1.552 + * Check the in progress gesture for completion. 1.553 + */ 1.554 +function checkProgressGesture(aGesture) { 1.555 + aGesture._inProgress = true; 1.556 + if (aGesture.lastEvent === 'pointerup') { 1.557 + if (aGesture.test) { 1.558 + aGesture.test(true); 1.559 + } 1.560 + aGesture._deferred.resolve(); 1.561 + } 1.562 +} 1.563 + 1.564 +/** 1.565 + * A common travel gesture. When the travel gesture is created, all subsequent 1.566 + * pointer events' points are tested for their total distance traveled. If that 1.567 + * distance exceeds the _threshold distance, the gesture will be rejected to a 1.568 + * _travelTo gesture. 1.569 + * @param {Number} aTimeStamp An original pointer event's timeStamp that started 1.570 + * the gesture resolution sequence. 1.571 + * @param {Object} aPoints An existing set of points (from previous events). 1.572 + * @param {?String} aLastEvent Last pointer event type. 1.573 + * @param {Function} aTravelTo A contructor for the gesture to reject to when 1.574 + * travelling (default: Explore). 1.575 + * @param {Number} aThreshold Travel threshold (default: 1.576 + * GestureSettings.travelThreshold). 1.577 + */ 1.578 +function TravelGesture(aTimeStamp, aPoints, aLastEvent, aTravelTo = Explore, aThreshold = GestureSettings.travelThreshold) { 1.579 + Gesture.call(this, aTimeStamp, aPoints, aLastEvent); 1.580 + this._travelTo = aTravelTo; 1.581 + this._threshold = aThreshold; 1.582 +} 1.583 + 1.584 +TravelGesture.prototype = Object.create(Gesture.prototype); 1.585 + 1.586 +/** 1.587 + * Test the gesture points for travel. The gesture will be rejected to 1.588 + * this._travelTo gesture iff at least one point crosses this._threshold. 1.589 + */ 1.590 +TravelGesture.prototype.test = function TravelGesture_test() { 1.591 + for (let identifier in this.points) { 1.592 + let point = this.points[identifier]; 1.593 + if (point.totalDistanceTraveled / Utils.dpi > this._threshold) { 1.594 + this._deferred.reject(this._travelTo); 1.595 + return; 1.596 + } 1.597 + } 1.598 +}; 1.599 + 1.600 +/** 1.601 + * DwellEnd gesture. 1.602 + * @param {Number} aTimeStamp An original pointer event's timeStamp that started 1.603 + * the gesture resolution sequence. 1.604 + * @param {Object} aPoints An existing set of points (from previous events). 1.605 + * @param {?String} aLastEvent Last pointer event type. 1.606 + */ 1.607 +function DwellEnd(aTimeStamp, aPoints, aLastEvent) { 1.608 + this._inProgress = true; 1.609 + // If the pointer travels, reject to Explore. 1.610 + TravelGesture.call(this, aTimeStamp, aPoints, aLastEvent); 1.611 + checkProgressGesture(this); 1.612 +} 1.613 + 1.614 +DwellEnd.prototype = Object.create(TravelGesture.prototype); 1.615 +DwellEnd.prototype.type = 'dwellend'; 1.616 + 1.617 +/** 1.618 + * TapHoldEnd gesture. This gesture can be represented as the following diagram: 1.619 + * pointerdown-pointerup-pointerdown-*wait*-pointerup. 1.620 + * @param {Number} aTimeStamp An original pointer event's timeStamp that started 1.621 + * the gesture resolution sequence. 1.622 + * @param {Object} aPoints An existing set of points (from previous events). 1.623 + * @param {?String} aLastEvent Last pointer event type. 1.624 + */ 1.625 +function TapHoldEnd(aTimeStamp, aPoints, aLastEvent) { 1.626 + this._inProgress = true; 1.627 + // If the pointer travels, reject to Explore. 1.628 + TravelGesture.call(this, aTimeStamp, aPoints, aLastEvent); 1.629 + checkProgressGesture(this); 1.630 +} 1.631 + 1.632 +TapHoldEnd.prototype = Object.create(TravelGesture.prototype); 1.633 +TapHoldEnd.prototype.type = 'tapholdend'; 1.634 + 1.635 +/** 1.636 + * DoubleTapHoldEnd gesture. This gesture can be represented as the following 1.637 + * diagram: 1.638 + * pointerdown-pointerup-pointerdown-pointerup-pointerdown-*wait*-pointerup. 1.639 + * @param {Number} aTimeStamp An original pointer event's timeStamp that started 1.640 + * the gesture resolution sequence. 1.641 + * @param {Object} aPoints An existing set of points (from previous events). 1.642 + * @param {?String} aLastEvent Last pointer event type. 1.643 + */ 1.644 +function DoubleTapHoldEnd(aTimeStamp, aPoints, aLastEvent) { 1.645 + this._inProgress = true; 1.646 + // If the pointer travels, reject to Explore. 1.647 + TravelGesture.call(this, aTimeStamp, aPoints, aLastEvent); 1.648 + checkProgressGesture(this); 1.649 +} 1.650 + 1.651 +DoubleTapHoldEnd.prototype = Object.create(TravelGesture.prototype); 1.652 +DoubleTapHoldEnd.prototype.type = 'doubletapholdend'; 1.653 + 1.654 +/** 1.655 + * A common tap gesture object. 1.656 + * @param {Number} aTimeStamp An original pointer event's timeStamp that started 1.657 + * the gesture resolution sequence. 1.658 + * @param {Object} aPoints An existing set of points (from previous events). 1.659 + * @param {?String} aLastEvent Last pointer event type. 1.660 + * @param {Function} aRejectTo A constructor for the next gesture to reject to 1.661 + * in case no pointermove or pointerup happens within the 1.662 + * GestureSettings.dwellThreshold. 1.663 + * @param {Function} aTravelTo An optional constuctor for the next gesture to 1.664 + * reject to in case the the TravelGesture test fails. 1.665 + */ 1.666 +function TapGesture(aTimeStamp, aPoints, aLastEvent, aRejectTo, aTravelTo) { 1.667 + this._rejectToOnWait = aRejectTo; 1.668 + // If the pointer travels, reject to aTravelTo. 1.669 + TravelGesture.call(this, aTimeStamp, aPoints, aLastEvent, aTravelTo, 1.670 + TAP_MAX_RADIUS); 1.671 +} 1.672 + 1.673 +TapGesture.prototype = Object.create(TravelGesture.prototype); 1.674 +TapGesture.prototype._getDelay = function TapGesture__getDelay() { 1.675 + // If, for TapGesture, no pointermove or pointerup happens within the 1.676 + // GestureSettings.dwellThreshold, reject. 1.677 + // Note: the original pointer event's timeStamp is irrelevant here. 1.678 + return GestureSettings.dwellThreshold; 1.679 +}; 1.680 + 1.681 +/** 1.682 + * Tap gesture. 1.683 + * @param {Number} aTimeStamp An original pointer event's timeStamp that started 1.684 + * the gesture resolution sequence. 1.685 + * @param {Object} aPoints An existing set of points (from previous events). 1.686 + * @param {?String} aLastEvent Last pointer event type. 1.687 + */ 1.688 +function Tap(aTimeStamp, aPoints, aLastEvent) { 1.689 + // If the pointer travels, reject to Swipe. 1.690 + TapGesture.call(this, aTimeStamp, aPoints, aLastEvent, Dwell, Swipe); 1.691 +} 1.692 + 1.693 +Tap.prototype = Object.create(TapGesture.prototype); 1.694 +Tap.prototype.type = 'tap'; 1.695 +Tap.prototype.resolveTo = DoubleTap; 1.696 + 1.697 +/** 1.698 + * Tap (multi) gesture on Android. 1.699 + * @param {Number} aTimeStamp An original pointer event's timeStamp that started 1.700 + * the gesture resolution sequence. 1.701 + * @param {Object} aPoints An existing set of points (from previous events). 1.702 + * @param {?String} aLastEvent Last pointer event type. 1.703 + */ 1.704 +function AndroidTap(aTimeStamp, aPoints, aLastEvent) { 1.705 + // If the pointer travels, reject to Swipe. On dwell threshold reject to 1.706 + // TapHold. 1.707 + TapGesture.call(this, aTimeStamp, aPoints, aLastEvent, TapHold, Swipe); 1.708 +} 1.709 +AndroidTap.prototype = Object.create(TapGesture.prototype); 1.710 +// Android double taps are translated to single taps. 1.711 +AndroidTap.prototype.type = 'doubletap'; 1.712 +AndroidTap.prototype.resolveTo = TripleTap; 1.713 + 1.714 +/** 1.715 + * Clear the pointerup handler timer in case of the 3 pointer swipe. 1.716 + */ 1.717 +AndroidTap.prototype.clearThreeFingerSwipeTimer = function AndroidTap_clearThreeFingerSwipeTimer() { 1.718 + clearTimeout(this._threeFingerSwipeTimer); 1.719 + delete this._threeFingerSwipeTimer; 1.720 +}; 1.721 + 1.722 +AndroidTap.prototype.pointerdown = function AndroidTap_pointerdown(aPoints, aTimeStamp) { 1.723 + this.clearThreeFingerSwipeTimer(); 1.724 + TapGesture.prototype.pointerdown.call(this, aPoints, aTimeStamp); 1.725 +}; 1.726 + 1.727 +AndroidTap.prototype.pointermove = function AndroidTap_pointermove(aPoints) { 1.728 + this.clearThreeFingerSwipeTimer(); 1.729 + this._moved = true; 1.730 + TapGesture.prototype.pointermove.call(this, aPoints); 1.731 +}; 1.732 + 1.733 +AndroidTap.prototype.pointerup = function AndroidTap_pointerup(aPoints) { 1.734 + if (this._moved) { 1.735 + // If there was a pointer move - handle the real gesture. 1.736 + TapGesture.prototype.pointerup.call(this, aPoints); 1.737 + } else { 1.738 + // Primptively delay the multi pointer gesture resolution, because Android 1.739 + // sometimes fires a pointerdown/poitnerup sequence before the real events. 1.740 + this._threeFingerSwipeTimer = setTimeout(() => { 1.741 + delete this._threeFingerSwipeTimer; 1.742 + TapGesture.prototype.pointerup.call(this, aPoints); 1.743 + }, ANDROID_TRIPLE_SWIPE_DELAY); 1.744 + } 1.745 +}; 1.746 + 1.747 +/** 1.748 + * Reject an android tap gesture. 1.749 + * @param {?Function} aRejectTo An optional next gesture constructor. 1.750 + * @return {Object} structure that looks like { 1.751 + * id: gesture_id, // Current AndroidTap gesture id. 1.752 + * gestureType: next_gesture // Optional 1.753 + * } 1.754 + */ 1.755 +AndroidTap.prototype._handleReject = function AndroidTap__handleReject(aRejectTo) { 1.756 + let keys = Object.keys(this.points); 1.757 + if (aRejectTo === Swipe && keys.length === 1) { 1.758 + let key = keys[0]; 1.759 + let point = this.points[key]; 1.760 + // Two finger swipe is translated into single swipe. 1.761 + this.points[key + '-copy'] = point; 1.762 + } 1.763 + return TapGesture.prototype._handleReject.call(this, aRejectTo); 1.764 +}; 1.765 + 1.766 +/** 1.767 + * Double Tap gesture. 1.768 + * @param {Number} aTimeStamp An original pointer event's timeStamp that started 1.769 + * the gesture resolution sequence. 1.770 + * @param {Object} aPoints An existing set of points (from previous events). 1.771 + * @param {?String} aLastEvent Last pointer event type. 1.772 + */ 1.773 +function DoubleTap(aTimeStamp, aPoints, aLastEvent) { 1.774 + TapGesture.call(this, aTimeStamp, aPoints, aLastEvent, TapHold); 1.775 +} 1.776 + 1.777 +DoubleTap.prototype = Object.create(TapGesture.prototype); 1.778 +DoubleTap.prototype.type = 'doubletap'; 1.779 +DoubleTap.prototype.resolveTo = TripleTap; 1.780 + 1.781 +/** 1.782 + * Triple Tap gesture. 1.783 + * @param {Number} aTimeStamp An original pointer event's timeStamp that started 1.784 + * the gesture resolution sequence. 1.785 + * @param {Object} aPoints An existing set of points (from previous events). 1.786 + * @param {?String} aLastEvent Last pointer event type. 1.787 + */ 1.788 +function TripleTap(aTimeStamp, aPoints, aLastEvent) { 1.789 + TapGesture.call(this, aTimeStamp, aPoints, aLastEvent, DoubleTapHold); 1.790 +} 1.791 + 1.792 +TripleTap.prototype = Object.create(TapGesture.prototype); 1.793 +TripleTap.prototype.type = 'tripletap'; 1.794 + 1.795 +/** 1.796 + * Common base object for gestures that are created as resolved. 1.797 + * @param {Number} aTimeStamp An original pointer event's timeStamp that started 1.798 + * the gesture resolution sequence. 1.799 + * @param {Object} aPoints An existing set of points (from previous events). 1.800 + * @param {?String} aLastEvent Last pointer event type. 1.801 + */ 1.802 +function ResolvedGesture(aTimeStamp, aPoints, aLastEvent) { 1.803 + Gesture.call(this, aTimeStamp, aPoints, aLastEvent); 1.804 + // Resolve the guesture right away. 1.805 + this._deferred.resolve(); 1.806 +} 1.807 + 1.808 +ResolvedGesture.prototype = Object.create(Gesture.prototype); 1.809 + 1.810 +/** 1.811 + * Dwell gesture 1.812 + * @param {Number} aTimeStamp An original pointer event's timeStamp that started 1.813 + * the gesture resolution sequence. 1.814 + * @param {Object} aPoints An existing set of points (from previous events). 1.815 + * @param {?String} aLastEvent Last pointer event type. 1.816 + */ 1.817 +function Dwell(aTimeStamp, aPoints, aLastEvent) { 1.818 + ResolvedGesture.call(this, aTimeStamp, aPoints, aLastEvent); 1.819 +} 1.820 + 1.821 +Dwell.prototype = Object.create(ResolvedGesture.prototype); 1.822 +Dwell.prototype.type = 'dwell'; 1.823 +Dwell.prototype.resolveTo = DwellEnd; 1.824 + 1.825 +/** 1.826 + * TapHold gesture 1.827 + * @param {Number} aTimeStamp An original pointer event's timeStamp that started 1.828 + * the gesture resolution sequence. 1.829 + * @param {Object} aPoints An existing set of points (from previous events). 1.830 + * @param {?String} aLastEvent Last pointer event type. 1.831 + */ 1.832 +function TapHold(aTimeStamp, aPoints, aLastEvent) { 1.833 + ResolvedGesture.call(this, aTimeStamp, aPoints, aLastEvent); 1.834 +} 1.835 + 1.836 +TapHold.prototype = Object.create(ResolvedGesture.prototype); 1.837 +TapHold.prototype.type = 'taphold'; 1.838 +TapHold.prototype.resolveTo = TapHoldEnd; 1.839 + 1.840 +/** 1.841 + * DoubleTapHold gesture 1.842 + * @param {Number} aTimeStamp An original pointer event's timeStamp that started 1.843 + * the gesture resolution sequence. 1.844 + * @param {Object} aPoints An existing set of points (from previous events). 1.845 + * @param {?String} aLastEvent Last pointer event type. 1.846 + */ 1.847 +function DoubleTapHold(aTimeStamp, aPoints, aLastEvent) { 1.848 + ResolvedGesture.call(this, aTimeStamp, aPoints, aLastEvent); 1.849 +} 1.850 + 1.851 +DoubleTapHold.prototype = Object.create(ResolvedGesture.prototype); 1.852 +DoubleTapHold.prototype.type = 'doubletaphold'; 1.853 +DoubleTapHold.prototype.resolveTo = DoubleTapHoldEnd; 1.854 + 1.855 +/** 1.856 + * Explore gesture 1.857 + * @param {Number} aTimeStamp An original pointer event's timeStamp that started 1.858 + * the gesture resolution sequence. 1.859 + * @param {Object} aPoints An existing set of points (from previous events). 1.860 + * @param {?String} aLastEvent Last pointer event type. 1.861 + */ 1.862 +function Explore(aTimeStamp, aPoints, aLastEvent) { 1.863 + ExploreGesture.call(this); 1.864 + ResolvedGesture.call(this, aTimeStamp, aPoints, aLastEvent); 1.865 +} 1.866 + 1.867 +Explore.prototype = Object.create(ResolvedGesture.prototype); 1.868 +Explore.prototype.type = 'explore'; 1.869 +Explore.prototype.resolveTo = ExploreEnd; 1.870 + 1.871 +/** 1.872 + * ExploreEnd gesture. 1.873 + * @param {Number} aTimeStamp An original pointer event's timeStamp that started 1.874 + * the gesture resolution sequence. 1.875 + * @param {Object} aPoints An existing set of points (from previous events). 1.876 + * @param {?String} aLastEvent Last pointer event type. 1.877 + */ 1.878 +function ExploreEnd(aTimeStamp, aPoints, aLastEvent) { 1.879 + this._inProgress = true; 1.880 + ExploreGesture.call(this); 1.881 + // If the pointer travels, reject to Explore. 1.882 + TravelGesture.call(this, aTimeStamp, aPoints, aLastEvent); 1.883 + checkProgressGesture(this); 1.884 +} 1.885 + 1.886 +ExploreEnd.prototype = Object.create(TravelGesture.prototype); 1.887 +ExploreEnd.prototype.type = 'exploreend'; 1.888 + 1.889 +/** 1.890 + * Swipe gesture. 1.891 + * @param {Number} aTimeStamp An original pointer event's timeStamp that started 1.892 + * the gesture resolution sequence. 1.893 + * @param {Object} aPoints An existing set of points (from previous events). 1.894 + * @param {?String} aLastEvent Last pointer event type. 1.895 + */ 1.896 +function Swipe(aTimeStamp, aPoints, aLastEvent) { 1.897 + this._inProgress = true; 1.898 + this._rejectToOnWait = Explore; 1.899 + Gesture.call(this, aTimeStamp, aPoints, aLastEvent); 1.900 + checkProgressGesture(this); 1.901 +} 1.902 + 1.903 +Swipe.prototype = Object.create(Gesture.prototype); 1.904 +Swipe.prototype.type = 'swipe'; 1.905 +Swipe.prototype._getDelay = function Swipe__getDelay(aTimeStamp) { 1.906 + // Swipe should be completed within the GestureSettings.swipeMaxDuration from 1.907 + // the initial pointer down event. 1.908 + return GestureSettings.swipeMaxDuration - this.startTime + aTimeStamp; 1.909 +}; 1.910 + 1.911 +/** 1.912 + * Determine wither the gesture was Swipe or Explore. 1.913 + * @param {Booler} aComplete A flag that indicates whether the gesture is and 1.914 + * will be complete after the test. 1.915 + */ 1.916 +Swipe.prototype.test = function Swipe_test(aComplete) { 1.917 + if (!aComplete) { 1.918 + // No need to test if the gesture is not completing or can't be complete. 1.919 + return; 1.920 + } 1.921 + let reject = true; 1.922 + // If at least one point travelled for more than SWIPE_MIN_DISTANCE and it was 1.923 + // direct enough, consider it a Swipe. 1.924 + for (let identifier in this.points) { 1.925 + let point = this.points[identifier]; 1.926 + let directDistance = point.directDistanceTraveled; 1.927 + if (directDistance / Utils.dpi >= SWIPE_MIN_DISTANCE || 1.928 + directDistance * DIRECTNESS_COEFF >= point.totalDistanceTraveled) { 1.929 + reject = false; 1.930 + } 1.931 + } 1.932 + if (reject) { 1.933 + this._deferred.reject(Explore); 1.934 + } 1.935 +}; 1.936 + 1.937 +/** 1.938 + * Compile a swipe related mozAccessFuGesture event detail. 1.939 + * @return {Object} A mozAccessFuGesture detail object. 1.940 + */ 1.941 +Swipe.prototype.compile = function Swipe_compile() { 1.942 + let type = this.type; 1.943 + let detail = compileDetail(type, this.points, 1.944 + {x1: 'startX', y1: 'startY', x2: 'x', y2: 'y'}); 1.945 + let deltaX = detail.deltaX; 1.946 + let deltaY = detail.deltaY; 1.947 + if (Math.abs(deltaX) > Math.abs(deltaY)) { 1.948 + // Horizontal swipe. 1.949 + detail.type = type + (deltaX > 0 ? 'right' : 'left'); 1.950 + } else { 1.951 + // Vertival swipe. 1.952 + detail.type = type + (deltaY > 0 ? 'down' : 'up'); 1.953 + } 1.954 + return detail; 1.955 +};