accessible/src/jsat/Gestures.jsm

Wed, 31 Dec 2014 07:16:47 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 07:16:47 +0100
branch
TOR_BUG_9701
changeset 3
141e0f1194b1
permissions
-rw-r--r--

Revert simplistic fix pending revisit of Mozilla integration attempt.

michael@0 1 /* This Source Code Form is subject to the terms of the Mozilla Public
michael@0 2 * License, v. 2.0. If a copy of the MPL was not distributed with this file,
michael@0 3 * You can obtain one at http://mozilla.org/MPL/2.0/. */
michael@0 4
michael@0 5 /* global Components, GestureSettings, XPCOMUtils, Utils, Promise, Logger */
michael@0 6 /* exported GestureSettings, GestureTracker */
michael@0 7
michael@0 8 /******************************************************************************
michael@0 9 All gestures have the following pathways when being resolved(v)/rejected(x):
michael@0 10 Tap -> DoubleTap (v)
michael@0 11 -> Dwell (x)
michael@0 12 -> Swipe (x)
michael@0 13
michael@0 14 AndroidTap -> TripleTap (v)
michael@0 15 -> TapHold (x)
michael@0 16 -> Swipe (x)
michael@0 17
michael@0 18 DoubleTap -> TripleTap (v)
michael@0 19 -> TapHold (x)
michael@0 20 -> Explore (x)
michael@0 21
michael@0 22 TripleTap -> DoubleTapHold (x)
michael@0 23 -> Explore (x)
michael@0 24
michael@0 25 Dwell -> DwellEnd (v)
michael@0 26
michael@0 27 Swipe -> Explore (x)
michael@0 28
michael@0 29 TapHold -> TapHoldEnd (v)
michael@0 30
michael@0 31 DoubleTapHold -> DoubleTapHoldEnd (v)
michael@0 32
michael@0 33 DwellEnd -> Explore (x)
michael@0 34
michael@0 35 TapHoldEnd -> Explore (x)
michael@0 36
michael@0 37 DoubleTapHoldEnd -> Explore (x)
michael@0 38
michael@0 39 ExploreEnd -> Explore (x)
michael@0 40
michael@0 41 Explore -> ExploreEnd (v)
michael@0 42 ******************************************************************************/
michael@0 43
michael@0 44 'use strict';
michael@0 45
michael@0 46 const Ci = Components.interfaces;
michael@0 47 const Cu = Components.utils;
michael@0 48
michael@0 49 this.EXPORTED_SYMBOLS = ['GestureSettings', 'GestureTracker']; // jshint ignore:line
michael@0 50
michael@0 51 Cu.import('resource://gre/modules/XPCOMUtils.jsm');
michael@0 52
michael@0 53 XPCOMUtils.defineLazyModuleGetter(this, 'Utils', // jshint ignore:line
michael@0 54 'resource://gre/modules/accessibility/Utils.jsm');
michael@0 55 XPCOMUtils.defineLazyModuleGetter(this, 'Logger', // jshint ignore:line
michael@0 56 'resource://gre/modules/accessibility/Utils.jsm');
michael@0 57 XPCOMUtils.defineLazyModuleGetter(this, 'setTimeout', // jshint ignore:line
michael@0 58 'resource://gre/modules/Timer.jsm');
michael@0 59 XPCOMUtils.defineLazyModuleGetter(this, 'clearTimeout', // jshint ignore:line
michael@0 60 'resource://gre/modules/Timer.jsm');
michael@0 61 XPCOMUtils.defineLazyModuleGetter(this, 'Promise', // jshint ignore:line
michael@0 62 'resource://gre/modules/Promise.jsm');
michael@0 63
michael@0 64 // Maximum amount of time allowed for a gesture to be considered a multitouch.
michael@0 65 const MAX_MULTITOUCH = 250;
michael@0 66 // Minimal swipe distance in inches
michael@0 67 const SWIPE_MIN_DISTANCE = 0.4;
michael@0 68 // Maximum distance the pointer could move during a tap in inches
michael@0 69 const TAP_MAX_RADIUS = 0.2;
michael@0 70 // Directness coefficient. It is based on the maximum 15 degree angle between
michael@0 71 // consequent pointer move lines.
michael@0 72 const DIRECTNESS_COEFF = 1.44;
michael@0 73 // An android flag.
michael@0 74 const IS_ANDROID = Utils.MozBuildApp === 'mobile/android' &&
michael@0 75 Utils.AndroidSdkVersion >= 14;
michael@0 76 // A single pointer down/up sequence periodically precedes the tripple swipe
michael@0 77 // gesture on Android. This delay acounts for that.
michael@0 78 const ANDROID_TRIPLE_SWIPE_DELAY = 50;
michael@0 79 // The virtual touch ID generated by a mouse event.
michael@0 80 const MOUSE_ID = 'mouse';
michael@0 81
michael@0 82 /**
michael@0 83 * A point object containing distance travelled data.
michael@0 84 * @param {Object} aPoint A point object that looks like: {
michael@0 85 * x: x coordinate in pixels,
michael@0 86 * y: y coordinate in pixels
michael@0 87 * }
michael@0 88 */
michael@0 89 function Point(aPoint) {
michael@0 90 this.startX = this.x = aPoint.x;
michael@0 91 this.startY = this.y = aPoint.y;
michael@0 92 this.distanceTraveled = 0;
michael@0 93 this.totalDistanceTraveled = 0;
michael@0 94 }
michael@0 95
michael@0 96 Point.prototype = {
michael@0 97 /**
michael@0 98 * Update the current point coordiates.
michael@0 99 * @param {Object} aPoint A new point coordinates.
michael@0 100 */
michael@0 101 update: function Point_update(aPoint) {
michael@0 102 let lastX = this.x;
michael@0 103 let lastY = this.y;
michael@0 104 this.x = aPoint.x;
michael@0 105 this.y = aPoint.y;
michael@0 106 this.distanceTraveled = this.getDistanceToCoord(lastX, lastY);
michael@0 107 this.totalDistanceTraveled += this.distanceTraveled;
michael@0 108 },
michael@0 109
michael@0 110 reset: function Point_reset() {
michael@0 111 this.distanceTraveled = 0;
michael@0 112 this.totalDistanceTraveled = 0;
michael@0 113 },
michael@0 114
michael@0 115 /**
michael@0 116 * Get distance between the current point coordinates and the given ones.
michael@0 117 * @param {Number} aX A pixel value for the x coordinate.
michael@0 118 * @param {Number} aY A pixel value for the y coordinate.
michael@0 119 * @return {Number} A distance between point's current and the given
michael@0 120 * coordinates.
michael@0 121 */
michael@0 122 getDistanceToCoord: function Point_getDistanceToCoord(aX, aY) {
michael@0 123 return Math.hypot(this.x - aX, this.y - aY);
michael@0 124 },
michael@0 125
michael@0 126 /**
michael@0 127 * Get the direct distance travelled by the point so far.
michael@0 128 */
michael@0 129 get directDistanceTraveled() {
michael@0 130 return this.getDistanceToCoord(this.startX, this.startY);
michael@0 131 }
michael@0 132 };
michael@0 133
michael@0 134 /**
michael@0 135 * An externally accessible collection of settings used in gesture resolition.
michael@0 136 * @type {Object}
michael@0 137 */
michael@0 138 this.GestureSettings = { // jshint ignore:line
michael@0 139 /**
michael@0 140 * Maximum duration of swipe
michael@0 141 * @type {Number}
michael@0 142 */
michael@0 143 swipeMaxDuration: 400,
michael@0 144
michael@0 145 /**
michael@0 146 * Maximum consecutive pointer event timeout.
michael@0 147 * @type {Number}
michael@0 148 */
michael@0 149 maxConsecutiveGestureDelay: 400,
michael@0 150
michael@0 151 /**
michael@0 152 * Delay before tap turns into dwell
michael@0 153 * @type {Number}
michael@0 154 */
michael@0 155 dwellThreshold: 500,
michael@0 156
michael@0 157 /**
michael@0 158 * Minimum distance that needs to be travelled for the pointer move to be
michael@0 159 * fired.
michael@0 160 * @type {Number}
michael@0 161 */
michael@0 162 travelThreshold: 0.025
michael@0 163 };
michael@0 164
michael@0 165 /**
michael@0 166 * An interface that handles the pointer events and calculates the appropriate
michael@0 167 * gestures.
michael@0 168 * @type {Object}
michael@0 169 */
michael@0 170 this.GestureTracker = { // jshint ignore:line
michael@0 171 /**
michael@0 172 * Reset GestureTracker to its initial state.
michael@0 173 * @return {[type]} [description]
michael@0 174 */
michael@0 175 reset: function GestureTracker_reset() {
michael@0 176 if (this.current) {
michael@0 177 this.current.clearTimer();
michael@0 178 }
michael@0 179 delete this.current;
michael@0 180 },
michael@0 181
michael@0 182 /**
michael@0 183 * Create a new gesture object and attach resolution handler to it as well as
michael@0 184 * handle the incoming pointer event.
michael@0 185 * @param {Object} aDetail A new pointer event detail.
michael@0 186 * @param {Number} aTimeStamp A new pointer event timeStamp.
michael@0 187 * @param {Function} aGesture A gesture constructor (default: Tap).
michael@0 188 */
michael@0 189 _init: function GestureTracker__init(aDetail, aTimeStamp, aGesture = Tap) {
michael@0 190 // Only create a new gesture on |pointerdown| event.
michael@0 191 if (aDetail.type !== 'pointerdown') {
michael@0 192 return;
michael@0 193 }
michael@0 194 let points = aDetail.points;
michael@0 195 let GestureConstructor = aGesture;
michael@0 196 if (IS_ANDROID && GestureConstructor === Tap && points.length === 1 &&
michael@0 197 points[0].identifier !== MOUSE_ID) {
michael@0 198 // Handle Android events when EBT is enabled. Two finger gestures are
michael@0 199 // translated to one.
michael@0 200 GestureConstructor = AndroidTap;
michael@0 201 }
michael@0 202 this._create(GestureConstructor);
michael@0 203 this._update(aDetail, aTimeStamp);
michael@0 204 },
michael@0 205
michael@0 206 /**
michael@0 207 * Handle the incoming pointer event with the existing gesture object(if
michael@0 208 * present) or with the newly created one.
michael@0 209 * @param {Object} aDetail A new pointer event detail.
michael@0 210 * @param {Number} aTimeStamp A new pointer event timeStamp.
michael@0 211 */
michael@0 212 handle: function GestureTracker_handle(aDetail, aTimeStamp) {
michael@0 213 Logger.debug(() => {
michael@0 214 return ['Pointer event', aDetail.type, 'at:', aTimeStamp,
michael@0 215 JSON.stringify(aDetail.points)];
michael@0 216 });
michael@0 217 this[this.current ? '_update' : '_init'](aDetail, aTimeStamp);
michael@0 218 },
michael@0 219
michael@0 220 /**
michael@0 221 * Create a new gesture object and attach resolution handler to it.
michael@0 222 * @param {Function} aGesture A gesture constructor.
michael@0 223 * @param {Number} aTimeStamp An original pointer event timeStamp.
michael@0 224 * @param {Array} aPoints All changed points associated with the new pointer
michael@0 225 * event.
michael@0 226 * @param {?String} aLastEvent Last pointer event type.
michael@0 227 */
michael@0 228 _create: function GestureTracker__create(aGesture, aTimeStamp, aPoints, aLastEvent) {
michael@0 229 this.current = new aGesture(aTimeStamp, aPoints, aLastEvent); /* A constructor name should start with an uppercase letter. */ // jshint ignore:line
michael@0 230 this.current.then(this._onFulfill.bind(this));
michael@0 231 },
michael@0 232
michael@0 233 /**
michael@0 234 * Handle the incoming pointer event with the existing gesture object.
michael@0 235 * @param {Object} aDetail A new pointer event detail.
michael@0 236 * @param {Number} aTimeStamp A new pointer event timeStamp.
michael@0 237 */
michael@0 238 _update: function GestureTracker_update(aDetail, aTimeStamp) {
michael@0 239 this.current[aDetail.type](aDetail.points, aTimeStamp);
michael@0 240 },
michael@0 241
michael@0 242 /**
michael@0 243 * A resolution handler function for the current gesture promise.
michael@0 244 * @param {Object} aResult A resolution payload with the relevant gesture id
michael@0 245 * and an optional new gesture contructor.
michael@0 246 */
michael@0 247 _onFulfill: function GestureTracker__onFulfill(aResult) {
michael@0 248 let {id, gestureType} = aResult;
michael@0 249 let current = this.current;
michael@0 250 // Do nothing if there's no existing gesture or there's already a newer
michael@0 251 // gesture.
michael@0 252 if (!current || current.id !== id) {
michael@0 253 return;
michael@0 254 }
michael@0 255 // Only create a gesture if we got a constructor.
michael@0 256 if (gestureType) {
michael@0 257 this._create(gestureType, current.startTime, current.points,
michael@0 258 current.lastEvent);
michael@0 259 } else {
michael@0 260 delete this.current;
michael@0 261 }
michael@0 262 }
michael@0 263 };
michael@0 264
michael@0 265 /**
michael@0 266 * Compile a mozAccessFuGesture detail structure.
michael@0 267 * @param {String} aType A gesture type.
michael@0 268 * @param {Object} aPoints Gesture's points.
michael@0 269 * @param {String} xKey A default key for the x coordinate. Default is
michael@0 270 * 'startX'.
michael@0 271 * @param {String} yKey A default key for the y coordinate. Default is
michael@0 272 * 'startY'.
michael@0 273 * @return {Object} a mozAccessFuGesture detail structure.
michael@0 274 */
michael@0 275 function compileDetail(aType, aPoints, keyMap = {x: 'startX', y: 'startY'}) {
michael@0 276 let touches = [];
michael@0 277 let maxDeltaX = 0;
michael@0 278 let maxDeltaY = 0;
michael@0 279 for (let identifier in aPoints) {
michael@0 280 let point = aPoints[identifier];
michael@0 281 let touch = {};
michael@0 282 for (let key in keyMap) {
michael@0 283 touch[key] = point[keyMap[key]];
michael@0 284 }
michael@0 285 touches.push(touch);
michael@0 286 let deltaX = point.x - point.startX;
michael@0 287 let deltaY = point.y - point.startY;
michael@0 288 // Determine the maximum x and y travel intervals.
michael@0 289 if (Math.abs(maxDeltaX) < Math.abs(deltaX)) {
michael@0 290 maxDeltaX = deltaX;
michael@0 291 }
michael@0 292 if (Math.abs(maxDeltaY) < Math.abs(deltaY)) {
michael@0 293 maxDeltaY = deltaY;
michael@0 294 }
michael@0 295 // Since the gesture is resolving, reset the points' distance information
michael@0 296 // since they are passed to the next potential gesture.
michael@0 297 point.reset();
michael@0 298 }
michael@0 299 return {
michael@0 300 type: aType,
michael@0 301 touches: touches,
michael@0 302 deltaX: maxDeltaX,
michael@0 303 deltaY: maxDeltaY
michael@0 304 };
michael@0 305 }
michael@0 306
michael@0 307 /**
michael@0 308 * A general gesture object.
michael@0 309 * @param {Number} aTimeStamp An original pointer event's timeStamp that started
michael@0 310 * the gesture resolution sequence.
michael@0 311 * @param {Object} aPoints An existing set of points (from previous events).
michael@0 312 * Default is an empty object.
michael@0 313 * @param {?String} aLastEvent Last pointer event type.
michael@0 314 */
michael@0 315 function Gesture(aTimeStamp, aPoints = {}, aLastEvent = undefined) {
michael@0 316 this.startTime = Date.now();
michael@0 317 Logger.debug('Creating', this.id, 'gesture.');
michael@0 318 this.points = aPoints;
michael@0 319 this.lastEvent = aLastEvent;
michael@0 320 this._deferred = Promise.defer();
michael@0 321 // Call this._handleResolve or this._handleReject when the promise is
michael@0 322 // fulfilled with either resolve or reject.
michael@0 323 this.promise = this._deferred.promise.then(this._handleResolve.bind(this),
michael@0 324 this._handleReject.bind(this));
michael@0 325 this.startTimer(aTimeStamp);
michael@0 326 }
michael@0 327
michael@0 328 Gesture.prototype = {
michael@0 329 /**
michael@0 330 * Get the gesture timeout delay.
michael@0 331 * @return {Number}
michael@0 332 */
michael@0 333 _getDelay: function Gesture__getDelay() {
michael@0 334 // If nothing happens withing the
michael@0 335 // GestureSettings.maxConsecutiveGestureDelay, we should not wait for any
michael@0 336 // more pointer events and consider them the part of the same gesture -
michael@0 337 // reject this gesture promise.
michael@0 338 return GestureSettings.maxConsecutiveGestureDelay;
michael@0 339 },
michael@0 340
michael@0 341 /**
michael@0 342 * Clear the existing timer.
michael@0 343 */
michael@0 344 clearTimer: function Gesture_clearTimer() {
michael@0 345 clearTimeout(this._timer);
michael@0 346 delete this._timer;
michael@0 347 },
michael@0 348
michael@0 349 /**
michael@0 350 * Start the timer for gesture timeout.
michael@0 351 * @param {Number} aTimeStamp An original pointer event's timeStamp that
michael@0 352 * started the gesture resolution sequence.
michael@0 353 */
michael@0 354 startTimer: function Gesture_startTimer(aTimeStamp) {
michael@0 355 this.clearTimer();
michael@0 356 let delay = this._getDelay(aTimeStamp);
michael@0 357 let handler = () => {
michael@0 358 delete this._timer;
michael@0 359 if (!this._inProgress) {
michael@0 360 this._deferred.reject();
michael@0 361 } else if (this._rejectToOnWait) {
michael@0 362 this._deferred.reject(this._rejectToOnWait);
michael@0 363 }
michael@0 364 };
michael@0 365 if (delay <= 0) {
michael@0 366 handler();
michael@0 367 } else {
michael@0 368 this._timer = setTimeout(handler, delay);
michael@0 369 }
michael@0 370 },
michael@0 371
michael@0 372 /**
michael@0 373 * Add a gesture promise resolution callback.
michael@0 374 * @param {Function} aCallback
michael@0 375 */
michael@0 376 then: function Gesture_then(aCallback) {
michael@0 377 this.promise.then(aCallback);
michael@0 378 },
michael@0 379
michael@0 380 /**
michael@0 381 * Update gesture's points. Test the points set with the optional gesture test
michael@0 382 * function.
michael@0 383 * @param {Array} aPoints An array with the changed points from the new
michael@0 384 * pointer event.
michael@0 385 * @param {String} aType Pointer event type.
michael@0 386 * @param {Boolean} aCanCreate A flag that enables including the new points.
michael@0 387 * Default is false.
michael@0 388 * @param {Boolean} aNeedComplete A flag that indicates that the gesture is
michael@0 389 * completing. Default is false.
michael@0 390 * @return {Boolean} Indicates whether the gesture can be complete (it is
michael@0 391 * set to true iff the aNeedComplete is true and there was a change to at
michael@0 392 * least one point that belongs to the gesture).
michael@0 393 */
michael@0 394 _update: function Gesture__update(aPoints, aType, aCanCreate = false, aNeedComplete = false) {
michael@0 395 let complete;
michael@0 396 let lastEvent;
michael@0 397 for (let point of aPoints) {
michael@0 398 let identifier = point.identifier;
michael@0 399 let gesturePoint = this.points[identifier];
michael@0 400 if (gesturePoint) {
michael@0 401 gesturePoint.update(point);
michael@0 402 if (aNeedComplete) {
michael@0 403 // Since the gesture is completing and at least one of the gesture
michael@0 404 // points is updated, set the return value to true.
michael@0 405 complete = true;
michael@0 406 }
michael@0 407 lastEvent = lastEvent || aType;
michael@0 408 } else if (aCanCreate) {
michael@0 409 // Only create a new point if aCanCreate is true.
michael@0 410 this.points[identifier] =
michael@0 411 new Point(point);
michael@0 412 lastEvent = lastEvent || aType;
michael@0 413 }
michael@0 414 }
michael@0 415 this.lastEvent = lastEvent || this.lastEvent;
michael@0 416 // If test function is defined test the points.
michael@0 417 if (this.test) {
michael@0 418 this.test(complete);
michael@0 419 }
michael@0 420 return complete;
michael@0 421 },
michael@0 422
michael@0 423 /**
michael@0 424 * Emit a mozAccessFuGesture (when the gesture is resolved).
michael@0 425 * @param {Object} aDetail a compiled mozAccessFuGesture detail structure.
michael@0 426 */
michael@0 427 _emit: function Gesture__emit(aDetail) {
michael@0 428 let evt = new Utils.win.CustomEvent('mozAccessFuGesture', {
michael@0 429 bubbles: true,
michael@0 430 cancelable: true,
michael@0 431 detail: aDetail
michael@0 432 });
michael@0 433 Utils.win.dispatchEvent(evt);
michael@0 434 },
michael@0 435
michael@0 436 /**
michael@0 437 * Handle the pointer down event.
michael@0 438 * @param {Array} aPoints A new pointer down points.
michael@0 439 * @param {Number} aTimeStamp A new pointer down timeStamp.
michael@0 440 */
michael@0 441 pointerdown: function Gesture_pointerdown(aPoints, aTimeStamp) {
michael@0 442 this._inProgress = true;
michael@0 443 this._update(aPoints, 'pointerdown',
michael@0 444 aTimeStamp - this.startTime < MAX_MULTITOUCH);
michael@0 445 },
michael@0 446
michael@0 447 /**
michael@0 448 * Handle the pointer move event.
michael@0 449 * @param {Array} aPoints A new pointer move points.
michael@0 450 */
michael@0 451 pointermove: function Gesture_pointermove(aPoints) {
michael@0 452 this._update(aPoints, 'pointermove');
michael@0 453 },
michael@0 454
michael@0 455 /**
michael@0 456 * Handle the pointer up event.
michael@0 457 * @param {Array} aPoints A new pointer up points.
michael@0 458 */
michael@0 459 pointerup: function Gesture_pointerup(aPoints) {
michael@0 460 let complete = this._update(aPoints, 'pointerup', false, true);
michael@0 461 if (complete) {
michael@0 462 this._deferred.resolve();
michael@0 463 }
michael@0 464 },
michael@0 465
michael@0 466 /**
michael@0 467 * A subsequent gesture constructor to resolve the current one to. E.g.
michael@0 468 * tap->doubletap, dwell->dwellend, etc.
michael@0 469 * @type {Function}
michael@0 470 */
michael@0 471 resolveTo: null,
michael@0 472
michael@0 473 /**
michael@0 474 * A unique id for the gesture. Composed of the type + timeStamp.
michael@0 475 */
michael@0 476 get id() {
michael@0 477 delete this._id;
michael@0 478 this._id = this.type + this.startTime;
michael@0 479 return this._id;
michael@0 480 },
michael@0 481
michael@0 482 /**
michael@0 483 * A gesture promise resolve callback. Compile and emit the gesture.
michael@0 484 * @return {Object} Returns a structure to the gesture handler that looks like
michael@0 485 * this: {
michael@0 486 * id: current gesture id,
michael@0 487 * gestureType: an optional subsequent gesture constructor.
michael@0 488 * }
michael@0 489 */
michael@0 490 _handleResolve: function Gesture__handleResolve() {
michael@0 491 if (this.isComplete) {
michael@0 492 return;
michael@0 493 }
michael@0 494 Logger.debug('Resolving', this.id, 'gesture.');
michael@0 495 this.isComplete = true;
michael@0 496 let detail = this.compile();
michael@0 497 if (detail) {
michael@0 498 this._emit(detail);
michael@0 499 }
michael@0 500 return {
michael@0 501 id: this.id,
michael@0 502 gestureType: this.resolveTo
michael@0 503 };
michael@0 504 },
michael@0 505
michael@0 506 /**
michael@0 507 * A gesture promise reject callback.
michael@0 508 * @return {Object} Returns a structure to the gesture handler that looks like
michael@0 509 * this: {
michael@0 510 * id: current gesture id,
michael@0 511 * gestureType: an optional subsequent gesture constructor.
michael@0 512 * }
michael@0 513 */
michael@0 514 _handleReject: function Gesture__handleReject(aRejectTo) {
michael@0 515 if (this.isComplete) {
michael@0 516 return;
michael@0 517 }
michael@0 518 Logger.debug('Rejecting', this.id, 'gesture.');
michael@0 519 this.isComplete = true;
michael@0 520 return {
michael@0 521 id: this.id,
michael@0 522 gestureType: aRejectTo
michael@0 523 };
michael@0 524 },
michael@0 525
michael@0 526 /**
michael@0 527 * A default compilation function used to build the mozAccessFuGesture event
michael@0 528 * detail. The detail always includes the type and the touches associated
michael@0 529 * with the gesture.
michael@0 530 * @return {Object} Gesture event detail.
michael@0 531 */
michael@0 532 compile: function Gesture_compile() {
michael@0 533 return compileDetail(this.type, this.points);
michael@0 534 }
michael@0 535 };
michael@0 536
michael@0 537 /**
michael@0 538 * A mixin for an explore related object.
michael@0 539 */
michael@0 540 function ExploreGesture() {
michael@0 541 this.compile = () => {
michael@0 542 // Unlike most of other gestures explore based gestures compile using the
michael@0 543 // current point position and not the start one.
michael@0 544 return compileDetail(this.type, this.points, {x: 'x', y: 'y'});
michael@0 545 };
michael@0 546 }
michael@0 547
michael@0 548 /**
michael@0 549 * Check the in progress gesture for completion.
michael@0 550 */
michael@0 551 function checkProgressGesture(aGesture) {
michael@0 552 aGesture._inProgress = true;
michael@0 553 if (aGesture.lastEvent === 'pointerup') {
michael@0 554 if (aGesture.test) {
michael@0 555 aGesture.test(true);
michael@0 556 }
michael@0 557 aGesture._deferred.resolve();
michael@0 558 }
michael@0 559 }
michael@0 560
michael@0 561 /**
michael@0 562 * A common travel gesture. When the travel gesture is created, all subsequent
michael@0 563 * pointer events' points are tested for their total distance traveled. If that
michael@0 564 * distance exceeds the _threshold distance, the gesture will be rejected to a
michael@0 565 * _travelTo gesture.
michael@0 566 * @param {Number} aTimeStamp An original pointer event's timeStamp that started
michael@0 567 * the gesture resolution sequence.
michael@0 568 * @param {Object} aPoints An existing set of points (from previous events).
michael@0 569 * @param {?String} aLastEvent Last pointer event type.
michael@0 570 * @param {Function} aTravelTo A contructor for the gesture to reject to when
michael@0 571 * travelling (default: Explore).
michael@0 572 * @param {Number} aThreshold Travel threshold (default:
michael@0 573 * GestureSettings.travelThreshold).
michael@0 574 */
michael@0 575 function TravelGesture(aTimeStamp, aPoints, aLastEvent, aTravelTo = Explore, aThreshold = GestureSettings.travelThreshold) {
michael@0 576 Gesture.call(this, aTimeStamp, aPoints, aLastEvent);
michael@0 577 this._travelTo = aTravelTo;
michael@0 578 this._threshold = aThreshold;
michael@0 579 }
michael@0 580
michael@0 581 TravelGesture.prototype = Object.create(Gesture.prototype);
michael@0 582
michael@0 583 /**
michael@0 584 * Test the gesture points for travel. The gesture will be rejected to
michael@0 585 * this._travelTo gesture iff at least one point crosses this._threshold.
michael@0 586 */
michael@0 587 TravelGesture.prototype.test = function TravelGesture_test() {
michael@0 588 for (let identifier in this.points) {
michael@0 589 let point = this.points[identifier];
michael@0 590 if (point.totalDistanceTraveled / Utils.dpi > this._threshold) {
michael@0 591 this._deferred.reject(this._travelTo);
michael@0 592 return;
michael@0 593 }
michael@0 594 }
michael@0 595 };
michael@0 596
michael@0 597 /**
michael@0 598 * DwellEnd gesture.
michael@0 599 * @param {Number} aTimeStamp An original pointer event's timeStamp that started
michael@0 600 * the gesture resolution sequence.
michael@0 601 * @param {Object} aPoints An existing set of points (from previous events).
michael@0 602 * @param {?String} aLastEvent Last pointer event type.
michael@0 603 */
michael@0 604 function DwellEnd(aTimeStamp, aPoints, aLastEvent) {
michael@0 605 this._inProgress = true;
michael@0 606 // If the pointer travels, reject to Explore.
michael@0 607 TravelGesture.call(this, aTimeStamp, aPoints, aLastEvent);
michael@0 608 checkProgressGesture(this);
michael@0 609 }
michael@0 610
michael@0 611 DwellEnd.prototype = Object.create(TravelGesture.prototype);
michael@0 612 DwellEnd.prototype.type = 'dwellend';
michael@0 613
michael@0 614 /**
michael@0 615 * TapHoldEnd gesture. This gesture can be represented as the following diagram:
michael@0 616 * pointerdown-pointerup-pointerdown-*wait*-pointerup.
michael@0 617 * @param {Number} aTimeStamp An original pointer event's timeStamp that started
michael@0 618 * the gesture resolution sequence.
michael@0 619 * @param {Object} aPoints An existing set of points (from previous events).
michael@0 620 * @param {?String} aLastEvent Last pointer event type.
michael@0 621 */
michael@0 622 function TapHoldEnd(aTimeStamp, aPoints, aLastEvent) {
michael@0 623 this._inProgress = true;
michael@0 624 // If the pointer travels, reject to Explore.
michael@0 625 TravelGesture.call(this, aTimeStamp, aPoints, aLastEvent);
michael@0 626 checkProgressGesture(this);
michael@0 627 }
michael@0 628
michael@0 629 TapHoldEnd.prototype = Object.create(TravelGesture.prototype);
michael@0 630 TapHoldEnd.prototype.type = 'tapholdend';
michael@0 631
michael@0 632 /**
michael@0 633 * DoubleTapHoldEnd gesture. This gesture can be represented as the following
michael@0 634 * diagram:
michael@0 635 * pointerdown-pointerup-pointerdown-pointerup-pointerdown-*wait*-pointerup.
michael@0 636 * @param {Number} aTimeStamp An original pointer event's timeStamp that started
michael@0 637 * the gesture resolution sequence.
michael@0 638 * @param {Object} aPoints An existing set of points (from previous events).
michael@0 639 * @param {?String} aLastEvent Last pointer event type.
michael@0 640 */
michael@0 641 function DoubleTapHoldEnd(aTimeStamp, aPoints, aLastEvent) {
michael@0 642 this._inProgress = true;
michael@0 643 // If the pointer travels, reject to Explore.
michael@0 644 TravelGesture.call(this, aTimeStamp, aPoints, aLastEvent);
michael@0 645 checkProgressGesture(this);
michael@0 646 }
michael@0 647
michael@0 648 DoubleTapHoldEnd.prototype = Object.create(TravelGesture.prototype);
michael@0 649 DoubleTapHoldEnd.prototype.type = 'doubletapholdend';
michael@0 650
michael@0 651 /**
michael@0 652 * A common tap gesture object.
michael@0 653 * @param {Number} aTimeStamp An original pointer event's timeStamp that started
michael@0 654 * the gesture resolution sequence.
michael@0 655 * @param {Object} aPoints An existing set of points (from previous events).
michael@0 656 * @param {?String} aLastEvent Last pointer event type.
michael@0 657 * @param {Function} aRejectTo A constructor for the next gesture to reject to
michael@0 658 * in case no pointermove or pointerup happens within the
michael@0 659 * GestureSettings.dwellThreshold.
michael@0 660 * @param {Function} aTravelTo An optional constuctor for the next gesture to
michael@0 661 * reject to in case the the TravelGesture test fails.
michael@0 662 */
michael@0 663 function TapGesture(aTimeStamp, aPoints, aLastEvent, aRejectTo, aTravelTo) {
michael@0 664 this._rejectToOnWait = aRejectTo;
michael@0 665 // If the pointer travels, reject to aTravelTo.
michael@0 666 TravelGesture.call(this, aTimeStamp, aPoints, aLastEvent, aTravelTo,
michael@0 667 TAP_MAX_RADIUS);
michael@0 668 }
michael@0 669
michael@0 670 TapGesture.prototype = Object.create(TravelGesture.prototype);
michael@0 671 TapGesture.prototype._getDelay = function TapGesture__getDelay() {
michael@0 672 // If, for TapGesture, no pointermove or pointerup happens within the
michael@0 673 // GestureSettings.dwellThreshold, reject.
michael@0 674 // Note: the original pointer event's timeStamp is irrelevant here.
michael@0 675 return GestureSettings.dwellThreshold;
michael@0 676 };
michael@0 677
michael@0 678 /**
michael@0 679 * Tap gesture.
michael@0 680 * @param {Number} aTimeStamp An original pointer event's timeStamp that started
michael@0 681 * the gesture resolution sequence.
michael@0 682 * @param {Object} aPoints An existing set of points (from previous events).
michael@0 683 * @param {?String} aLastEvent Last pointer event type.
michael@0 684 */
michael@0 685 function Tap(aTimeStamp, aPoints, aLastEvent) {
michael@0 686 // If the pointer travels, reject to Swipe.
michael@0 687 TapGesture.call(this, aTimeStamp, aPoints, aLastEvent, Dwell, Swipe);
michael@0 688 }
michael@0 689
michael@0 690 Tap.prototype = Object.create(TapGesture.prototype);
michael@0 691 Tap.prototype.type = 'tap';
michael@0 692 Tap.prototype.resolveTo = DoubleTap;
michael@0 693
michael@0 694 /**
michael@0 695 * Tap (multi) gesture on Android.
michael@0 696 * @param {Number} aTimeStamp An original pointer event's timeStamp that started
michael@0 697 * the gesture resolution sequence.
michael@0 698 * @param {Object} aPoints An existing set of points (from previous events).
michael@0 699 * @param {?String} aLastEvent Last pointer event type.
michael@0 700 */
michael@0 701 function AndroidTap(aTimeStamp, aPoints, aLastEvent) {
michael@0 702 // If the pointer travels, reject to Swipe. On dwell threshold reject to
michael@0 703 // TapHold.
michael@0 704 TapGesture.call(this, aTimeStamp, aPoints, aLastEvent, TapHold, Swipe);
michael@0 705 }
michael@0 706 AndroidTap.prototype = Object.create(TapGesture.prototype);
michael@0 707 // Android double taps are translated to single taps.
michael@0 708 AndroidTap.prototype.type = 'doubletap';
michael@0 709 AndroidTap.prototype.resolveTo = TripleTap;
michael@0 710
michael@0 711 /**
michael@0 712 * Clear the pointerup handler timer in case of the 3 pointer swipe.
michael@0 713 */
michael@0 714 AndroidTap.prototype.clearThreeFingerSwipeTimer = function AndroidTap_clearThreeFingerSwipeTimer() {
michael@0 715 clearTimeout(this._threeFingerSwipeTimer);
michael@0 716 delete this._threeFingerSwipeTimer;
michael@0 717 };
michael@0 718
michael@0 719 AndroidTap.prototype.pointerdown = function AndroidTap_pointerdown(aPoints, aTimeStamp) {
michael@0 720 this.clearThreeFingerSwipeTimer();
michael@0 721 TapGesture.prototype.pointerdown.call(this, aPoints, aTimeStamp);
michael@0 722 };
michael@0 723
michael@0 724 AndroidTap.prototype.pointermove = function AndroidTap_pointermove(aPoints) {
michael@0 725 this.clearThreeFingerSwipeTimer();
michael@0 726 this._moved = true;
michael@0 727 TapGesture.prototype.pointermove.call(this, aPoints);
michael@0 728 };
michael@0 729
michael@0 730 AndroidTap.prototype.pointerup = function AndroidTap_pointerup(aPoints) {
michael@0 731 if (this._moved) {
michael@0 732 // If there was a pointer move - handle the real gesture.
michael@0 733 TapGesture.prototype.pointerup.call(this, aPoints);
michael@0 734 } else {
michael@0 735 // Primptively delay the multi pointer gesture resolution, because Android
michael@0 736 // sometimes fires a pointerdown/poitnerup sequence before the real events.
michael@0 737 this._threeFingerSwipeTimer = setTimeout(() => {
michael@0 738 delete this._threeFingerSwipeTimer;
michael@0 739 TapGesture.prototype.pointerup.call(this, aPoints);
michael@0 740 }, ANDROID_TRIPLE_SWIPE_DELAY);
michael@0 741 }
michael@0 742 };
michael@0 743
michael@0 744 /**
michael@0 745 * Reject an android tap gesture.
michael@0 746 * @param {?Function} aRejectTo An optional next gesture constructor.
michael@0 747 * @return {Object} structure that looks like {
michael@0 748 * id: gesture_id, // Current AndroidTap gesture id.
michael@0 749 * gestureType: next_gesture // Optional
michael@0 750 * }
michael@0 751 */
michael@0 752 AndroidTap.prototype._handleReject = function AndroidTap__handleReject(aRejectTo) {
michael@0 753 let keys = Object.keys(this.points);
michael@0 754 if (aRejectTo === Swipe && keys.length === 1) {
michael@0 755 let key = keys[0];
michael@0 756 let point = this.points[key];
michael@0 757 // Two finger swipe is translated into single swipe.
michael@0 758 this.points[key + '-copy'] = point;
michael@0 759 }
michael@0 760 return TapGesture.prototype._handleReject.call(this, aRejectTo);
michael@0 761 };
michael@0 762
michael@0 763 /**
michael@0 764 * Double Tap gesture.
michael@0 765 * @param {Number} aTimeStamp An original pointer event's timeStamp that started
michael@0 766 * the gesture resolution sequence.
michael@0 767 * @param {Object} aPoints An existing set of points (from previous events).
michael@0 768 * @param {?String} aLastEvent Last pointer event type.
michael@0 769 */
michael@0 770 function DoubleTap(aTimeStamp, aPoints, aLastEvent) {
michael@0 771 TapGesture.call(this, aTimeStamp, aPoints, aLastEvent, TapHold);
michael@0 772 }
michael@0 773
michael@0 774 DoubleTap.prototype = Object.create(TapGesture.prototype);
michael@0 775 DoubleTap.prototype.type = 'doubletap';
michael@0 776 DoubleTap.prototype.resolveTo = TripleTap;
michael@0 777
michael@0 778 /**
michael@0 779 * Triple Tap gesture.
michael@0 780 * @param {Number} aTimeStamp An original pointer event's timeStamp that started
michael@0 781 * the gesture resolution sequence.
michael@0 782 * @param {Object} aPoints An existing set of points (from previous events).
michael@0 783 * @param {?String} aLastEvent Last pointer event type.
michael@0 784 */
michael@0 785 function TripleTap(aTimeStamp, aPoints, aLastEvent) {
michael@0 786 TapGesture.call(this, aTimeStamp, aPoints, aLastEvent, DoubleTapHold);
michael@0 787 }
michael@0 788
michael@0 789 TripleTap.prototype = Object.create(TapGesture.prototype);
michael@0 790 TripleTap.prototype.type = 'tripletap';
michael@0 791
michael@0 792 /**
michael@0 793 * Common base object for gestures that are created as resolved.
michael@0 794 * @param {Number} aTimeStamp An original pointer event's timeStamp that started
michael@0 795 * the gesture resolution sequence.
michael@0 796 * @param {Object} aPoints An existing set of points (from previous events).
michael@0 797 * @param {?String} aLastEvent Last pointer event type.
michael@0 798 */
michael@0 799 function ResolvedGesture(aTimeStamp, aPoints, aLastEvent) {
michael@0 800 Gesture.call(this, aTimeStamp, aPoints, aLastEvent);
michael@0 801 // Resolve the guesture right away.
michael@0 802 this._deferred.resolve();
michael@0 803 }
michael@0 804
michael@0 805 ResolvedGesture.prototype = Object.create(Gesture.prototype);
michael@0 806
michael@0 807 /**
michael@0 808 * Dwell gesture
michael@0 809 * @param {Number} aTimeStamp An original pointer event's timeStamp that started
michael@0 810 * the gesture resolution sequence.
michael@0 811 * @param {Object} aPoints An existing set of points (from previous events).
michael@0 812 * @param {?String} aLastEvent Last pointer event type.
michael@0 813 */
michael@0 814 function Dwell(aTimeStamp, aPoints, aLastEvent) {
michael@0 815 ResolvedGesture.call(this, aTimeStamp, aPoints, aLastEvent);
michael@0 816 }
michael@0 817
michael@0 818 Dwell.prototype = Object.create(ResolvedGesture.prototype);
michael@0 819 Dwell.prototype.type = 'dwell';
michael@0 820 Dwell.prototype.resolveTo = DwellEnd;
michael@0 821
michael@0 822 /**
michael@0 823 * TapHold gesture
michael@0 824 * @param {Number} aTimeStamp An original pointer event's timeStamp that started
michael@0 825 * the gesture resolution sequence.
michael@0 826 * @param {Object} aPoints An existing set of points (from previous events).
michael@0 827 * @param {?String} aLastEvent Last pointer event type.
michael@0 828 */
michael@0 829 function TapHold(aTimeStamp, aPoints, aLastEvent) {
michael@0 830 ResolvedGesture.call(this, aTimeStamp, aPoints, aLastEvent);
michael@0 831 }
michael@0 832
michael@0 833 TapHold.prototype = Object.create(ResolvedGesture.prototype);
michael@0 834 TapHold.prototype.type = 'taphold';
michael@0 835 TapHold.prototype.resolveTo = TapHoldEnd;
michael@0 836
michael@0 837 /**
michael@0 838 * DoubleTapHold gesture
michael@0 839 * @param {Number} aTimeStamp An original pointer event's timeStamp that started
michael@0 840 * the gesture resolution sequence.
michael@0 841 * @param {Object} aPoints An existing set of points (from previous events).
michael@0 842 * @param {?String} aLastEvent Last pointer event type.
michael@0 843 */
michael@0 844 function DoubleTapHold(aTimeStamp, aPoints, aLastEvent) {
michael@0 845 ResolvedGesture.call(this, aTimeStamp, aPoints, aLastEvent);
michael@0 846 }
michael@0 847
michael@0 848 DoubleTapHold.prototype = Object.create(ResolvedGesture.prototype);
michael@0 849 DoubleTapHold.prototype.type = 'doubletaphold';
michael@0 850 DoubleTapHold.prototype.resolveTo = DoubleTapHoldEnd;
michael@0 851
michael@0 852 /**
michael@0 853 * Explore gesture
michael@0 854 * @param {Number} aTimeStamp An original pointer event's timeStamp that started
michael@0 855 * the gesture resolution sequence.
michael@0 856 * @param {Object} aPoints An existing set of points (from previous events).
michael@0 857 * @param {?String} aLastEvent Last pointer event type.
michael@0 858 */
michael@0 859 function Explore(aTimeStamp, aPoints, aLastEvent) {
michael@0 860 ExploreGesture.call(this);
michael@0 861 ResolvedGesture.call(this, aTimeStamp, aPoints, aLastEvent);
michael@0 862 }
michael@0 863
michael@0 864 Explore.prototype = Object.create(ResolvedGesture.prototype);
michael@0 865 Explore.prototype.type = 'explore';
michael@0 866 Explore.prototype.resolveTo = ExploreEnd;
michael@0 867
michael@0 868 /**
michael@0 869 * ExploreEnd gesture.
michael@0 870 * @param {Number} aTimeStamp An original pointer event's timeStamp that started
michael@0 871 * the gesture resolution sequence.
michael@0 872 * @param {Object} aPoints An existing set of points (from previous events).
michael@0 873 * @param {?String} aLastEvent Last pointer event type.
michael@0 874 */
michael@0 875 function ExploreEnd(aTimeStamp, aPoints, aLastEvent) {
michael@0 876 this._inProgress = true;
michael@0 877 ExploreGesture.call(this);
michael@0 878 // If the pointer travels, reject to Explore.
michael@0 879 TravelGesture.call(this, aTimeStamp, aPoints, aLastEvent);
michael@0 880 checkProgressGesture(this);
michael@0 881 }
michael@0 882
michael@0 883 ExploreEnd.prototype = Object.create(TravelGesture.prototype);
michael@0 884 ExploreEnd.prototype.type = 'exploreend';
michael@0 885
michael@0 886 /**
michael@0 887 * Swipe gesture.
michael@0 888 * @param {Number} aTimeStamp An original pointer event's timeStamp that started
michael@0 889 * the gesture resolution sequence.
michael@0 890 * @param {Object} aPoints An existing set of points (from previous events).
michael@0 891 * @param {?String} aLastEvent Last pointer event type.
michael@0 892 */
michael@0 893 function Swipe(aTimeStamp, aPoints, aLastEvent) {
michael@0 894 this._inProgress = true;
michael@0 895 this._rejectToOnWait = Explore;
michael@0 896 Gesture.call(this, aTimeStamp, aPoints, aLastEvent);
michael@0 897 checkProgressGesture(this);
michael@0 898 }
michael@0 899
michael@0 900 Swipe.prototype = Object.create(Gesture.prototype);
michael@0 901 Swipe.prototype.type = 'swipe';
michael@0 902 Swipe.prototype._getDelay = function Swipe__getDelay(aTimeStamp) {
michael@0 903 // Swipe should be completed within the GestureSettings.swipeMaxDuration from
michael@0 904 // the initial pointer down event.
michael@0 905 return GestureSettings.swipeMaxDuration - this.startTime + aTimeStamp;
michael@0 906 };
michael@0 907
michael@0 908 /**
michael@0 909 * Determine wither the gesture was Swipe or Explore.
michael@0 910 * @param {Booler} aComplete A flag that indicates whether the gesture is and
michael@0 911 * will be complete after the test.
michael@0 912 */
michael@0 913 Swipe.prototype.test = function Swipe_test(aComplete) {
michael@0 914 if (!aComplete) {
michael@0 915 // No need to test if the gesture is not completing or can't be complete.
michael@0 916 return;
michael@0 917 }
michael@0 918 let reject = true;
michael@0 919 // If at least one point travelled for more than SWIPE_MIN_DISTANCE and it was
michael@0 920 // direct enough, consider it a Swipe.
michael@0 921 for (let identifier in this.points) {
michael@0 922 let point = this.points[identifier];
michael@0 923 let directDistance = point.directDistanceTraveled;
michael@0 924 if (directDistance / Utils.dpi >= SWIPE_MIN_DISTANCE ||
michael@0 925 directDistance * DIRECTNESS_COEFF >= point.totalDistanceTraveled) {
michael@0 926 reject = false;
michael@0 927 }
michael@0 928 }
michael@0 929 if (reject) {
michael@0 930 this._deferred.reject(Explore);
michael@0 931 }
michael@0 932 };
michael@0 933
michael@0 934 /**
michael@0 935 * Compile a swipe related mozAccessFuGesture event detail.
michael@0 936 * @return {Object} A mozAccessFuGesture detail object.
michael@0 937 */
michael@0 938 Swipe.prototype.compile = function Swipe_compile() {
michael@0 939 let type = this.type;
michael@0 940 let detail = compileDetail(type, this.points,
michael@0 941 {x1: 'startX', y1: 'startY', x2: 'x', y2: 'y'});
michael@0 942 let deltaX = detail.deltaX;
michael@0 943 let deltaY = detail.deltaY;
michael@0 944 if (Math.abs(deltaX) > Math.abs(deltaY)) {
michael@0 945 // Horizontal swipe.
michael@0 946 detail.type = type + (deltaX > 0 ? 'right' : 'left');
michael@0 947 } else {
michael@0 948 // Vertival swipe.
michael@0 949 detail.type = type + (deltaY > 0 ? 'down' : 'up');
michael@0 950 }
michael@0 951 return detail;
michael@0 952 };

mercurial