Bug 981015 - added a number of improvements to make the TouchAdapter(now called PointerAdapter) more reliable. Fixed and updated tests for stability. r=eeejay, marcoz

---
 accessible/src/jsat/AccessFu.jsm                   |   9 +-
 accessible/src/jsat/Gestures.jsm                   | 948 +++++++++++++++++++++
 accessible/src/jsat/PointerAdapter.jsm             | 210 +++++
 accessible/src/jsat/TouchAdapter.jsm               | 431 ----------
 accessible/src/jsat/Utils.jsm                      |  10 +
 accessible/src/jsat/moz.build                      |   3 +-
 accessible/tests/mochitest/jsat/a11y.ini           |   4 +-
 accessible/tests/mochitest/jsat/dom_helper.js      |  74 +-
 accessible/tests/mochitest/jsat/gestures.json      | 253 +++---
 ...ouch_adapter.html => test_gesture_tracker.html} |  42 +-
 .../tests/mochitest/jsat/test_pointer_relay.html   |  95 +++
 11 files changed, 1499 insertions(+), 580 deletions(-)
 create mode 100644 accessible/src/jsat/Gestures.jsm
 create mode 100644 accessible/src/jsat/PointerAdapter.jsm
 delete mode 100644 accessible/src/jsat/TouchAdapter.jsm
 rename accessible/tests/mochitest/jsat/{test_touch_adapter.html => test_gesture_tracker.html} (51%)
 create mode 100644 accessible/tests/mochitest/jsat/test_pointer_relay.html

--HG--
rename : accessible/tests/mochitest/jsat/test_touch_adapter.html => accessible/tests/mochitest/jsat/test_gesture_tracker.html
This commit is contained in:
Yura Zenevich 2014-04-11 10:30:37 -04:00
parent 9790077578
commit 6ad74c4020
11 changed files with 1499 additions and 580 deletions

View File

