gecko/accessible/jsat/Gestures.jsm

1003 lines
33 KiB
JavaScript

/* 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;
};