/* 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) AndroidTap -> TripleTap (x) -> TapHold (x) -> Swipe (x) DoubleTap -> TripleTap (x) -> TapHold (x) -> Explore (x) TripleTap -> DoubleTapHold (x) -> Explore (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 Ci = Components.interfaces; 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; // An android flag. const IS_ANDROID = Utils.MozBuildApp === 'mobile/android' && Utils.AndroidSdkVersion >= 14; // A single pointer down/up sequence periodically precedes the tripple swipe // gesture on Android. This delay acounts for that. const ANDROID_TRIPLE_SWIPE_DELAY = 50; // The virtual touch ID generated by a mouse event. const MOUSE_ID = 'mouse'; // 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 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, /** * 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 = Tap) { // Only create a new gesture on |pointerdown| event. if (aDetail.type !== 'pointerdown') { return; } let points = aDetail.points; let GestureConstructor = aGesture; if (IS_ANDROID && GestureConstructor === Tap && points.length === 1 && points[0].identifier !== MOUSE_ID) { // Handle Android events when EBT is enabled. Two finger gestures are // translated to one. GestureConstructor = AndroidTap; } 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', aDetail.type, 'at:', aTimeStamp, JSON.stringify(aDetail.points)]; }); 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 { 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'); delete this._timer; 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) { 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; 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; 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() { 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} aRejectToOnPointerDown A constructor for the gesture to * reject to if a finger comes down immediately after the tap. * @param {Function} aTravelTo An optional constuctor for the next gesture to * reject to in case the the TravelGesture test fails. */ 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.maxConsecutiveGestureDelay) { this._pointerUpTimer = setTimeout(() => { delete this._pointerUpTimer; this._deferred.resolve(); }, GestureSettings.maxConsecutiveGestureDelay); } else { this._deferred.resolve(); } } } else { TravelGesture.prototype.pointerup.call(this, aPoints); } }; TapGesture.prototype.pointerdown = function TapGesture_pointerdown(aPoints, aTimeStamp) { TravelGesture.prototype.pointerdown.call(this, aPoints, aTimeStamp); if (this._pointerUpTimer) { clearTimeout(this._pointerUpTimer); delete this._pointerUpTimer; this._deferred.reject(this._rejectToOnPointerDown); } }; /** * 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'; /** * Tap (multi) gesture on Android. * @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 AndroidTap(aTimeStamp, aPoints, aLastEvent) { // If the pointer travels, reject to Swipe. On dwell threshold reject to // TapHold. TapGesture.call(this, aTimeStamp, aPoints, aLastEvent, TapHold, Swipe, TripleTap); } AndroidTap.prototype = Object.create(TapGesture.prototype); // Android double taps are translated to single taps. AndroidTap.prototype.type = 'doubletap'; /** * Clear the pointerup handler timer in case of the 3 pointer swipe. */ AndroidTap.prototype.clearThreeFingerSwipeTimer = function AndroidTap_clearThreeFingerSwipeTimer() { clearTimeout(this._threeFingerSwipeTimer); delete this._threeFingerSwipeTimer; }; AndroidTap.prototype.pointerdown = function AndroidTap_pointerdown(aPoints, aTimeStamp) { this.clearThreeFingerSwipeTimer(); TapGesture.prototype.pointerdown.call(this, aPoints, aTimeStamp); }; AndroidTap.prototype.pointermove = function AndroidTap_pointermove(aPoints) { this.clearThreeFingerSwipeTimer(); this._moved = true; TapGesture.prototype.pointermove.call(this, aPoints); }; AndroidTap.prototype.pointerup = function AndroidTap_pointerup(aPoints) { if (this._moved) { // If there was a pointer move - handle the real gesture. TapGesture.prototype.pointerup.call(this, aPoints); } else { // Primptively delay the multi pointer gesture resolution, because Android // sometimes fires a pointerdown/poitnerup sequence before the real events. this._threeFingerSwipeTimer = setTimeout(() => { delete this._threeFingerSwipeTimer; TapGesture.prototype.pointerup.call(this, aPoints); }, ANDROID_TRIPLE_SWIPE_DELAY); } }; /** * 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); } 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 = [touch.x1 for (touch of detail.touches)]; 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 = [touch.y1 for (touch of detail.touches)]; 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; };