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