michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this file, michael@0: * You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: /* global Components, GestureSettings, XPCOMUtils, Utils, Promise, Logger */ michael@0: /* exported GestureSettings, GestureTracker */ michael@0: michael@0: /****************************************************************************** michael@0: All gestures have the following pathways when being resolved(v)/rejected(x): michael@0: Tap -> DoubleTap (v) michael@0: -> Dwell (x) michael@0: -> Swipe (x) michael@0: michael@0: AndroidTap -> TripleTap (v) michael@0: -> TapHold (x) michael@0: -> Swipe (x) michael@0: michael@0: DoubleTap -> TripleTap (v) michael@0: -> TapHold (x) michael@0: -> Explore (x) michael@0: michael@0: TripleTap -> DoubleTapHold (x) michael@0: -> Explore (x) michael@0: michael@0: Dwell -> DwellEnd (v) michael@0: michael@0: Swipe -> Explore (x) michael@0: michael@0: TapHold -> TapHoldEnd (v) michael@0: michael@0: DoubleTapHold -> DoubleTapHoldEnd (v) michael@0: michael@0: DwellEnd -> Explore (x) michael@0: michael@0: TapHoldEnd -> Explore (x) michael@0: michael@0: DoubleTapHoldEnd -> Explore (x) michael@0: michael@0: ExploreEnd -> Explore (x) michael@0: michael@0: Explore -> ExploreEnd (v) michael@0: ******************************************************************************/ michael@0: michael@0: 'use strict'; michael@0: michael@0: const Ci = Components.interfaces; michael@0: const Cu = Components.utils; michael@0: michael@0: this.EXPORTED_SYMBOLS = ['GestureSettings', 'GestureTracker']; // jshint ignore:line michael@0: michael@0: Cu.import('resource://gre/modules/XPCOMUtils.jsm'); michael@0: michael@0: XPCOMUtils.defineLazyModuleGetter(this, 'Utils', // jshint ignore:line michael@0: 'resource://gre/modules/accessibility/Utils.jsm'); michael@0: XPCOMUtils.defineLazyModuleGetter(this, 'Logger', // jshint ignore:line michael@0: 'resource://gre/modules/accessibility/Utils.jsm'); michael@0: XPCOMUtils.defineLazyModuleGetter(this, 'setTimeout', // jshint ignore:line michael@0: 'resource://gre/modules/Timer.jsm'); michael@0: XPCOMUtils.defineLazyModuleGetter(this, 'clearTimeout', // jshint ignore:line michael@0: 'resource://gre/modules/Timer.jsm'); michael@0: XPCOMUtils.defineLazyModuleGetter(this, 'Promise', // jshint ignore:line michael@0: 'resource://gre/modules/Promise.jsm'); michael@0: michael@0: // Maximum amount of time allowed for a gesture to be considered a multitouch. michael@0: const MAX_MULTITOUCH = 250; michael@0: // Minimal swipe distance in inches michael@0: const SWIPE_MIN_DISTANCE = 0.4; michael@0: // Maximum distance the pointer could move during a tap in inches michael@0: const TAP_MAX_RADIUS = 0.2; michael@0: // Directness coefficient. It is based on the maximum 15 degree angle between michael@0: // consequent pointer move lines. michael@0: const DIRECTNESS_COEFF = 1.44; michael@0: // An android flag. michael@0: const IS_ANDROID = Utils.MozBuildApp === 'mobile/android' && michael@0: Utils.AndroidSdkVersion >= 14; michael@0: // A single pointer down/up sequence periodically precedes the tripple swipe michael@0: // gesture on Android. This delay acounts for that. michael@0: const ANDROID_TRIPLE_SWIPE_DELAY = 50; michael@0: // The virtual touch ID generated by a mouse event. michael@0: const MOUSE_ID = 'mouse'; michael@0: michael@0: /** michael@0: * A point object containing distance travelled data. michael@0: * @param {Object} aPoint A point object that looks like: { michael@0: * x: x coordinate in pixels, michael@0: * y: y coordinate in pixels michael@0: * } michael@0: */ michael@0: function Point(aPoint) { michael@0: this.startX = this.x = aPoint.x; michael@0: this.startY = this.y = aPoint.y; michael@0: this.distanceTraveled = 0; michael@0: this.totalDistanceTraveled = 0; michael@0: } michael@0: michael@0: Point.prototype = { michael@0: /** michael@0: * Update the current point coordiates. michael@0: * @param {Object} aPoint A new point coordinates. michael@0: */ michael@0: update: function Point_update(aPoint) { michael@0: let lastX = this.x; michael@0: let lastY = this.y; michael@0: this.x = aPoint.x; michael@0: this.y = aPoint.y; michael@0: this.distanceTraveled = this.getDistanceToCoord(lastX, lastY); michael@0: this.totalDistanceTraveled += this.distanceTraveled; michael@0: }, michael@0: michael@0: reset: function Point_reset() { michael@0: this.distanceTraveled = 0; michael@0: this.totalDistanceTraveled = 0; michael@0: }, michael@0: michael@0: /** michael@0: * Get distance between the current point coordinates and the given ones. michael@0: * @param {Number} aX A pixel value for the x coordinate. michael@0: * @param {Number} aY A pixel value for the y coordinate. michael@0: * @return {Number} A distance between point's current and the given michael@0: * coordinates. michael@0: */ michael@0: getDistanceToCoord: function Point_getDistanceToCoord(aX, aY) { michael@0: return Math.hypot(this.x - aX, this.y - aY); michael@0: }, michael@0: michael@0: /** michael@0: * Get the direct distance travelled by the point so far. michael@0: */ michael@0: get directDistanceTraveled() { michael@0: return this.getDistanceToCoord(this.startX, this.startY); michael@0: } michael@0: }; michael@0: michael@0: /** michael@0: * An externally accessible collection of settings used in gesture resolition. michael@0: * @type {Object} michael@0: */ michael@0: this.GestureSettings = { // jshint ignore:line michael@0: /** michael@0: * Maximum duration of swipe michael@0: * @type {Number} michael@0: */ michael@0: swipeMaxDuration: 400, michael@0: michael@0: /** michael@0: * Maximum consecutive pointer event timeout. michael@0: * @type {Number} michael@0: */ michael@0: maxConsecutiveGestureDelay: 400, michael@0: michael@0: /** michael@0: * Delay before tap turns into dwell michael@0: * @type {Number} michael@0: */ michael@0: dwellThreshold: 500, michael@0: michael@0: /** michael@0: * Minimum distance that needs to be travelled for the pointer move to be michael@0: * fired. michael@0: * @type {Number} michael@0: */ michael@0: travelThreshold: 0.025 michael@0: }; michael@0: michael@0: /** michael@0: * An interface that handles the pointer events and calculates the appropriate michael@0: * gestures. michael@0: * @type {Object} michael@0: */ michael@0: this.GestureTracker = { // jshint ignore:line michael@0: /** michael@0: * Reset GestureTracker to its initial state. michael@0: * @return {[type]} [description] michael@0: */ michael@0: reset: function GestureTracker_reset() { michael@0: if (this.current) { michael@0: this.current.clearTimer(); michael@0: } michael@0: delete this.current; michael@0: }, michael@0: michael@0: /** michael@0: * Create a new gesture object and attach resolution handler to it as well as michael@0: * handle the incoming pointer event. michael@0: * @param {Object} aDetail A new pointer event detail. michael@0: * @param {Number} aTimeStamp A new pointer event timeStamp. michael@0: * @param {Function} aGesture A gesture constructor (default: Tap). michael@0: */ michael@0: _init: function GestureTracker__init(aDetail, aTimeStamp, aGesture = Tap) { michael@0: // Only create a new gesture on |pointerdown| event. michael@0: if (aDetail.type !== 'pointerdown') { michael@0: return; michael@0: } michael@0: let points = aDetail.points; michael@0: let GestureConstructor = aGesture; michael@0: if (IS_ANDROID && GestureConstructor === Tap && points.length === 1 && michael@0: points[0].identifier !== MOUSE_ID) { michael@0: // Handle Android events when EBT is enabled. Two finger gestures are michael@0: // translated to one. michael@0: GestureConstructor = AndroidTap; michael@0: } michael@0: this._create(GestureConstructor); michael@0: this._update(aDetail, aTimeStamp); michael@0: }, michael@0: michael@0: /** michael@0: * Handle the incoming pointer event with the existing gesture object(if michael@0: * present) or with the newly created one. michael@0: * @param {Object} aDetail A new pointer event detail. michael@0: * @param {Number} aTimeStamp A new pointer event timeStamp. michael@0: */ michael@0: handle: function GestureTracker_handle(aDetail, aTimeStamp) { michael@0: Logger.debug(() => { michael@0: return ['Pointer event', aDetail.type, 'at:', aTimeStamp, michael@0: JSON.stringify(aDetail.points)]; michael@0: }); michael@0: this[this.current ? '_update' : '_init'](aDetail, aTimeStamp); michael@0: }, michael@0: michael@0: /** michael@0: * Create a new gesture object and attach resolution handler to it. michael@0: * @param {Function} aGesture A gesture constructor. michael@0: * @param {Number} aTimeStamp An original pointer event timeStamp. michael@0: * @param {Array} aPoints All changed points associated with the new pointer michael@0: * event. michael@0: * @param {?String} aLastEvent Last pointer event type. michael@0: */ michael@0: _create: function GestureTracker__create(aGesture, aTimeStamp, aPoints, aLastEvent) { michael@0: this.current = new aGesture(aTimeStamp, aPoints, aLastEvent); /* A constructor name should start with an uppercase letter. */ // jshint ignore:line michael@0: this.current.then(this._onFulfill.bind(this)); michael@0: }, michael@0: michael@0: /** michael@0: * Handle the incoming pointer event with the existing gesture object. michael@0: * @param {Object} aDetail A new pointer event detail. michael@0: * @param {Number} aTimeStamp A new pointer event timeStamp. michael@0: */ michael@0: _update: function GestureTracker_update(aDetail, aTimeStamp) { michael@0: this.current[aDetail.type](aDetail.points, aTimeStamp); michael@0: }, michael@0: michael@0: /** michael@0: * A resolution handler function for the current gesture promise. michael@0: * @param {Object} aResult A resolution payload with the relevant gesture id michael@0: * and an optional new gesture contructor. michael@0: */ michael@0: _onFulfill: function GestureTracker__onFulfill(aResult) { michael@0: let {id, gestureType} = aResult; michael@0: let current = this.current; michael@0: // Do nothing if there's no existing gesture or there's already a newer michael@0: // gesture. michael@0: if (!current || current.id !== id) { michael@0: return; michael@0: } michael@0: // Only create a gesture if we got a constructor. michael@0: if (gestureType) { michael@0: this._create(gestureType, current.startTime, current.points, michael@0: current.lastEvent); michael@0: } else { michael@0: delete this.current; michael@0: } michael@0: } michael@0: }; michael@0: michael@0: /** michael@0: * Compile a mozAccessFuGesture detail structure. michael@0: * @param {String} aType A gesture type. michael@0: * @param {Object} aPoints Gesture's points. michael@0: * @param {String} xKey A default key for the x coordinate. Default is michael@0: * 'startX'. michael@0: * @param {String} yKey A default key for the y coordinate. Default is michael@0: * 'startY'. michael@0: * @return {Object} a mozAccessFuGesture detail structure. michael@0: */ michael@0: function compileDetail(aType, aPoints, keyMap = {x: 'startX', y: 'startY'}) { michael@0: let touches = []; michael@0: let maxDeltaX = 0; michael@0: let maxDeltaY = 0; michael@0: for (let identifier in aPoints) { michael@0: let point = aPoints[identifier]; michael@0: let touch = {}; michael@0: for (let key in keyMap) { michael@0: touch[key] = point[keyMap[key]]; michael@0: } michael@0: touches.push(touch); michael@0: let deltaX = point.x - point.startX; michael@0: let deltaY = point.y - point.startY; michael@0: // Determine the maximum x and y travel intervals. michael@0: if (Math.abs(maxDeltaX) < Math.abs(deltaX)) { michael@0: maxDeltaX = deltaX; michael@0: } michael@0: if (Math.abs(maxDeltaY) < Math.abs(deltaY)) { michael@0: maxDeltaY = deltaY; michael@0: } michael@0: // Since the gesture is resolving, reset the points' distance information michael@0: // since they are passed to the next potential gesture. michael@0: point.reset(); michael@0: } michael@0: return { michael@0: type: aType, michael@0: touches: touches, michael@0: deltaX: maxDeltaX, michael@0: deltaY: maxDeltaY michael@0: }; michael@0: } michael@0: michael@0: /** michael@0: * A general gesture object. michael@0: * @param {Number} aTimeStamp An original pointer event's timeStamp that started michael@0: * the gesture resolution sequence. michael@0: * @param {Object} aPoints An existing set of points (from previous events). michael@0: * Default is an empty object. michael@0: * @param {?String} aLastEvent Last pointer event type. michael@0: */ michael@0: function Gesture(aTimeStamp, aPoints = {}, aLastEvent = undefined) { michael@0: this.startTime = Date.now(); michael@0: Logger.debug('Creating', this.id, 'gesture.'); michael@0: this.points = aPoints; michael@0: this.lastEvent = aLastEvent; michael@0: this._deferred = Promise.defer(); michael@0: // Call this._handleResolve or this._handleReject when the promise is michael@0: // fulfilled with either resolve or reject. michael@0: this.promise = this._deferred.promise.then(this._handleResolve.bind(this), michael@0: this._handleReject.bind(this)); michael@0: this.startTimer(aTimeStamp); michael@0: } michael@0: michael@0: Gesture.prototype = { michael@0: /** michael@0: * Get the gesture timeout delay. michael@0: * @return {Number} michael@0: */ michael@0: _getDelay: function Gesture__getDelay() { michael@0: // If nothing happens withing the michael@0: // GestureSettings.maxConsecutiveGestureDelay, we should not wait for any michael@0: // more pointer events and consider them the part of the same gesture - michael@0: // reject this gesture promise. michael@0: return GestureSettings.maxConsecutiveGestureDelay; michael@0: }, michael@0: michael@0: /** michael@0: * Clear the existing timer. michael@0: */ michael@0: clearTimer: function Gesture_clearTimer() { michael@0: clearTimeout(this._timer); michael@0: delete this._timer; michael@0: }, michael@0: michael@0: /** michael@0: * Start the timer for gesture timeout. michael@0: * @param {Number} aTimeStamp An original pointer event's timeStamp that michael@0: * started the gesture resolution sequence. michael@0: */ michael@0: startTimer: function Gesture_startTimer(aTimeStamp) { michael@0: this.clearTimer(); michael@0: let delay = this._getDelay(aTimeStamp); michael@0: let handler = () => { michael@0: delete this._timer; michael@0: if (!this._inProgress) { michael@0: this._deferred.reject(); michael@0: } else if (this._rejectToOnWait) { michael@0: this._deferred.reject(this._rejectToOnWait); michael@0: } michael@0: }; michael@0: if (delay <= 0) { michael@0: handler(); michael@0: } else { michael@0: this._timer = setTimeout(handler, delay); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Add a gesture promise resolution callback. michael@0: * @param {Function} aCallback michael@0: */ michael@0: then: function Gesture_then(aCallback) { michael@0: this.promise.then(aCallback); michael@0: }, michael@0: michael@0: /** michael@0: * Update gesture's points. Test the points set with the optional gesture test michael@0: * function. michael@0: * @param {Array} aPoints An array with the changed points from the new michael@0: * pointer event. michael@0: * @param {String} aType Pointer event type. michael@0: * @param {Boolean} aCanCreate A flag that enables including the new points. michael@0: * Default is false. michael@0: * @param {Boolean} aNeedComplete A flag that indicates that the gesture is michael@0: * completing. Default is false. michael@0: * @return {Boolean} Indicates whether the gesture can be complete (it is michael@0: * set to true iff the aNeedComplete is true and there was a change to at michael@0: * least one point that belongs to the gesture). michael@0: */ michael@0: _update: function Gesture__update(aPoints, aType, aCanCreate = false, aNeedComplete = false) { michael@0: let complete; michael@0: let lastEvent; michael@0: for (let point of aPoints) { michael@0: let identifier = point.identifier; michael@0: let gesturePoint = this.points[identifier]; michael@0: if (gesturePoint) { michael@0: gesturePoint.update(point); michael@0: if (aNeedComplete) { michael@0: // Since the gesture is completing and at least one of the gesture michael@0: // points is updated, set the return value to true. michael@0: complete = true; michael@0: } michael@0: lastEvent = lastEvent || aType; michael@0: } else if (aCanCreate) { michael@0: // Only create a new point if aCanCreate is true. michael@0: this.points[identifier] = michael@0: new Point(point); michael@0: lastEvent = lastEvent || aType; michael@0: } michael@0: } michael@0: this.lastEvent = lastEvent || this.lastEvent; michael@0: // If test function is defined test the points. michael@0: if (this.test) { michael@0: this.test(complete); michael@0: } michael@0: return complete; michael@0: }, michael@0: michael@0: /** michael@0: * Emit a mozAccessFuGesture (when the gesture is resolved). michael@0: * @param {Object} aDetail a compiled mozAccessFuGesture detail structure. michael@0: */ michael@0: _emit: function Gesture__emit(aDetail) { michael@0: let evt = new Utils.win.CustomEvent('mozAccessFuGesture', { michael@0: bubbles: true, michael@0: cancelable: true, michael@0: detail: aDetail michael@0: }); michael@0: Utils.win.dispatchEvent(evt); michael@0: }, michael@0: michael@0: /** michael@0: * Handle the pointer down event. michael@0: * @param {Array} aPoints A new pointer down points. michael@0: * @param {Number} aTimeStamp A new pointer down timeStamp. michael@0: */ michael@0: pointerdown: function Gesture_pointerdown(aPoints, aTimeStamp) { michael@0: this._inProgress = true; michael@0: this._update(aPoints, 'pointerdown', michael@0: aTimeStamp - this.startTime < MAX_MULTITOUCH); michael@0: }, michael@0: michael@0: /** michael@0: * Handle the pointer move event. michael@0: * @param {Array} aPoints A new pointer move points. michael@0: */ michael@0: pointermove: function Gesture_pointermove(aPoints) { michael@0: this._update(aPoints, 'pointermove'); michael@0: }, michael@0: michael@0: /** michael@0: * Handle the pointer up event. michael@0: * @param {Array} aPoints A new pointer up points. michael@0: */ michael@0: pointerup: function Gesture_pointerup(aPoints) { michael@0: let complete = this._update(aPoints, 'pointerup', false, true); michael@0: if (complete) { michael@0: this._deferred.resolve(); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * A subsequent gesture constructor to resolve the current one to. E.g. michael@0: * tap->doubletap, dwell->dwellend, etc. michael@0: * @type {Function} michael@0: */ michael@0: resolveTo: null, michael@0: michael@0: /** michael@0: * A unique id for the gesture. Composed of the type + timeStamp. michael@0: */ michael@0: get id() { michael@0: delete this._id; michael@0: this._id = this.type + this.startTime; michael@0: return this._id; michael@0: }, michael@0: michael@0: /** michael@0: * A gesture promise resolve callback. Compile and emit the gesture. michael@0: * @return {Object} Returns a structure to the gesture handler that looks like michael@0: * this: { michael@0: * id: current gesture id, michael@0: * gestureType: an optional subsequent gesture constructor. michael@0: * } michael@0: */ michael@0: _handleResolve: function Gesture__handleResolve() { michael@0: if (this.isComplete) { michael@0: return; michael@0: } michael@0: Logger.debug('Resolving', this.id, 'gesture.'); michael@0: this.isComplete = true; michael@0: let detail = this.compile(); michael@0: if (detail) { michael@0: this._emit(detail); michael@0: } michael@0: return { michael@0: id: this.id, michael@0: gestureType: this.resolveTo michael@0: }; michael@0: }, michael@0: michael@0: /** michael@0: * A gesture promise reject callback. michael@0: * @return {Object} Returns a structure to the gesture handler that looks like michael@0: * this: { michael@0: * id: current gesture id, michael@0: * gestureType: an optional subsequent gesture constructor. michael@0: * } michael@0: */ michael@0: _handleReject: function Gesture__handleReject(aRejectTo) { michael@0: if (this.isComplete) { michael@0: return; michael@0: } michael@0: Logger.debug('Rejecting', this.id, 'gesture.'); michael@0: this.isComplete = true; michael@0: return { michael@0: id: this.id, michael@0: gestureType: aRejectTo michael@0: }; michael@0: }, michael@0: michael@0: /** michael@0: * A default compilation function used to build the mozAccessFuGesture event michael@0: * detail. The detail always includes the type and the touches associated michael@0: * with the gesture. michael@0: * @return {Object} Gesture event detail. michael@0: */ michael@0: compile: function Gesture_compile() { michael@0: return compileDetail(this.type, this.points); michael@0: } michael@0: }; michael@0: michael@0: /** michael@0: * A mixin for an explore related object. michael@0: */ michael@0: function ExploreGesture() { michael@0: this.compile = () => { michael@0: // Unlike most of other gestures explore based gestures compile using the michael@0: // current point position and not the start one. michael@0: return compileDetail(this.type, this.points, {x: 'x', y: 'y'}); michael@0: }; michael@0: } michael@0: michael@0: /** michael@0: * Check the in progress gesture for completion. michael@0: */ michael@0: function checkProgressGesture(aGesture) { michael@0: aGesture._inProgress = true; michael@0: if (aGesture.lastEvent === 'pointerup') { michael@0: if (aGesture.test) { michael@0: aGesture.test(true); michael@0: } michael@0: aGesture._deferred.resolve(); michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * A common travel gesture. When the travel gesture is created, all subsequent michael@0: * pointer events' points are tested for their total distance traveled. If that michael@0: * distance exceeds the _threshold distance, the gesture will be rejected to a michael@0: * _travelTo gesture. michael@0: * @param {Number} aTimeStamp An original pointer event's timeStamp that started michael@0: * the gesture resolution sequence. michael@0: * @param {Object} aPoints An existing set of points (from previous events). michael@0: * @param {?String} aLastEvent Last pointer event type. michael@0: * @param {Function} aTravelTo A contructor for the gesture to reject to when michael@0: * travelling (default: Explore). michael@0: * @param {Number} aThreshold Travel threshold (default: michael@0: * GestureSettings.travelThreshold). michael@0: */ michael@0: function TravelGesture(aTimeStamp, aPoints, aLastEvent, aTravelTo = Explore, aThreshold = GestureSettings.travelThreshold) { michael@0: Gesture.call(this, aTimeStamp, aPoints, aLastEvent); michael@0: this._travelTo = aTravelTo; michael@0: this._threshold = aThreshold; michael@0: } michael@0: michael@0: TravelGesture.prototype = Object.create(Gesture.prototype); michael@0: michael@0: /** michael@0: * Test the gesture points for travel. The gesture will be rejected to michael@0: * this._travelTo gesture iff at least one point crosses this._threshold. michael@0: */ michael@0: TravelGesture.prototype.test = function TravelGesture_test() { michael@0: for (let identifier in this.points) { michael@0: let point = this.points[identifier]; michael@0: if (point.totalDistanceTraveled / Utils.dpi > this._threshold) { michael@0: this._deferred.reject(this._travelTo); michael@0: return; michael@0: } michael@0: } michael@0: }; michael@0: michael@0: /** michael@0: * DwellEnd gesture. michael@0: * @param {Number} aTimeStamp An original pointer event's timeStamp that started michael@0: * the gesture resolution sequence. michael@0: * @param {Object} aPoints An existing set of points (from previous events). michael@0: * @param {?String} aLastEvent Last pointer event type. michael@0: */ michael@0: function DwellEnd(aTimeStamp, aPoints, aLastEvent) { michael@0: this._inProgress = true; michael@0: // If the pointer travels, reject to Explore. michael@0: TravelGesture.call(this, aTimeStamp, aPoints, aLastEvent); michael@0: checkProgressGesture(this); michael@0: } michael@0: michael@0: DwellEnd.prototype = Object.create(TravelGesture.prototype); michael@0: DwellEnd.prototype.type = 'dwellend'; michael@0: michael@0: /** michael@0: * TapHoldEnd gesture. This gesture can be represented as the following diagram: michael@0: * pointerdown-pointerup-pointerdown-*wait*-pointerup. michael@0: * @param {Number} aTimeStamp An original pointer event's timeStamp that started michael@0: * the gesture resolution sequence. michael@0: * @param {Object} aPoints An existing set of points (from previous events). michael@0: * @param {?String} aLastEvent Last pointer event type. michael@0: */ michael@0: function TapHoldEnd(aTimeStamp, aPoints, aLastEvent) { michael@0: this._inProgress = true; michael@0: // If the pointer travels, reject to Explore. michael@0: TravelGesture.call(this, aTimeStamp, aPoints, aLastEvent); michael@0: checkProgressGesture(this); michael@0: } michael@0: michael@0: TapHoldEnd.prototype = Object.create(TravelGesture.prototype); michael@0: TapHoldEnd.prototype.type = 'tapholdend'; michael@0: michael@0: /** michael@0: * DoubleTapHoldEnd gesture. This gesture can be represented as the following michael@0: * diagram: michael@0: * pointerdown-pointerup-pointerdown-pointerup-pointerdown-*wait*-pointerup. michael@0: * @param {Number} aTimeStamp An original pointer event's timeStamp that started michael@0: * the gesture resolution sequence. michael@0: * @param {Object} aPoints An existing set of points (from previous events). michael@0: * @param {?String} aLastEvent Last pointer event type. michael@0: */ michael@0: function DoubleTapHoldEnd(aTimeStamp, aPoints, aLastEvent) { michael@0: this._inProgress = true; michael@0: // If the pointer travels, reject to Explore. michael@0: TravelGesture.call(this, aTimeStamp, aPoints, aLastEvent); michael@0: checkProgressGesture(this); michael@0: } michael@0: michael@0: DoubleTapHoldEnd.prototype = Object.create(TravelGesture.prototype); michael@0: DoubleTapHoldEnd.prototype.type = 'doubletapholdend'; michael@0: michael@0: /** michael@0: * A common tap gesture object. michael@0: * @param {Number} aTimeStamp An original pointer event's timeStamp that started michael@0: * the gesture resolution sequence. michael@0: * @param {Object} aPoints An existing set of points (from previous events). michael@0: * @param {?String} aLastEvent Last pointer event type. michael@0: * @param {Function} aRejectTo A constructor for the next gesture to reject to michael@0: * in case no pointermove or pointerup happens within the michael@0: * GestureSettings.dwellThreshold. michael@0: * @param {Function} aTravelTo An optional constuctor for the next gesture to michael@0: * reject to in case the the TravelGesture test fails. michael@0: */ michael@0: function TapGesture(aTimeStamp, aPoints, aLastEvent, aRejectTo, aTravelTo) { michael@0: this._rejectToOnWait = aRejectTo; michael@0: // If the pointer travels, reject to aTravelTo. michael@0: TravelGesture.call(this, aTimeStamp, aPoints, aLastEvent, aTravelTo, michael@0: TAP_MAX_RADIUS); michael@0: } michael@0: michael@0: TapGesture.prototype = Object.create(TravelGesture.prototype); michael@0: TapGesture.prototype._getDelay = function TapGesture__getDelay() { michael@0: // If, for TapGesture, no pointermove or pointerup happens within the michael@0: // GestureSettings.dwellThreshold, reject. michael@0: // Note: the original pointer event's timeStamp is irrelevant here. michael@0: return GestureSettings.dwellThreshold; michael@0: }; michael@0: michael@0: /** michael@0: * Tap gesture. michael@0: * @param {Number} aTimeStamp An original pointer event's timeStamp that started michael@0: * the gesture resolution sequence. michael@0: * @param {Object} aPoints An existing set of points (from previous events). michael@0: * @param {?String} aLastEvent Last pointer event type. michael@0: */ michael@0: function Tap(aTimeStamp, aPoints, aLastEvent) { michael@0: // If the pointer travels, reject to Swipe. michael@0: TapGesture.call(this, aTimeStamp, aPoints, aLastEvent, Dwell, Swipe); michael@0: } michael@0: michael@0: Tap.prototype = Object.create(TapGesture.prototype); michael@0: Tap.prototype.type = 'tap'; michael@0: Tap.prototype.resolveTo = DoubleTap; michael@0: michael@0: /** michael@0: * Tap (multi) gesture on Android. michael@0: * @param {Number} aTimeStamp An original pointer event's timeStamp that started michael@0: * the gesture resolution sequence. michael@0: * @param {Object} aPoints An existing set of points (from previous events). michael@0: * @param {?String} aLastEvent Last pointer event type. michael@0: */ michael@0: function AndroidTap(aTimeStamp, aPoints, aLastEvent) { michael@0: // If the pointer travels, reject to Swipe. On dwell threshold reject to michael@0: // TapHold. michael@0: TapGesture.call(this, aTimeStamp, aPoints, aLastEvent, TapHold, Swipe); michael@0: } michael@0: AndroidTap.prototype = Object.create(TapGesture.prototype); michael@0: // Android double taps are translated to single taps. michael@0: AndroidTap.prototype.type = 'doubletap'; michael@0: AndroidTap.prototype.resolveTo = TripleTap; michael@0: michael@0: /** michael@0: * Clear the pointerup handler timer in case of the 3 pointer swipe. michael@0: */ michael@0: AndroidTap.prototype.clearThreeFingerSwipeTimer = function AndroidTap_clearThreeFingerSwipeTimer() { michael@0: clearTimeout(this._threeFingerSwipeTimer); michael@0: delete this._threeFingerSwipeTimer; michael@0: }; michael@0: michael@0: AndroidTap.prototype.pointerdown = function AndroidTap_pointerdown(aPoints, aTimeStamp) { michael@0: this.clearThreeFingerSwipeTimer(); michael@0: TapGesture.prototype.pointerdown.call(this, aPoints, aTimeStamp); michael@0: }; michael@0: michael@0: AndroidTap.prototype.pointermove = function AndroidTap_pointermove(aPoints) { michael@0: this.clearThreeFingerSwipeTimer(); michael@0: this._moved = true; michael@0: TapGesture.prototype.pointermove.call(this, aPoints); michael@0: }; michael@0: michael@0: AndroidTap.prototype.pointerup = function AndroidTap_pointerup(aPoints) { michael@0: if (this._moved) { michael@0: // If there was a pointer move - handle the real gesture. michael@0: TapGesture.prototype.pointerup.call(this, aPoints); michael@0: } else { michael@0: // Primptively delay the multi pointer gesture resolution, because Android michael@0: // sometimes fires a pointerdown/poitnerup sequence before the real events. michael@0: this._threeFingerSwipeTimer = setTimeout(() => { michael@0: delete this._threeFingerSwipeTimer; michael@0: TapGesture.prototype.pointerup.call(this, aPoints); michael@0: }, ANDROID_TRIPLE_SWIPE_DELAY); michael@0: } michael@0: }; michael@0: michael@0: /** michael@0: * Reject an android tap gesture. michael@0: * @param {?Function} aRejectTo An optional next gesture constructor. michael@0: * @return {Object} structure that looks like { michael@0: * id: gesture_id, // Current AndroidTap gesture id. michael@0: * gestureType: next_gesture // Optional michael@0: * } michael@0: */ michael@0: AndroidTap.prototype._handleReject = function AndroidTap__handleReject(aRejectTo) { michael@0: let keys = Object.keys(this.points); michael@0: if (aRejectTo === Swipe && keys.length === 1) { michael@0: let key = keys[0]; michael@0: let point = this.points[key]; michael@0: // Two finger swipe is translated into single swipe. michael@0: this.points[key + '-copy'] = point; michael@0: } michael@0: return TapGesture.prototype._handleReject.call(this, aRejectTo); michael@0: }; michael@0: michael@0: /** michael@0: * Double Tap gesture. michael@0: * @param {Number} aTimeStamp An original pointer event's timeStamp that started michael@0: * the gesture resolution sequence. michael@0: * @param {Object} aPoints An existing set of points (from previous events). michael@0: * @param {?String} aLastEvent Last pointer event type. michael@0: */ michael@0: function DoubleTap(aTimeStamp, aPoints, aLastEvent) { michael@0: TapGesture.call(this, aTimeStamp, aPoints, aLastEvent, TapHold); michael@0: } michael@0: michael@0: DoubleTap.prototype = Object.create(TapGesture.prototype); michael@0: DoubleTap.prototype.type = 'doubletap'; michael@0: DoubleTap.prototype.resolveTo = TripleTap; michael@0: michael@0: /** michael@0: * Triple Tap gesture. michael@0: * @param {Number} aTimeStamp An original pointer event's timeStamp that started michael@0: * the gesture resolution sequence. michael@0: * @param {Object} aPoints An existing set of points (from previous events). michael@0: * @param {?String} aLastEvent Last pointer event type. michael@0: */ michael@0: function TripleTap(aTimeStamp, aPoints, aLastEvent) { michael@0: TapGesture.call(this, aTimeStamp, aPoints, aLastEvent, DoubleTapHold); michael@0: } michael@0: michael@0: TripleTap.prototype = Object.create(TapGesture.prototype); michael@0: TripleTap.prototype.type = 'tripletap'; michael@0: michael@0: /** michael@0: * Common base object for gestures that are created as resolved. michael@0: * @param {Number} aTimeStamp An original pointer event's timeStamp that started michael@0: * the gesture resolution sequence. michael@0: * @param {Object} aPoints An existing set of points (from previous events). michael@0: * @param {?String} aLastEvent Last pointer event type. michael@0: */ michael@0: function ResolvedGesture(aTimeStamp, aPoints, aLastEvent) { michael@0: Gesture.call(this, aTimeStamp, aPoints, aLastEvent); michael@0: // Resolve the guesture right away. michael@0: this._deferred.resolve(); michael@0: } michael@0: michael@0: ResolvedGesture.prototype = Object.create(Gesture.prototype); michael@0: michael@0: /** michael@0: * Dwell gesture michael@0: * @param {Number} aTimeStamp An original pointer event's timeStamp that started michael@0: * the gesture resolution sequence. michael@0: * @param {Object} aPoints An existing set of points (from previous events). michael@0: * @param {?String} aLastEvent Last pointer event type. michael@0: */ michael@0: function Dwell(aTimeStamp, aPoints, aLastEvent) { michael@0: ResolvedGesture.call(this, aTimeStamp, aPoints, aLastEvent); michael@0: } michael@0: michael@0: Dwell.prototype = Object.create(ResolvedGesture.prototype); michael@0: Dwell.prototype.type = 'dwell'; michael@0: Dwell.prototype.resolveTo = DwellEnd; michael@0: michael@0: /** michael@0: * TapHold gesture michael@0: * @param {Number} aTimeStamp An original pointer event's timeStamp that started michael@0: * the gesture resolution sequence. michael@0: * @param {Object} aPoints An existing set of points (from previous events). michael@0: * @param {?String} aLastEvent Last pointer event type. michael@0: */ michael@0: function TapHold(aTimeStamp, aPoints, aLastEvent) { michael@0: ResolvedGesture.call(this, aTimeStamp, aPoints, aLastEvent); michael@0: } michael@0: michael@0: TapHold.prototype = Object.create(ResolvedGesture.prototype); michael@0: TapHold.prototype.type = 'taphold'; michael@0: TapHold.prototype.resolveTo = TapHoldEnd; michael@0: michael@0: /** michael@0: * DoubleTapHold gesture michael@0: * @param {Number} aTimeStamp An original pointer event's timeStamp that started michael@0: * the gesture resolution sequence. michael@0: * @param {Object} aPoints An existing set of points (from previous events). michael@0: * @param {?String} aLastEvent Last pointer event type. michael@0: */ michael@0: function DoubleTapHold(aTimeStamp, aPoints, aLastEvent) { michael@0: ResolvedGesture.call(this, aTimeStamp, aPoints, aLastEvent); michael@0: } michael@0: michael@0: DoubleTapHold.prototype = Object.create(ResolvedGesture.prototype); michael@0: DoubleTapHold.prototype.type = 'doubletaphold'; michael@0: DoubleTapHold.prototype.resolveTo = DoubleTapHoldEnd; michael@0: michael@0: /** michael@0: * Explore gesture michael@0: * @param {Number} aTimeStamp An original pointer event's timeStamp that started michael@0: * the gesture resolution sequence. michael@0: * @param {Object} aPoints An existing set of points (from previous events). michael@0: * @param {?String} aLastEvent Last pointer event type. michael@0: */ michael@0: function Explore(aTimeStamp, aPoints, aLastEvent) { michael@0: ExploreGesture.call(this); michael@0: ResolvedGesture.call(this, aTimeStamp, aPoints, aLastEvent); michael@0: } michael@0: michael@0: Explore.prototype = Object.create(ResolvedGesture.prototype); michael@0: Explore.prototype.type = 'explore'; michael@0: Explore.prototype.resolveTo = ExploreEnd; michael@0: michael@0: /** michael@0: * ExploreEnd gesture. michael@0: * @param {Number} aTimeStamp An original pointer event's timeStamp that started michael@0: * the gesture resolution sequence. michael@0: * @param {Object} aPoints An existing set of points (from previous events). michael@0: * @param {?String} aLastEvent Last pointer event type. michael@0: */ michael@0: function ExploreEnd(aTimeStamp, aPoints, aLastEvent) { michael@0: this._inProgress = true; michael@0: ExploreGesture.call(this); michael@0: // If the pointer travels, reject to Explore. michael@0: TravelGesture.call(this, aTimeStamp, aPoints, aLastEvent); michael@0: checkProgressGesture(this); michael@0: } michael@0: michael@0: ExploreEnd.prototype = Object.create(TravelGesture.prototype); michael@0: ExploreEnd.prototype.type = 'exploreend'; michael@0: michael@0: /** michael@0: * Swipe gesture. michael@0: * @param {Number} aTimeStamp An original pointer event's timeStamp that started michael@0: * the gesture resolution sequence. michael@0: * @param {Object} aPoints An existing set of points (from previous events). michael@0: * @param {?String} aLastEvent Last pointer event type. michael@0: */ michael@0: function Swipe(aTimeStamp, aPoints, aLastEvent) { michael@0: this._inProgress = true; michael@0: this._rejectToOnWait = Explore; michael@0: Gesture.call(this, aTimeStamp, aPoints, aLastEvent); michael@0: checkProgressGesture(this); michael@0: } michael@0: michael@0: Swipe.prototype = Object.create(Gesture.prototype); michael@0: Swipe.prototype.type = 'swipe'; michael@0: Swipe.prototype._getDelay = function Swipe__getDelay(aTimeStamp) { michael@0: // Swipe should be completed within the GestureSettings.swipeMaxDuration from michael@0: // the initial pointer down event. michael@0: return GestureSettings.swipeMaxDuration - this.startTime + aTimeStamp; michael@0: }; michael@0: michael@0: /** michael@0: * Determine wither the gesture was Swipe or Explore. michael@0: * @param {Booler} aComplete A flag that indicates whether the gesture is and michael@0: * will be complete after the test. michael@0: */ michael@0: Swipe.prototype.test = function Swipe_test(aComplete) { michael@0: if (!aComplete) { michael@0: // No need to test if the gesture is not completing or can't be complete. michael@0: return; michael@0: } michael@0: let reject = true; michael@0: // If at least one point travelled for more than SWIPE_MIN_DISTANCE and it was michael@0: // direct enough, consider it a Swipe. michael@0: for (let identifier in this.points) { michael@0: let point = this.points[identifier]; michael@0: let directDistance = point.directDistanceTraveled; michael@0: if (directDistance / Utils.dpi >= SWIPE_MIN_DISTANCE || michael@0: directDistance * DIRECTNESS_COEFF >= point.totalDistanceTraveled) { michael@0: reject = false; michael@0: } michael@0: } michael@0: if (reject) { michael@0: this._deferred.reject(Explore); michael@0: } michael@0: }; michael@0: michael@0: /** michael@0: * Compile a swipe related mozAccessFuGesture event detail. michael@0: * @return {Object} A mozAccessFuGesture detail object. michael@0: */ michael@0: Swipe.prototype.compile = function Swipe_compile() { michael@0: let type = this.type; michael@0: let detail = compileDetail(type, this.points, michael@0: {x1: 'startX', y1: 'startY', x2: 'x', y2: 'y'}); michael@0: let deltaX = detail.deltaX; michael@0: let deltaY = detail.deltaY; michael@0: if (Math.abs(deltaX) > Math.abs(deltaY)) { michael@0: // Horizontal swipe. michael@0: detail.type = type + (deltaX > 0 ? 'right' : 'left'); michael@0: } else { michael@0: // Vertival swipe. michael@0: detail.type = type + (deltaY > 0 ? 'down' : 'up'); michael@0: } michael@0: return detail; michael@0: };