@ -82,7 +82,7 @@ this.AccessFu = {
this._enabled = true;
Cu.import('resource://gre/modules/accessibility/Utils.jsm');
Cu.import('resource://gre/modules/accessibility/TouchAdapter.jsm');
Cu.import('resource://gre/modules/accessibility/PointerAdapter.jsm');
Cu.import('resource://gre/modules/accessibility/Presentation.jsm');
Logger.info('Enabled');
@ -115,7 +115,7 @@ this.AccessFu = {
this.Input.start();
Output.start();
TouchAdapter.start();
PointerAdapter.start();
Services.obs.addObserver(this, 'remote-browser-shown', false);
Services.obs.addObserver(this, 'inprocess-browser-shown', false);
@ -165,7 +165,7 @@ this.AccessFu = {
this.Input.stop();
Output.stop();
TouchAdapter.stop();
PointerAdapter.stop();
Utils.win.removeEventListener('TabOpen', this);
Utils.win.removeEventListener('TabClose', this);
@ -787,7 +787,8 @@ var Input = {
switch (gestureName) {
case 'dwell1':
case 'explore1':
this.moveToPoint('Simple', aGesture.x, aGesture.y);
this.moveToPoint('Simple', aGesture.touches[0].x,
aGesture.touches[0].y);
break;
case 'doubletap1':
this.activateCurrent();

View File

@ -0,0 +1,948 @@
/* 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 (v)
-> Dwell (x)
-> Swipe (x)
AndroidTap -> TripleTap (v)
-> TapHold (x)
-> Swipe (x)
DoubleTap -> TripleTap (v)
-> 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');
// Maximum amount of time allowed for a gesture to be considered a multitouch.
const MAX_MULTITOUCH = 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';
/**
* 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: 400,
/**
* Maximum consecutive pointer event timeout.
* @type {Number}
*/
maxConsecutiveGestureDelay: 400,
/**
* Delay before tap turns into dwell
* @type {Number}
*/
dwellThreshold: 500,
/**
* 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) {
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.debug('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() {
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) {
this.clearTimer();
let delay = this._getDelay(aTimeStamp);
let 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 < MAX_MULTITOUCH);
},
/**
* 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.debug('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.debug('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} aRejectTo 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.
*/
function TapGesture(aTimeStamp, aPoints, aLastEvent, aRejectTo, aTravelTo) {
this._rejectToOnWait = aRejectTo;
// 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;
};
/**
* 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);
}
Tap.prototype = Object.create(TapGesture.prototype);
Tap.prototype.type = 'tap';
Tap.prototype.resolveTo = DoubleTap;
/**
* 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);
}
AndroidTap.prototype = Object.create(TapGesture.prototype);
// Android double taps are translated to single taps.
AndroidTap.prototype.type = 'doubletap';
AndroidTap.prototype.resolveTo = TripleTap;
/**
* 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);
}
};
/**
* Reject an android tap gesture.
* @param {?Function} aRejectTo An optional next gesture constructor.
* @return {Object} structure that looks like {
* id: gesture_id, // Current AndroidTap gesture id.
* gestureType: next_gesture // Optional
* }
*/
AndroidTap.prototype._handleReject = function AndroidTap__handleReject(aRejectTo) {
let keys = Object.keys(this.points);
if (aRejectTo === Swipe && keys.length === 1) {
let key = keys[0];
let point = this.points[key];
// Two finger swipe is translated into single swipe.
this.points[key + '-copy'] = point;
}
return TapGesture.prototype._handleReject.call(this, aRejectTo);
};
/**
* 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) {
TapGesture.call(this, aTimeStamp, aPoints, aLastEvent, TapHold);
}
DoubleTap.prototype = Object.create(TapGesture.prototype);
DoubleTap.prototype.type = 'doubletap';
DoubleTap.prototype.resolveTo = TripleTap;
/**
* 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) {
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;
if (Math.abs(deltaX) > Math.abs(deltaY)) {
// Horizontal swipe.
detail.type = type + (deltaX > 0 ? 'right' : 'left');
} else {
// Vertival swipe.
detail.type = type + (deltaY > 0 ? 'down' : 'up');
}
return detail;
};

View File

@ -0,0 +1,210 @@
/* 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, XPCOMUtils, Utils, Logger, GestureSettings,
GestureTracker */
/* exported PointerRelay, PointerAdapter */
'use strict';
const Ci = Components.interfaces;
const Cu = Components.utils;
this.EXPORTED_SYMBOLS = ['PointerRelay', 'PointerAdapter']; // 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, 'GestureSettings', // jshint ignore:line
'resource://gre/modules/accessibility/Gestures.jsm');
XPCOMUtils.defineLazyModuleGetter(this, 'GestureTracker', // jshint ignore:line
'resource://gre/modules/accessibility/Gestures.jsm');
// The virtual touch ID generated by a mouse event.
const MOUSE_ID = 'mouse';
// Synthesized touch ID.
const SYNTH_ID = -1;
let PointerRelay = { // jshint ignore:line
/**
* A mapping of events we should be intercepting. Entries with a value of
* |true| are used for compiling high-level gesture events. Entries with a
* value of |false| are cancelled and do not propogate to content.
*/
get _eventsOfInterest() {
delete this._eventsOfInterest;
switch (Utils.widgetToolkit) {
case 'gonk':
this._eventsOfInterest = {
'touchstart' : true,
'touchmove' : true,
'touchend' : true,
'mousedown' : false,
'mousemove' : false,
'mouseup': false,
'click': false };
break;
case 'android':
this._eventsOfInterest = {
'touchstart' : true,
'touchmove' : true,
'touchend' : true,
'mousemove' : true,
'mouseenter' : true,
'mouseleave' : true,
'mousedown' : false,
'mouseup': false,
'click': false };
break;
default:
// Desktop.
this._eventsOfInterest = {
'mousemove' : true,
'mousedown' : true,
'mouseup': true,
'click': false
};
if ('ontouchstart' in Utils.win) {
for (let eventType of ['touchstart', 'touchmove', 'touchend']) {
this._eventsOfInterest[eventType] = true;
}
}
break;
}
return this._eventsOfInterest;
},
_eventMap: {
'touchstart' : 'pointerdown',
'mousedown' : 'pointerdown',
'mouseenter' : 'pointerdown',
'touchmove' : 'pointermove',
'mousemove' : 'pointermove',
'touchend' : 'pointerup',
'mouseup': 'pointerup',
'mouseleave': 'pointerup'
},
start: function PointerRelay_start(aOnPointerEvent) {
Logger.debug('PointerRelay.start');
this.onPointerEvent = aOnPointerEvent;
for (let eventType in this._eventsOfInterest) {
Utils.win.addEventListener(eventType, this, true, true);
}
},
stop: function PointerRelay_stop() {
Logger.debug('PointerRelay.stop');
delete this.lastPointerMove;
delete this.onPointerEvent;
for (let eventType in this._eventsOfInterest) {
Utils.win.removeEventListener(eventType, this, true, true);
}
},
_suppressPointerMove: function PointerRelay__suppressPointerMove(aChangedTouches) {
if (!this.lastPointerMove) {
return false;
}
for (let i = 0; i < aChangedTouches.length; ++i) {
let touch = aChangedTouches[i];
let lastTouch;
try {
lastTouch = this.lastPointerMove.identifiedTouch ?
this.lastPointerMove.identifiedTouch(touch.identifier) :
this.lastPointerMove[i];
} catch (x) {
// Sometimes touch object can't be accessed after page navigation.
}
if (!lastTouch || lastTouch.target !== touch.target ||
Math.hypot(touch.screenX - lastTouch.screenX, touch.screenY -
lastTouch.screenY) / Utils.dpi >= GestureSettings.travelThreshold) {
return false;
}
}
return true;
},
handleEvent: function PointerRelay_handleEvent(aEvent) {
// Don't bother with chrome mouse events.
if (Utils.MozBuildApp === 'browser' &&
aEvent.view.top instanceof Ci.nsIDOMChromeWindow) {
return;
}
if (aEvent.mozInputSource === Ci.nsIDOMMouseEvent.MOZ_SOURCE_UNKNOWN) {
// Ignore events that are scripted or clicks from the a11y API.
return;
}
let changedTouches = aEvent.changedTouches || [{
identifier: MOUSE_ID,
screenX: aEvent.screenX,
screenY: aEvent.screenY,
target: aEvent.target
}];
if (changedTouches.length === 1 &&
changedTouches[0].identifier === SYNTH_ID) {
return;
}
aEvent.preventDefault();
aEvent.stopImmediatePropagation();
let type = aEvent.type;
if (!this._eventsOfInterest[type]) {
return;
}
let pointerType = this._eventMap[type];
if (pointerType === 'pointermove') {
if (this._suppressPointerMove(changedTouches)) {
// Do not fire pointermove more than every POINTERMOVE_THROTTLE.
return;
}
this.lastPointerMove = changedTouches;
}
this.onPointerEvent({
type: pointerType,
points: Array.prototype.map.call(changedTouches,
function mapTouch(aTouch) {
return {
identifier: aTouch.identifier,
x: aTouch.screenX,
y: aTouch.screenY
};
}
)
});
}
};
this.PointerAdapter = { // jshint ignore:line
start: function PointerAdapter_start() {
Logger.debug('PointerAdapter.start');
GestureTracker.reset();
PointerRelay.start(this.handleEvent);
},
stop: function PointerAdapter_stop() {
Logger.debug('PointerAdapter.stop');
PointerRelay.stop();
GestureTracker.reset();
},
handleEvent: function PointerAdapter_handleEvent(aDetail) {
let timeStamp = Date.now();
Logger.debug(() => {
return ['Pointer event', aDetail.type, 'at:', timeStamp,
JSON.stringify(aDetail.points)];
});
GestureTracker.handle(aDetail, timeStamp);
}
};

View File

@ -1,431 +0,0 @@
/* 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/. */
'use strict';
const Cc = Components.classes;
const Ci = Components.interfaces;
const Cu = Components.utils;
const Cr = Components.results;
this.EXPORTED_SYMBOLS = ['TouchAdapter'];
Cu.import('resource://gre/modules/accessibility/Utils.jsm');
// We should not be emitting explore events more than 10 times a second.
// It is granular enough to feel natural, and it does not hammer the CPU.
const EXPLORE_THROTTLE = 100;
this.TouchAdapter = {
// minimal swipe distance in inches
SWIPE_MIN_DISTANCE: 0.4,
// maximum duration of swipe
SWIPE_MAX_DURATION: 400,
// how straight does a swipe need to be
SWIPE_DIRECTNESS: 1.2,
// maximum consecutive
MAX_CONSECUTIVE_GESTURE_DELAY: 400,
// delay before tap turns into dwell
DWELL_THRESHOLD: 500,
// delay before distinct dwell events
DWELL_REPEAT_DELAY: 300,
// maximum distance the mouse could move during a tap in inches
TAP_MAX_RADIUS: 0.2,
// The virtual touch ID generated by a mouse event.
MOUSE_ID: 'mouse',
// Synthesized touch ID.
SYNTH_ID: -1,
start: function TouchAdapter_start() {
Logger.debug('TouchAdapter.start');
this._touchPoints = {};
this._dwellTimeout = 0;
this._prevGestures = {};
this._lastExploreTime = 0;
this._dpi = Utils.win.QueryInterface(Ci.nsIInterfaceRequestor).
getInterface(Ci.nsIDOMWindowUtils).displayDPI;
let target = Utils.win;
for (let eventType in this.eventsOfInterest) {
target.addEventListener(eventType, this, true, true);
}
},
stop: function TouchAdapter_stop() {
Logger.debug('TouchAdapter.stop');
let target = Utils.win;
for (let eventType in this.eventsOfInterest) {
target.removeEventListener(eventType, this, true, true);
}
},
/*
* A mapping of events we should be intercepting. Entries with a value of
* |true| are used for compiling high-level gesture events. Entries with
* a value of |false| are cancelled and do not propogate to content.
*/
get eventsOfInterest() {
delete this.eventsOfInterest;
switch (Utils.widgetToolkit) {
case 'gonk':
this.eventsOfInterest = {
'touchstart' : true,
'touchmove' : true,
'touchend' : true,
'mousedown' : false,
'mousemove' : false,
'mouseup': false,
'click': false };
break;
case 'android':
this.eventsOfInterest = {
'touchstart' : true,
'touchmove' : true,
'touchend' : true,
'mousemove' : true,
'mouseenter' : true,
'mouseleave' : true,
'mousedown' : false,
'mouseup': false,
'click': false };
break;
default:
this.eventsOfInterest = {
'mousemove' : true,
'mousedown' : true,
'mouseup': true,
'click': false };
if ('ontouchstart' in Utils.win) {
for (let eventType of ['touchstart', 'touchmove', 'touchend']) {
this.eventsOfInterest[eventType] = true;
}
}
break;
}
return this.eventsOfInterest;
},
handleEvent: function TouchAdapter_handleEvent(aEvent) {
// Don't bother with chrome mouse events.
if (Utils.MozBuildApp == 'browser' &&
aEvent.view.top instanceof Ci.nsIDOMChromeWindow) {
return;
}
if (aEvent.mozInputSource == Ci.nsIDOMMouseEvent.MOZ_SOURCE_UNKNOWN) {
return;
}
let changedTouches = aEvent.changedTouches || [aEvent];
if (changedTouches.length == 1 &&
changedTouches[0].identifier == this.SYNTH_ID) {
return;
}
if (!this.eventsOfInterest[aEvent.type]) {
aEvent.preventDefault();
aEvent.stopImmediatePropagation();
return;
}
if (this._delayedEvent) {
Utils.win.clearTimeout(this._delayedEvent);
delete this._delayedEvent;
}
// XXX: Until bug 77992 is resolved, on desktop we get microseconds
// instead of milliseconds.
let timeStamp = (Utils.OS == 'Android') ? aEvent.timeStamp : Date.now();
switch (aEvent.type) {
case 'mousedown':
case 'mouseenter':
case 'touchstart':
for (var i = 0; i < changedTouches.length; i++) {
let touch = changedTouches[i];
let touchPoint = new TouchPoint(touch, timeStamp, this._dpi);
let identifier = (touch.identifier == undefined) ?
this.MOUSE_ID : touch.identifier;
this._touchPoints[identifier] = touchPoint;
this._lastExploreTime = timeStamp + this.SWIPE_MAX_DURATION;
}
this._dwellTimeout = Utils.win.setTimeout(
(function () {
this.compileAndEmit(timeStamp + this.DWELL_THRESHOLD);
}).bind(this), this.DWELL_THRESHOLD);
break;
case 'mousemove':
case 'touchmove':
for (var i = 0; i < changedTouches.length; i++) {
let touch = changedTouches[i];
let identifier = (touch.identifier == undefined) ?
this.MOUSE_ID : touch.identifier;
let touchPoint = this._touchPoints[identifier];
if (touchPoint)
touchPoint.update(touch, timeStamp);
}
if (timeStamp - this._lastExploreTime >= EXPLORE_THROTTLE) {
this.compileAndEmit(timeStamp);
this._lastExploreTime = timeStamp;
}
break;
case 'mouseup':
case 'mouseleave':
case 'touchend':
for (var i = 0; i < changedTouches.length; i++) {
let touch = changedTouches[i];
let identifier = (touch.identifier == undefined) ?
this.MOUSE_ID : touch.identifier;
let touchPoint = this._touchPoints[identifier];
if (touchPoint) {
touchPoint.update(touch, timeStamp);
touchPoint.finish();
}
}
this.compileAndEmit(timeStamp);
break;
}
aEvent.preventDefault();
aEvent.stopImmediatePropagation();
},
cleanupTouches: function cleanupTouches() {
for (var identifier in this._touchPoints) {
if (!this._touchPoints[identifier].done)
continue;
delete this._touchPoints[identifier];
}
},
compile: function TouchAdapter_compile(aTime) {
let multiDetails = {};
// Compound multiple simultaneous touch gestures.
for (let identifier in this._touchPoints) {
let touchPoint = this._touchPoints[identifier];
let details = touchPoint.compile(aTime);
if (!details)
continue;
details.touches = [identifier];
let otherTouches = multiDetails[details.type];
if (otherTouches) {
otherTouches.touches.push(identifier);
otherTouches.startTime =
Math.min(otherTouches.startTime, touchPoint.startTime);
} else {
details.startTime = touchPoint.startTime;
details.endTime = aTime;
multiDetails[details.type] = details;
}
}
// Compound multiple consecutive touch gestures.
for each (let details in multiDetails) {
let idhash = details.touches.slice().sort().toString();
let prevGesture = this._prevGestures[idhash];
if (prevGesture) {
// The time delta is calculated as the period between the end of the
// last gesture and the start of this one.
let timeDelta = details.startTime - prevGesture.endTime;
if (timeDelta > this.MAX_CONSECUTIVE_GESTURE_DELAY) {
delete this._prevGestures[idhash];
} else {
let sequence = prevGesture.type + '-' + details.type;
switch (sequence) {
case 'tap-tap':
details.type = 'doubletap';
break;
case 'doubletap-tap':
details.type = 'tripletap';
break;
case 'doubletap-dwell':
details.type = 'doubletaphold';
break;
case 'tap-dwell':
details.type = 'taphold';
break;
case 'explore-explore':
details.deltaX = details.x - prevGesture.x;
details.deltaY = details.y - prevGesture.y;
break;
}
}
}
this._prevGestures[idhash] = details;
}
Utils.win.clearTimeout(this._dwellTimeout);
this.cleanupTouches();
return multiDetails;
},
emitGesture: function TouchAdapter_emitGesture(aDetails) {
let emitDelay = 0;
// Unmutate gestures we are getting from Android when EBT is enabled.
// Two finger gestures are translated to one. Double taps are translated
// to single taps.
if (Utils.MozBuildApp == 'mobile/android' &&
Utils.AndroidSdkVersion >= 14 &&
aDetails.touches[0] != this.MOUSE_ID) {
if (aDetails.touches.length == 1) {
if (aDetails.type == 'tap') {
emitDelay = 50;
aDetails.type = 'doubletap';
} else if (aDetails.type == 'dwell') {
emitDelay = 50;
aDetails.type = 'doubletaphold';
} else {
aDetails.touches.push(this.MOUSE_ID);
}
}
}
let emit = function emit() {
let evt = Utils.win.document.createEvent('CustomEvent');
evt.initCustomEvent('mozAccessFuGesture', true, true, aDetails);
Utils.win.dispatchEvent(evt);
delete this._delayedEvent;
}.bind(this);
if (emitDelay) {
this._delayedEvent = Utils.win.setTimeout(emit, emitDelay);
} else {
emit();
}
},
compileAndEmit: function TouchAdapter_compileAndEmit(aTime) {
for each (let details in this.compile(aTime)) {
this.emitGesture(details);
}
}
};
/***
* A TouchPoint represents a single touch from the moment of contact until it is
* lifted from the surface. It is capable of compiling gestures from the scope
* of one single touch.
*/
function TouchPoint(aTouch, aTime, aDPI) {
this.startX = this.x = aTouch.screenX;
this.startY = this.y = aTouch.screenY;
this.startTime = aTime;
this.distanceTraveled = 0;
this.dpi = aDPI;
this.done = false;
}
TouchPoint.prototype = {
update: function TouchPoint_update(aTouch, aTime) {
let lastX = this.x;
let lastY = this.y;
this.x = aTouch.screenX;
this.y = aTouch.screenY;
this.time = aTime;
this.distanceTraveled += this.getDistanceToCoord(lastX, lastY);
},
getDistanceToCoord: function TouchPoint_getDistanceToCoord(aX, aY) {
return Math.sqrt(Math.pow(this.x - aX, 2) + Math.pow(this.y - aY, 2));
},
finish: function TouchPoint_finish() {
this.done = true;
},
/**
* Compile a gesture from an individual touch point. This is used by the
* TouchAdapter to compound multiple single gestures in to higher level
* gestures.
*/
compile: function TouchPoint_compile(aTime) {
let directDistance = this.directDistanceTraveled;
let duration = aTime - this.startTime;
// To be considered a tap/dwell...
if ((this.distanceTraveled / this.dpi) < TouchAdapter.TAP_MAX_RADIUS) { // Didn't travel
if (duration < TouchAdapter.DWELL_THRESHOLD) {
// Mark it as done so we don't use this touch for another gesture.
this.finish();
return {type: 'tap', x: this.startX, y: this.startY};
} else if (!this.done && duration == TouchAdapter.DWELL_THRESHOLD) {
return {type: 'dwell', x: this.startX, y: this.startY};
} else if (this.done && duration > TouchAdapter.DWELL_THRESHOLD) {
return {type: 'dwellend', x: this.startX, y: this.startY};
}
}
// To be considered a swipe...
if (duration <= TouchAdapter.SWIPE_MAX_DURATION && // Quick enough
(directDistance / this.dpi) >= TouchAdapter.SWIPE_MIN_DISTANCE && // Traveled far
(directDistance * 1.2) >= this.distanceTraveled) { // Direct enough
let swipeGesture = {x1: this.startX, y1: this.startY,
x2: this.x, y2: this.y};
let deltaX = this.x - this.startX;
let deltaY = this.y - this.startY;
if (Math.abs(deltaX) > Math.abs(deltaY)) {
// Horizontal swipe.
if (deltaX > 0)
swipeGesture.type = 'swiperight';
else
swipeGesture.type = 'swipeleft';
} else if (Math.abs(deltaX) < Math.abs(deltaY)) {
// Vertical swipe.
if (deltaY > 0)
swipeGesture.type = 'swipedown';
else
swipeGesture.type = 'swipeup';
} else {
// A perfect 45 degree swipe?? Not in our book.
return null;
}
this.finish();
return swipeGesture;
}
// To be considered an explore...
if (duration > TouchAdapter.SWIPE_MAX_DURATION &&
(this.distanceTraveled / this.dpi) > TouchAdapter.TAP_MAX_RADIUS) {
return {type: this.done ? 'exploreend' : 'explore',
x: this.x, y: this.y};
}
return null;
},
get directDistanceTraveled() {
return this.getDistanceToCoord(this.startX, this.startY);
}
};

View File

@ -247,6 +247,16 @@ this.Utils = {
return new Rect(objX.value, objY.value, objW.value, objH.value);
},
/**
* Get current display DPI.
*/
get dpi() {
delete this.dpi;
this.dpi = this.win.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(
Ci.nsIDOMWindowUtils).displayDPI;
return this.dpi;
},
isInSubtree: function isInSubtree(aAccessible, aSubTreeRoot) {
let acc = aAccessible;
while (acc) {

View File

@ -11,9 +11,10 @@ EXTRA_JS_MODULES += [
'Constants.jsm',
'ContentControl.jsm',
'EventManager.jsm',
'Gestures.jsm',
'OutputGenerator.jsm',
'PointerAdapter.jsm',
'Presentation.jsm',
'TouchAdapter.jsm',
'TraversalRules.jsm',
'Utils.jsm'
]

View File

@ -12,10 +12,10 @@ support-files =
[test_content_integration.html]
[test_content_text.html]
[test_explicit_names.html]
[test_gesture_tracker.html]
[test_landmarks.html]
[test_live_regions.html]
[test_output.html]
[test_tables.html]
[test_touch_adapter.html]
skip-if = true # disabled for a number of intermitten failures: Bug 982326
[test_pointer_relay.html]
[test_traversal.html]

View File

@ -1,18 +1,33 @@
'use strict';
/*global getMainChromeWindow, getBoundsForDOMElm, AccessFuTest, Point*/
/* exported loadJSON*/
/* global getMainChromeWindow, AccessFuTest, GestureSettings, GestureTracker,
SimpleTest, getBoundsForDOMElm, Point, Utils */
/* exported loadJSON, eventMap */
const Ci = Components.interfaces;
const Cu = Components.utils;
Cu.import('resource://gre/modules/accessibility/Utils.jsm');
Cu.import('resource://gre/modules/Geometry.jsm');
Cu.import("resource://gre/modules/accessibility/Gestures.jsm");
var win = getMainChromeWindow(window);
var winUtils = win.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(
Ci.nsIDOMWindowUtils);
/**
* Convert inch based point coordinates into pixels.
* @param {Array} aPoints Array of coordinates in inches.
* @return {Array} Array of coordinates in pixels.
*/
function convertPointCoordinates(aPoints) {
var dpi = Utils.dpi;
return aPoints.map(function convert(aPoint) {
return {
x: aPoint.x * dpi,
y: aPoint.y * dpi,
identifier: aPoint.identifier
};
});
}
/**
* For a given list of points calculate their coordinates in relation to the
@ -28,12 +43,11 @@ var winUtils = win.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(
*/
function calculateTouchListCoordinates(aTouchPoints) {
var coords = [];
var dpi = winUtils.displayDPI;
for (var i = 0, target = aTouchPoints[i]; i < aTouchPoints.length; ++i) {
var bounds = getBoundsForDOMElm(target.base);
var parentBounds = getBoundsForDOMElm('root');
var point = new Point(target.x || 0, target.y || 0);
point.scale(dpi);
point.scale(Utils.dpi);
point.add(bounds[0], bounds[1]);
point.add(bounds[2] / 2, bounds[3] / 2);
point.subtract(parentBounds[0], parentBounds[0]);
@ -86,6 +100,9 @@ var eventMap = {
touchmove: sendTouchEvent
};
var originalDwellThreshold = GestureSettings.dwellThreshold;
var originalSwipeMaxDuration = GestureSettings.swipeMaxDuration;
/**
* Attach a listener for the mozAccessFuGesture event that tests its
* type.
@ -112,18 +129,49 @@ function testMozAccessFuGesture(aExpectedGestures) {
win.addEventListener('mozAccessFuGesture', handleGesture);
}
/**
* Reset the thresholds and max delays that affect gesture rejection.
* @param {Number} aTimeStamp Gesture time stamp.
* @param {Boolean} aRemoveDwellThreshold An optional flag to reset dwell
* threshold.
* @param {Boolean} aRemoveSwipeMaxDuration An optional flag to reset swipe max
* duration.
*/
function setTimers(aTimeStamp, aRemoveDwellThreshold, aRemoveSwipeMaxDuration) {
GestureSettings.dwellThreshold = originalDwellThreshold;
GestureSettings.swipeMaxDuration = originalSwipeMaxDuration;
if (!aRemoveDwellThreshold && !aRemoveSwipeMaxDuration) {
return;
}
if (aRemoveDwellThreshold) {
GestureSettings.dwellThreshold = 0;
}
if (aRemoveSwipeMaxDuration) {
GestureSettings.swipeMaxDuration = 0;
}
GestureTracker.current.clearTimer();
GestureTracker.current.startTimer(aTimeStamp);
}
/**
* An extention to AccessFuTest that adds an ability to test a sequence of
* touch/mouse/etc events and their expected mozAccessFuGesture events.
* @param {Object} aSequence An object that has a list of touch/mouse/etc events
* to be generated and the expected mozAccessFuGesture events.
* pointer events and their expected mozAccessFuGesture events.
* @param {Object} aSequence An object that has a list of pointer events to be
* generated and the expected mozAccessFuGesture events.
*/
AccessFuTest.addSequence = function AccessFuTest_addSequence(aSequence) {
AccessFuTest.addFunc(function testSequence() {
testMozAccessFuGesture(aSequence.expectedGestures);
var events = aSequence.events;
function fireEvent(aEvent) {
eventMap[aEvent.type](aEvent.target, aEvent.type);
var event = {
points: convertPointCoordinates(aEvent.points),
type: aEvent.type
};
var timeStamp = Date.now();
GestureTracker.handle(event, timeStamp);
setTimers(timeStamp, aEvent.removeDwellThreshold,
aEvent.removeSwipeMaxDuration);
processEvents();
}
function processEvents() {
@ -131,11 +179,9 @@ AccessFuTest.addSequence = function AccessFuTest_addSequence(aSequence) {
return;
}
var event = events.shift();
if (event.delay) {
window.setTimeout(fireEvent, event.delay, event);
} else {
SimpleTest.executeSoon(function() {
fireEvent(event);
}
});
}
processEvents();
});

View File

@ -1,193 +1,248 @@
[
{
"events": [
{"type": "touchstart", "target": [{"base": "button", "x": 0, "y": 0}]},
{"type": "touchend", "delay": 50}
{"type": "pointerdown", "points": [{"x": 1, "y": 1, "identifier": 1}]},
{"type": "pointerup", "points": [{"x": 1, "y": 1, "identifier": 1}]}
],
"expectedGestures": "tap"
},
{
"events": [
{"type": "touchstart", "target": [{"base": "button", "x": 0, "y": 0}]},
{"type": "touchmove",
"target": [{"base": "button", "x": 0.03, "y": 0.02}], "delay": 25},
{"type": "touchend", "delay": 25}
{"type": "pointerdown", "points": [{"x": 1, "y": 1, "identifier": 1}]},
{"type": "pointermove",
"points": [{"x": 1.03, "y": 1.03, "identifier": 1}]},
{"type": "pointerup", "points": [{"x": 1.03, "y": 1.03, "identifier": 1}]}
],
"expectedGestures": "tap"
},
{
"events": [
{"type": "touchstart", "target": [{"base": "button", "x": 0, "y": 0}]},
{"type": "touchend", "delay": 525}
{"type": "pointerdown", "points": [{"x": 1, "y": 1, "identifier": 1}],
"removeDwellThreshold": true},
{"type": "pointerup", "points": [{"x": 1, "y": 1, "identifier": 1}]}
],
"expectedGestures": ["dwell", "dwellend"]
},
{
"events": [
{"type": "touchstart", "target": [{"base": "button", "x": 0, "y": 0}]},
{"type": "touchmove",
"target": [{"base": "button", "x": 0.03, "y": 0.02}], "delay": 25},
{"type": "touchend", "delay": 25},
{"type": "touchstart", "target": [{"base": "button", "x": 0, "y": 0}],
"delay": 25},
{"type": "touchmove",
"target": [{"base": "button", "x": -0.03, "y": 0.01}], "delay": 25},
{"type": "touchend", "delay": 25}
{"type": "pointerdown", "points": [{"x": 1, "y": 1, "identifier": 1}]},
{"type": "pointermove",
"points": [{"x": 1.03, "y": 1.02, "identifier": 1}]},
{"type": "pointerup",
"points": [{"x": 1.03, "y": 1.02, "identifier": 1}]},
{"type": "pointerdown", "points": [{"x": 1, "y": 1, "identifier": 1}]},
{"type": "pointermove",
"points": [{"x": 0.97, "y": 1.01, "identifier": 1}]},
{"type": "pointerup",
"points": [{"x": 0.97, "y": 1.01, "identifier": 1}]}
],
"expectedGestures": ["tap", "doubletap"]
},
{
"events": [
{"type": "touchstart", "target": [{"base": "button", "x": 0, "y": 0}]},
{"type": "touchend", "delay": 25},
{"type": "touchstart", "target": [{"base": "button", "x": 0, "y": 0}],
"delay": 25},
{"type": "touchend", "delay": 25},
{"type": "touchstart", "target": [{"base": "button", "x": 0, "y": 0}],
"delay": 25},
{"type": "touchend", "delay": 25}
{"type": "pointerdown", "points": [{"x": 1, "y": 1, "identifier": 1}]},
{"type": "pointerup", "points": [{"x": 1, "y": 1, "identifier": 1}]},
{"type": "pointerdown", "points": [{"x": 1, "y": 1, "identifier": 1}]},
{"type": "pointerup", "points": [{"x": 1, "y": 1, "identifier": 1}]},
{"type": "pointerdown", "points": [{"x": 1, "y": 1, "identifier": 1}]},
{"type": "pointerup", "points": [{"x": 1, "y": 1, "identifier": 1}]}
],
"expectedGestures": ["tap", "doubletap", "tripletap"]
},
{
"events": [
{"type": "touchstart", "target": [{"base": "button", "x": 0, "y": 0}]},
{"type": "touchend", "delay": 25},
{"type": "touchstart", "target": [{"base": "button", "x": 0, "y": 0}],
"delay": 25},
{"type": "touchend", "delay": 25},
{"type": "touchstart", "target": [{"base": "button", "x": 0, "y": 0}],
"delay": 25},
{"type": "touchend", "delay": 525}
{"type": "pointerdown", "points": [{"x": 1, "y": 1, "identifier": 1}]},
{"type": "pointerup", "points": [{"x": 1, "y": 1, "identifier": 1}]},
{"type": "pointerdown", "points": [{"x": 1, "y": 1, "identifier": 1}]},
{"type": "pointerup", "points": [{"x": 1, "y": 1, "identifier": 1}]},
{"type": "pointerdown", "points": [{"x": 1, "y": 1, "identifier": 1}],
"removeDwellThreshold": true},
{"type": "pointerup", "points": [{"x": 1, "y": 1, "identifier": 1}]}
],
"expectedGestures": ["tap", "doubletap", "doubletaphold", "dwellend"]
"expectedGestures": ["tap", "doubletap", "doubletaphold",
"doubletapholdend"]
},
{
"events": [
{"type": "touchstart", "target": [{"base": "button", "x": 0, "y": 0}]},
{"type": "touchend", "delay": 25},
{"type": "touchstart", "target": [{"base": "button", "x": 0, "y": 0}],
"delay": 25},
{"type": "touchend", "delay": 525}
{"type": "pointerdown", "points": [{"x": 1, "y": 1, "identifier": 1}]},
{"type": "pointerup", "points": [{"x": 1, "y": 1, "identifier": 1}]},
{"type": "pointerdown", "points": [{"x": 1, "y": 1, "identifier": 1}],
"removeDwellThreshold": true},
{"type": "pointerup", "points": [{"x": 1, "y": 1, "identifier": 1}]}
],
"expectedGestures": ["tap", "taphold", "dwellend"]
"expectedGestures": ["tap", "taphold", "tapholdend"]
},
{
"events": [
{"type": "touchstart", "target": [{"base": "button", "x": 0, "y": 0}]},
{"type": "touchmove", "target": [{"base": "button", "x": 0.5, "y": 0}],
"delay": 25},
{"type": "touchend", "delay": 25}
{"type": "pointerdown", "points": [{"x": 1, "y": 1, "identifier": 1}]},
{"type": "pointermove", "points": [{"x": 1.5, "y": 1, "identifier": 1}]},
{"type": "pointerup", "points": [{"x": 1.5, "y": 1, "identifier": 1}]}
],
"expectedGestures": "swiperight"
},
{
"events": [
{"type": "touchstart", "target": [{"base": "button", "x": 0.5, "y": 0}]},
{"type": "touchmove", "target": [{"base": "button", "x": 0, "y": 0}],
"delay": 25},
{"type": "touchend", "delay": 25}
{"type": "pointerdown", "points": [{"x": 1, "y": 1, "identifier": 1}]},
{"type": "pointermove", "points": [{"x": 1.15, "y": 1, "identifier": 1}]},
{"type": "pointermove", "points": [{"x": 1.3, "y": 1, "identifier": 1}]},
{"type": "pointerup", "points": [{"x": 1.3, "y": 1, "identifier": 1}]}
],
"expectedGestures": "swiperight"
},
{
"events": [
{"type": "pointerdown", "points": [{"x": 1.5, "y": 1, "identifier": 1}]},
{"type": "pointermove", "points": [{"x": 1, "y": 1, "identifier": 1}]},
{"type": "pointerup", "points": [{"x": 1, "y": 1, "identifier": 1}]}
],
"expectedGestures": "swipeleft"
},
{
"events": [
{"type": "touchstart", "target": [{"base": "button", "x": 0, "y": 0}]},
{"type": "touchmove", "target": [{"base": "button", "x": 0, "y": 0.5}],
"delay": 25},
{"type": "touchend", "delay": 25}
{"type": "pointerdown", "points": [{"x": 1, "y": 1, "identifier": 1}]},
{"type": "pointermove", "points": [{"x": 1, "y": 1.5, "identifier": 1}]},
{"type": "pointerup", "points": [{"x": 1, "y": 1.5, "identifier": 1}]}
],
"expectedGestures": "swipedown"
},
{
"events": [
{"type": "touchstart", "target": [{"base": "button", "x": 0, "y": 0.5}]},
{"type": "touchmove", "target": [{"base": "button", "x": 0, "y": 0}],
"delay": 25},
{"type": "touchend", "delay": 25}
{"type": "pointerdown", "points": [{"x": 1, "y": 1.5, "identifier": 1}]},
{"type": "pointermove", "points": [{"x": 1, "y": 1, "identifier": 1}]},
{"type": "pointerup", "points": [{"x": 1, "y": 1, "identifier": 1}]}
],
"expectedGestures": "swipeup"
},
{
"events": [
{"type": "touchstart", "target": [{"base": "button", "x": 0, "y": 0}]},
{"type": "touchmove", "target": [{"base": "button", "x": 0.5, "y": 0.1}],
"delay": 25},
{"type": "touchend", "delay": 25}
{"type": "pointerdown", "points": [{"x": 1, "y": 1, "identifier": 1}]},
{"type": "pointermove",
"points": [{"x": 1.5, "y": 1.1, "identifier": 1}]},
{"type": "pointerup", "points": [{"x": 1.5, "y": 1.1, "identifier": 1}]}
],
"expectedGestures": "swiperight"
},
{
"events": [
{"type": "touchstart",
"target": [{"base": "button", "x": 0.5, "y": 0.1}]},
{"type": "touchmove", "target": [{"base": "button", "x": 0, "y": -0.05}],
"delay": 25},
{"type": "touchend", "delay": 25}
{"type": "pointerdown",
"points": [{"x": 1.5, "y": 1.1, "identifier": 1}]},
{"type": "pointermove", "points": [{"x": 1, "y": 0.95, "identifier": 1}]},
{"type": "pointerup", "points": [{"x": 1, "y": 0.95, "identifier": 1}]}
],
"expectedGestures": "swipeleft"
},
{
"events": [
{"type": "touchstart", "target": [{"base": "button", "x": 0, "y": 0}]},
{"type": "touchmove",
"target": [{"base": "button", "x": -0.1, "y": 0.5}], "delay": 25},
{"type": "touchend", "delay": 25}
{"type": "pointerdown", "points": [{"x": 1, "y": 1, "identifier": 1}]},
{"type": "pointermove",
"points": [{"x": 0.9, "y": 1.5, "identifier": 1}]},
{"type": "pointerup", "points": [{"x": 0.9, "y": 1.5, "identifier": 1}]}
],
"expectedGestures": "swipedown"
},
{
"events": [
{"type": "touchstart",
"target": [{"base": "button", "x": 0.1, "y": 0.5}]},
{"type": "touchmove", "target": [{"base": "button", "x": 0, "y": 0}],
"delay": 25},
{"type": "touchend", "delay": 25}
{"type": "pointerdown",
"points": [{"x": 1.1, "y": 1.5, "identifier": 1}]},
{"type": "pointermove", "points": [{"x": 1, "y": 1, "identifier": 1}]},
{"type": "pointerup", "points": [{"x": 1, "y": 1, "identifier": 1}]}
],
"expectedGestures": "swipeup"
},
{
"events": [
{"type": "touchstart", "target": [{"base": "button", "x": 0, "y": 0},
{"base": "button", "x": 0, "y": 0.5}]},
{"type": "touchmove", "target": [{"base": "button", "x": 0.5, "y": 0},
{"base": "button", "x": 0.5, "y": 0.5}], "delay": 25},
{"type": "touchend", "delay": 25}
{"type": "pointerdown", "points": [{"x": 1, "y": 1, "identifier": 1},
{"x": 1, "y": 1.5, "identifier": 2}]},
{"type": "pointermove", "points": [{"x": 1.5, "y": 1, "identifier": 1},
{"x": 1.5, "y": 1.5, "identifier": 2}]},
{"type": "pointerup", "points": [{"x": 1.5, "y": 1, "identifier": 1},
{"x": 1.5, "y": 1.5, "identifier": 2}]}
],
"expectedGestures": "swiperight"
},
{
"events": [
{"type": "touchstart", "target": [{"base": "button", "x": 0, "y": 0},
{"base": "button", "x": 0, "y": 0.5},
{"base": "button", "x": 0, "y": 1}]},
{"type": "touchmove", "target": [{"base": "button", "x": 0.5, "y": 0},
{"base": "button", "x": 0.5, "y": 0.5},
{"base": "button", "x": 0.5, "y": 1}],
"delay": 25},
{"type": "touchend", "delay": 25}
{"type": "pointerdown", "points": [{"x": 1, "y": 1, "identifier": 1},
{"x": 1, "y": 1.5, "identifier": 2}]},
{"type": "pointermove", "points": [{"x": 1.5, "y": 1, "identifier": 1},
{"x": 1.5, "y": 1.5, "identifier": 2}]},
{"type": "pointerup", "points": [{"x": 1.5, "y": 1, "identifier": 1}]},
{"type": "pointerup", "points": [{"x": 1.5, "y": 1.5, "identifier": 2}]}
],
"expectedGestures": "swiperight"
},
{
"events": [
{"type": "touchstart",
"target": [{"base": "button", "x": 0.6, "y": 0.5}]},
{"type": "touchmove", "target": [{"base": "button", "x": 0, "y": 0}],
"delay": 525},
{"type": "touchend", "delay": 25}
{"type": "pointerdown", "points": [{"x": 1, "y": 1, "identifier": 1},
{"x": 1, "y": 1.5, "identifier": 2},
{"x": 1, "y": 2, "identifier": 3}]},
{"type": "pointermove", "points": [{"x": 1.5, "y": 1, "identifier": 1},
{"x": 1.5, "y": 1.5, "identifier": 2},
{"x": 1.5, "y": 2, "identifier": 3}]},
{"type": "pointerup", "points": [{"x": 1.5, "y": 1, "identifier": 1},
{"x": 1.5, "y": 1.5, "identifier": 2},
{"x": 1.5, "y": 2, "identifier": 3}]}
],
"expectedGestures": "swiperight"
},
{
"events": [
{"type": "pointerdown",
"points": [{"x": 1.6, "y": 1.5, "identifier": 1}],
"removeDwellThreshold": true},
{"type": "pointermove", "points": [{"x": 1, "y": 1, "identifier": 1}]},
{"type": "pointerup", "points": [{"x": 1, "y": 1, "identifier": 1}]}
],
"expectedGestures": ["dwell", "explore", "exploreend"]
},
{
"events": [
{"type": "touchstart",
"target": [{"base": "button", "x": 0, "y": 0.5}]},
{"type": "touchmove", "target": [{"base": "button", "x": 0, "y": 0}],
"delay": 525},
{"type": "touchmove", "target": [{"base": "button", "x": 0.5, "y": 0}],
"delay": 125},
{"type": "touchend", "delay": 25}
{"type": "pointerdown",
"points": [{"x": 1.6, "y": 1.5, "identifier": 1}],
"removeDwellThreshold": true},
{"type": "pointermove", "points": [{"x": 1, "y": 1, "identifier": 1}]},
{"type": "pointermove", "points": [{"x": 2, "y": 2, "identifier": 1}]},
{"type": "pointerup", "points": [{"x": 2, "y": 2, "identifier": 1}]}
],
"expectedGestures": ["dwell", "explore", "explore", "exploreend"]
},
{
"events": [
{"type": "pointerdown",
"points": [{"x": 1.6, "y": 1.5, "identifier": 1}]},
{"type": "pointermove", "points": [{"x": 1, "y": 1, "identifier": 1}],
"removeSwipeMaxDuration": true},
{"type": "pointerup", "points": [{"x": 1, "y": 1, "identifier": 1}]}
],
"expectedGestures": ["explore", "exploreend"]
},
{
"events": [
{"type": "pointerdown", "points": [{"x": 1, "y": 1.5, "identifier": 1}]},
{"type": "pointermove", "points": [{"x": 1, "y": 1, "identifier": 1}],
"removeSwipeMaxDuration": true},
{"type": "pointermove", "points": [{"x": 1.5, "y": 1, "identifier": 1}]},
{"type": "pointerup", "points": [{"x": 1.5, "y": 1, "identifier": 1}]}
],
"expectedGestures": ["explore", "explore", "exploreend"]
},
{
"events": [
{"type": "pointerdown", "points": [{"x": 1, "y": 1, "identifier": 1}],
"removeDwellThreshold": true},
{"type": "pointermove",
"points": [{"x": 1.5, "y": 1.5, "identifier": 1}]},
{"type": "pointermove",
"points": [{"x": 1.55, "y": 1.5, "identifier": 1}]},
{"type": "pointermove",
"points": [{"x": 1.6, "y": 1.5, "identifier": 1}]},
{"type": "pointermove",
"points": [{"x": 1.65, "y": 1.5, "identifier": 1}]},
{"type": "pointermove",
"points": [{"x": 1.7, "y": 1.5, "identifier": 1}]},
{"type": "pointermove",
"points": [{"x": 1.75, "y": 1.5, "identifier": 1}]},
{"type": "pointerup", "points": [{"x": 1.75, "y": 1.5, "identifier": 1}]}
],
"expectedGestures": ["dwell", "explore", "explore", "exploreend"]
}

View File

@ -1,7 +1,7 @@
<html>
<head>
<title>AccessFu tests for touch adapters.</title>
<title>AccessFu tests for gesture tracker.</title>
<link rel="stylesheet" type="text/css"
href="chrome://mochikit/content/tests/SimpleTest/test.css" />
@ -13,36 +13,23 @@
<script type="application/javascript" src="./dom_helper.js"></script>
<script type="application/javascript">
Components.utils.import(
"resource://gre/modules/accessibility/TouchAdapter.jsm");
function startComponents() {
TouchAdapter.start();
function startGestureTracker() {
GestureTracker.reset();
AccessFuTest.nextTest();
}
/**
* Clean up all state data related to previous events/gestures.
*/
function cleanupTouchAdapter() {
TouchAdapter.cleanupTouches();
TouchAdapter._touchPoints = {};
TouchAdapter._dwellTimeout = 0;
TouchAdapter._prevGestures = {};
TouchAdapter._lastExploreTime = 0;
}
function stopComponents() {
TouchAdapter.stop();
function stopGestureTracker() {
GestureTracker.reset();
AccessFuTest.finish();
}
function doTest() {
loadJSON("./gestures.json", function onSuccess(gestures) {
AccessFuTest.addFunc(startComponents);
AccessFuTest.sequenceCleanup = cleanupTouchAdapter;
AccessFuTest.addFunc(startGestureTracker);
AccessFuTest.sequenceCleanup = GestureTracker.reset.bind(
GestureTracker);
gestures.forEach(AccessFuTest.addSequence);
AccessFuTest.addFunc(stopComponents);
AccessFuTest.addFunc(stopGestureTracker);
AccessFuTest.waitForExplicitFinish();
AccessFuTest.runTests();
});
@ -53,14 +40,11 @@
</script>
</head>
<body id="root">
<body>
<a target="_blank"
href="https://bugzilla.mozilla.org/show_bug.cgi?id=976082"
title="[AccessFu] Provide tests for touch adapter.">
Mozilla Bug 976082
href="https://bugzilla.mozilla.org/show_bug.cgi?id=981015"
title="AccessFu tests for gesture tracker.">
Mozilla Bug 981015
</a>
<div>
<button id="button">I am a button</button>
</div>
</body>
</html>

View File

@ -0,0 +1,95 @@
<html>
<head>
<title>AccessFu tests for pointer relay.</title>
<link rel="stylesheet" type="text/css"
href="chrome://mochikit/content/tests/SimpleTest/test.css" />
<script type="application/javascript"
src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
<script type="application/javascript" src="../common.js"></script>
<script type="application/javascript" src="../layout.js"></script>
<script type="application/javascript" src="./jsatcommon.js"></script>
<script type="application/javascript" src="./dom_helper.js"></script>
<script type="application/javascript">
Components.utils.import(
"resource://gre/modules/accessibility/PointerAdapter.jsm");
var tests = [
{
type: 'touchstart', target: [{base: 'button'}],
expected: {type: 'pointerdown', length: 1}
},
{
type: 'touchmove', target: [{base: 'button'}],
expected: {type: 'pointermove', length: 1}
},
{
type: 'touchend', target: [{base: 'button'}],
expected: {type: 'pointerup'}
},
{
type: 'touchstart', target: [{base: 'button'},
{base: 'button', x: 0.5, y: 0.3}],
expected: {type: 'pointerdown', length: 2}
},
{
type: 'touchend', target: [{base: 'button'},
{base: 'button', x: 0.5, y: 0.3}],
expected: {type: 'pointerup'}
},
{
type: 'touchstart', target: [{base: 'button'},
{base: 'button', x: 0.5, y: 0.3},
{base: 'button', x: 0.5, y: -0.3}],
expected: {type: 'pointerdown', length: 3}
},
{
type: 'touchend', target: [{base: 'button'},
{base: 'button', x: 0.5, y: 0.3},
{base: 'button', x: 0.5, y: -0.3}],
expected: {type: 'pointerup'}
}
];
function makeTestFromSpec(test) {
return function runTest() {
PointerRelay.start(function onPointerEvent(aDetail) {
is(aDetail.type, test.expected.type,
'mozAccessFuPointerEvent is correct.');
if (test.expected.length) {
is(aDetail.points.length, test.expected.length,
'mozAccessFuPointerEvent points length is correct.');
}
PointerRelay.stop();
AccessFuTest.nextTest();
});
eventMap[test.type](test.target, test.type);
};
}
function doTest() {
tests.forEach(function addTest(test) {
AccessFuTest.addFunc(makeTestFromSpec(test));
});
AccessFuTest.waitForExplicitFinish();
AccessFuTest.runTests();
}
SimpleTest.waitForExplicitFinish();
addA11yLoadEvent(doTest);
</script>
</head>
<body id="root">
<a target="_blank"
href="https://bugzilla.mozilla.org/show_bug.cgi?id=976082"
title="[AccessFu] Provide tests for pointer relay.">
Mozilla Bug 981015
</a>
<div>
<button id="button">I am a button</button>
</div>
</body>
</html>