browser/base/content/browser-gestureSupport.js

Wed, 31 Dec 2014 06:55:46 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 06:55:46 +0100
changeset 1
ca08bd8f51b2
permissions
-rw-r--r--

Added tag TORBROWSER_REPLICA for changeset 6474c204b198

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
michael@0 3 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
michael@0 4
michael@0 5 Cu.import("resource://gre/modules/TelemetryStopwatch.jsm", this);
michael@0 6
michael@0 7 // Simple gestures support
michael@0 8 //
michael@0 9 // As per bug #412486, web content must not be allowed to receive any
michael@0 10 // simple gesture events. Multi-touch gesture APIs are in their
michael@0 11 // infancy and we do NOT want to be forced into supporting an API that
michael@0 12 // will probably have to change in the future. (The current Mac OS X
michael@0 13 // API is undocumented and was reverse-engineered.) Until support is
michael@0 14 // implemented in the event dispatcher to keep these events as
michael@0 15 // chrome-only, we must listen for the simple gesture events during
michael@0 16 // the capturing phase and call stopPropagation on every event.
michael@0 17
michael@0 18 let gGestureSupport = {
michael@0 19 _currentRotation: 0,
michael@0 20 _lastRotateDelta: 0,
michael@0 21 _rotateMomentumThreshold: .75,
michael@0 22
michael@0 23 /**
michael@0 24 * Add or remove mouse gesture event listeners
michael@0 25 *
michael@0 26 * @param aAddListener
michael@0 27 * True to add/init listeners and false to remove/uninit
michael@0 28 */
michael@0 29 init: function GS_init(aAddListener) {
michael@0 30 const gestureEvents = ["SwipeGestureStart",
michael@0 31 "SwipeGestureUpdate", "SwipeGestureEnd", "SwipeGesture",
michael@0 32 "MagnifyGestureStart", "MagnifyGestureUpdate", "MagnifyGesture",
michael@0 33 "RotateGestureStart", "RotateGestureUpdate", "RotateGesture",
michael@0 34 "TapGesture", "PressTapGesture"];
michael@0 35
michael@0 36 let addRemove = aAddListener ? window.addEventListener :
michael@0 37 window.removeEventListener;
michael@0 38
michael@0 39 gestureEvents.forEach(function (event) addRemove("Moz" + event, this, true),
michael@0 40 this);
michael@0 41 },
michael@0 42
michael@0 43 /**
michael@0 44 * Dispatch events based on the type of mouse gesture event. For now, make
michael@0 45 * sure to stop propagation of every gesture event so that web content cannot
michael@0 46 * receive gesture events.
michael@0 47 *
michael@0 48 * @param aEvent
michael@0 49 * The gesture event to handle
michael@0 50 */
michael@0 51 handleEvent: function GS_handleEvent(aEvent) {
michael@0 52 if (!Services.prefs.getBoolPref(
michael@0 53 "dom.debug.propagate_gesture_events_through_content")) {
michael@0 54 aEvent.stopPropagation();
michael@0 55 }
michael@0 56
michael@0 57 // Create a preference object with some defaults
michael@0 58 let def = function(aThreshold, aLatched)
michael@0 59 ({ threshold: aThreshold, latched: !!aLatched });
michael@0 60
michael@0 61 switch (aEvent.type) {
michael@0 62 case "MozSwipeGestureStart":
michael@0 63 if (this._setupSwipeGesture(aEvent)) {
michael@0 64 aEvent.preventDefault();
michael@0 65 }
michael@0 66 break;
michael@0 67 case "MozSwipeGestureUpdate":
michael@0 68 aEvent.preventDefault();
michael@0 69 this._doUpdate(aEvent);
michael@0 70 break;
michael@0 71 case "MozSwipeGestureEnd":
michael@0 72 aEvent.preventDefault();
michael@0 73 this._doEnd(aEvent);
michael@0 74 break;
michael@0 75 case "MozSwipeGesture":
michael@0 76 aEvent.preventDefault();
michael@0 77 this.onSwipe(aEvent);
michael@0 78 break;
michael@0 79 case "MozMagnifyGestureStart":
michael@0 80 aEvent.preventDefault();
michael@0 81 #ifdef XP_WIN
michael@0 82 this._setupGesture(aEvent, "pinch", def(25, 0), "out", "in");
michael@0 83 #else
michael@0 84 this._setupGesture(aEvent, "pinch", def(150, 1), "out", "in");
michael@0 85 #endif
michael@0 86 break;
michael@0 87 case "MozRotateGestureStart":
michael@0 88 aEvent.preventDefault();
michael@0 89 this._setupGesture(aEvent, "twist", def(25, 0), "right", "left");
michael@0 90 break;
michael@0 91 case "MozMagnifyGestureUpdate":
michael@0 92 case "MozRotateGestureUpdate":
michael@0 93 aEvent.preventDefault();
michael@0 94 this._doUpdate(aEvent);
michael@0 95 break;
michael@0 96 case "MozTapGesture":
michael@0 97 aEvent.preventDefault();
michael@0 98 this._doAction(aEvent, ["tap"]);
michael@0 99 break;
michael@0 100 case "MozRotateGesture":
michael@0 101 aEvent.preventDefault();
michael@0 102 this._doAction(aEvent, ["twist", "end"]);
michael@0 103 break;
michael@0 104 /* case "MozPressTapGesture":
michael@0 105 break; */
michael@0 106 }
michael@0 107 },
michael@0 108
michael@0 109 /**
michael@0 110 * Called at the start of "pinch" and "twist" gestures to setup all of the
michael@0 111 * information needed to process the gesture
michael@0 112 *
michael@0 113 * @param aEvent
michael@0 114 * The continual motion start event to handle
michael@0 115 * @param aGesture
michael@0 116 * Name of the gesture to handle
michael@0 117 * @param aPref
michael@0 118 * Preference object with the names of preferences and defaults
michael@0 119 * @param aInc
michael@0 120 * Command to trigger for increasing motion (without gesture name)
michael@0 121 * @param aDec
michael@0 122 * Command to trigger for decreasing motion (without gesture name)
michael@0 123 */
michael@0 124 _setupGesture: function GS__setupGesture(aEvent, aGesture, aPref, aInc, aDec) {
michael@0 125 // Try to load user-set values from preferences
michael@0 126 for (let [pref, def] in Iterator(aPref))
michael@0 127 aPref[pref] = this._getPref(aGesture + "." + pref, def);
michael@0 128
michael@0 129 // Keep track of the total deltas and latching behavior
michael@0 130 let offset = 0;
michael@0 131 let latchDir = aEvent.delta > 0 ? 1 : -1;
michael@0 132 let isLatched = false;
michael@0 133
michael@0 134 // Create the update function here to capture closure state
michael@0 135 this._doUpdate = function GS__doUpdate(aEvent) {
michael@0 136 // Update the offset with new event data
michael@0 137 offset += aEvent.delta;
michael@0 138
michael@0 139 // Check if the cumulative deltas exceed the threshold
michael@0 140 if (Math.abs(offset) > aPref["threshold"]) {
michael@0 141 // Trigger the action if we don't care about latching; otherwise, make
michael@0 142 // sure either we're not latched and going the same direction of the
michael@0 143 // initial motion; or we're latched and going the opposite way
michael@0 144 let sameDir = (latchDir ^ offset) >= 0;
michael@0 145 if (!aPref["latched"] || (isLatched ^ sameDir)) {
michael@0 146 this._doAction(aEvent, [aGesture, offset > 0 ? aInc : aDec]);
michael@0 147
michael@0 148 // We must be getting latched or leaving it, so just toggle
michael@0 149 isLatched = !isLatched;
michael@0 150 }
michael@0 151
michael@0 152 // Reset motion counter to prepare for more of the same gesture
michael@0 153 offset = 0;
michael@0 154 }
michael@0 155 };
michael@0 156
michael@0 157 // The start event also contains deltas, so handle an update right away
michael@0 158 this._doUpdate(aEvent);
michael@0 159 },
michael@0 160
michael@0 161 /**
michael@0 162 * Checks whether a swipe gesture event can navigate the browser history or
michael@0 163 * not.
michael@0 164 *
michael@0 165 * @param aEvent
michael@0 166 * The swipe gesture event.
michael@0 167 * @return true if the swipe event may navigate the history, false othwerwise.
michael@0 168 */
michael@0 169 _swipeNavigatesHistory: function GS__swipeNavigatesHistory(aEvent) {
michael@0 170 return this._getCommand(aEvent, ["swipe", "left"])
michael@0 171 == "Browser:BackOrBackDuplicate" &&
michael@0 172 this._getCommand(aEvent, ["swipe", "right"])
michael@0 173 == "Browser:ForwardOrForwardDuplicate";
michael@0 174 },
michael@0 175
michael@0 176 /**
michael@0 177 * Sets up swipe gestures. This includes setting up swipe animations for the
michael@0 178 * gesture, if enabled.
michael@0 179 *
michael@0 180 * @param aEvent
michael@0 181 * The swipe gesture start event.
michael@0 182 * @return true if swipe gestures could successfully be set up, false
michael@0 183 * othwerwise.
michael@0 184 */
michael@0 185 _setupSwipeGesture: function GS__setupSwipeGesture(aEvent) {
michael@0 186 if (!this._swipeNavigatesHistory(aEvent)) {
michael@0 187 return false;
michael@0 188 }
michael@0 189
michael@0 190 let isVerticalSwipe = false;
michael@0 191 if (aEvent.direction == aEvent.DIRECTION_UP) {
michael@0 192 if (content.pageYOffset > 0) {
michael@0 193 return false;
michael@0 194 }
michael@0 195 isVerticalSwipe = true;
michael@0 196 } else if (aEvent.direction == aEvent.DIRECTION_DOWN) {
michael@0 197 if (content.pageYOffset < content.scrollMaxY) {
michael@0 198 return false;
michael@0 199 }
michael@0 200 isVerticalSwipe = true;
michael@0 201 }
michael@0 202 if (isVerticalSwipe) {
michael@0 203 // Vertical overscroll has been temporarily disabled until bug 939480 is
michael@0 204 // fixed.
michael@0 205 return false;
michael@0 206 }
michael@0 207
michael@0 208 let canGoBack = gHistorySwipeAnimation.canGoBack();
michael@0 209 let canGoForward = gHistorySwipeAnimation.canGoForward();
michael@0 210 let isLTR = gHistorySwipeAnimation.isLTR;
michael@0 211
michael@0 212 if (canGoBack) {
michael@0 213 aEvent.allowedDirections |= isLTR ? aEvent.DIRECTION_LEFT :
michael@0 214 aEvent.DIRECTION_RIGHT;
michael@0 215 }
michael@0 216 if (canGoForward) {
michael@0 217 aEvent.allowedDirections |= isLTR ? aEvent.DIRECTION_RIGHT :
michael@0 218 aEvent.DIRECTION_LEFT;
michael@0 219 }
michael@0 220
michael@0 221 gHistorySwipeAnimation.startAnimation(isVerticalSwipe);
michael@0 222
michael@0 223 this._doUpdate = function GS__doUpdate(aEvent) {
michael@0 224 gHistorySwipeAnimation.updateAnimation(aEvent.delta);
michael@0 225 };
michael@0 226
michael@0 227 this._doEnd = function GS__doEnd(aEvent) {
michael@0 228 gHistorySwipeAnimation.swipeEndEventReceived();
michael@0 229
michael@0 230 this._doUpdate = function (aEvent) {};
michael@0 231 this._doEnd = function (aEvent) {};
michael@0 232 }
michael@0 233
michael@0 234 return true;
michael@0 235 },
michael@0 236
michael@0 237 /**
michael@0 238 * Generator producing the powerset of the input array where the first result
michael@0 239 * is the complete set and the last result (before StopIteration) is empty.
michael@0 240 *
michael@0 241 * @param aArray
michael@0 242 * Source array containing any number of elements
michael@0 243 * @yield Array that is a subset of the input array from full set to empty
michael@0 244 */
michael@0 245 _power: function GS__power(aArray) {
michael@0 246 // Create a bitmask based on the length of the array
michael@0 247 let num = 1 << aArray.length;
michael@0 248 while (--num >= 0) {
michael@0 249 // Only select array elements where the current bit is set
michael@0 250 yield aArray.reduce(function (aPrev, aCurr, aIndex) {
michael@0 251 if (num & 1 << aIndex)
michael@0 252 aPrev.push(aCurr);
michael@0 253 return aPrev;
michael@0 254 }, []);
michael@0 255 }
michael@0 256 },
michael@0 257
michael@0 258 /**
michael@0 259 * Determine what action to do for the gesture based on which keys are
michael@0 260 * pressed and which commands are set, and execute the command.
michael@0 261 *
michael@0 262 * @param aEvent
michael@0 263 * The original gesture event to convert into a fake click event
michael@0 264 * @param aGesture
michael@0 265 * Array of gesture name parts (to be joined by periods)
michael@0 266 * @return Name of the executed command. Returns null if no command is
michael@0 267 * found.
michael@0 268 */
michael@0 269 _doAction: function GS__doAction(aEvent, aGesture) {
michael@0 270 let command = this._getCommand(aEvent, aGesture);
michael@0 271 return command && this._doCommand(aEvent, command);
michael@0 272 },
michael@0 273
michael@0 274 /**
michael@0 275 * Determine what action to do for the gesture based on which keys are
michael@0 276 * pressed and which commands are set
michael@0 277 *
michael@0 278 * @param aEvent
michael@0 279 * The original gesture event to convert into a fake click event
michael@0 280 * @param aGesture
michael@0 281 * Array of gesture name parts (to be joined by periods)
michael@0 282 */
michael@0 283 _getCommand: function GS__getCommand(aEvent, aGesture) {
michael@0 284 // Create an array of pressed keys in a fixed order so that a command for
michael@0 285 // "meta" is preferred over "ctrl" when both buttons are pressed (and a
michael@0 286 // command for both don't exist)
michael@0 287 let keyCombos = [];
michael@0 288 ["shift", "alt", "ctrl", "meta"].forEach(function (key) {
michael@0 289 if (aEvent[key + "Key"])
michael@0 290 keyCombos.push(key);
michael@0 291 });
michael@0 292
michael@0 293 // Try each combination of key presses in decreasing order for commands
michael@0 294 for (let subCombo of this._power(keyCombos)) {
michael@0 295 // Convert a gesture and pressed keys into the corresponding command
michael@0 296 // action where the preference has the gesture before "shift" before
michael@0 297 // "alt" before "ctrl" before "meta" all separated by periods
michael@0 298 let command;
michael@0 299 try {
michael@0 300 command = this._getPref(aGesture.concat(subCombo).join("."));
michael@0 301 } catch (e) {}
michael@0 302
michael@0 303 if (command)
michael@0 304 return command;
michael@0 305 }
michael@0 306 return null;
michael@0 307 },
michael@0 308
michael@0 309 /**
michael@0 310 * Execute the specified command.
michael@0 311 *
michael@0 312 * @param aEvent
michael@0 313 * The original gesture event to convert into a fake click event
michael@0 314 * @param aCommand
michael@0 315 * Name of the command found for the event's keys and gesture.
michael@0 316 */
michael@0 317 _doCommand: function GS__doCommand(aEvent, aCommand) {
michael@0 318 let node = document.getElementById(aCommand);
michael@0 319 if (node) {
michael@0 320 if (node.getAttribute("disabled") != "true") {
michael@0 321 let cmdEvent = document.createEvent("xulcommandevent");
michael@0 322 cmdEvent.initCommandEvent("command", true, true, window, 0,
michael@0 323 aEvent.ctrlKey, aEvent.altKey,
michael@0 324 aEvent.shiftKey, aEvent.metaKey, aEvent);
michael@0 325 node.dispatchEvent(cmdEvent);
michael@0 326 }
michael@0 327
michael@0 328 }
michael@0 329 else {
michael@0 330 goDoCommand(aCommand);
michael@0 331 }
michael@0 332 },
michael@0 333
michael@0 334 /**
michael@0 335 * Handle continual motion events. This function will be set by
michael@0 336 * _setupGesture or _setupSwipe.
michael@0 337 *
michael@0 338 * @param aEvent
michael@0 339 * The continual motion update event to handle
michael@0 340 */
michael@0 341 _doUpdate: function(aEvent) {},
michael@0 342
michael@0 343 /**
michael@0 344 * Handle gesture end events. This function will be set by _setupSwipe.
michael@0 345 *
michael@0 346 * @param aEvent
michael@0 347 * The gesture end event to handle
michael@0 348 */
michael@0 349 _doEnd: function(aEvent) {},
michael@0 350
michael@0 351 /**
michael@0 352 * Convert the swipe gesture into a browser action based on the direction.
michael@0 353 *
michael@0 354 * @param aEvent
michael@0 355 * The swipe event to handle
michael@0 356 */
michael@0 357 onSwipe: function GS_onSwipe(aEvent) {
michael@0 358 // Figure out which one (and only one) direction was triggered
michael@0 359 for (let dir of ["UP", "RIGHT", "DOWN", "LEFT"]) {
michael@0 360 if (aEvent.direction == aEvent["DIRECTION_" + dir]) {
michael@0 361 this._coordinateSwipeEventWithAnimation(aEvent, dir);
michael@0 362 break;
michael@0 363 }
michael@0 364 }
michael@0 365 },
michael@0 366
michael@0 367 /**
michael@0 368 * Process a swipe event based on the given direction.
michael@0 369 *
michael@0 370 * @param aEvent
michael@0 371 * The swipe event to handle
michael@0 372 * @param aDir
michael@0 373 * The direction for the swipe event
michael@0 374 */
michael@0 375 processSwipeEvent: function GS_processSwipeEvent(aEvent, aDir) {
michael@0 376 this._doAction(aEvent, ["swipe", aDir.toLowerCase()]);
michael@0 377 },
michael@0 378
michael@0 379 /**
michael@0 380 * Coordinates the swipe event with the swipe animation, if any.
michael@0 381 * If an animation is currently running, the swipe event will be
michael@0 382 * processed once the animation stops. This will guarantee a fluid
michael@0 383 * motion of the animation.
michael@0 384 *
michael@0 385 * @param aEvent
michael@0 386 * The swipe event to handle
michael@0 387 * @param aDir
michael@0 388 * The direction for the swipe event
michael@0 389 */
michael@0 390 _coordinateSwipeEventWithAnimation:
michael@0 391 function GS__coordinateSwipeEventWithAnimation(aEvent, aDir) {
michael@0 392 if ((gHistorySwipeAnimation.isAnimationRunning()) &&
michael@0 393 (aDir == "RIGHT" || aDir == "LEFT")) {
michael@0 394 gHistorySwipeAnimation.processSwipeEvent(aEvent, aDir);
michael@0 395 }
michael@0 396 else {
michael@0 397 this.processSwipeEvent(aEvent, aDir);
michael@0 398 }
michael@0 399 },
michael@0 400
michael@0 401 /**
michael@0 402 * Get a gesture preference or use a default if it doesn't exist
michael@0 403 *
michael@0 404 * @param aPref
michael@0 405 * Name of the preference to load under the gesture branch
michael@0 406 * @param aDef
michael@0 407 * Default value if the preference doesn't exist
michael@0 408 */
michael@0 409 _getPref: function GS__getPref(aPref, aDef) {
michael@0 410 // Preferences branch under which all gestures preferences are stored
michael@0 411 const branch = "browser.gesture.";
michael@0 412
michael@0 413 try {
michael@0 414 // Determine what type of data to load based on default value's type
michael@0 415 let type = typeof aDef;
michael@0 416 let getFunc = "get" + (type == "boolean" ? "Bool" :
michael@0 417 type == "number" ? "Int" : "Char") + "Pref";
michael@0 418 return gPrefService[getFunc](branch + aPref);
michael@0 419 }
michael@0 420 catch (e) {
michael@0 421 return aDef;
michael@0 422 }
michael@0 423 },
michael@0 424
michael@0 425 /**
michael@0 426 * Perform rotation for ImageDocuments
michael@0 427 *
michael@0 428 * @param aEvent
michael@0 429 * The MozRotateGestureUpdate event triggering this call
michael@0 430 */
michael@0 431 rotate: function(aEvent) {
michael@0 432 if (!(content.document instanceof ImageDocument))
michael@0 433 return;
michael@0 434
michael@0 435 let contentElement = content.document.body.firstElementChild;
michael@0 436 if (!contentElement)
michael@0 437 return;
michael@0 438 // If we're currently snapping, cancel that snap
michael@0 439 if (contentElement.classList.contains("completeRotation"))
michael@0 440 this._clearCompleteRotation();
michael@0 441
michael@0 442 this.rotation = Math.round(this.rotation + aEvent.delta);
michael@0 443 contentElement.style.transform = "rotate(" + this.rotation + "deg)";
michael@0 444 this._lastRotateDelta = aEvent.delta;
michael@0 445 },
michael@0 446
michael@0 447 /**
michael@0 448 * Perform a rotation end for ImageDocuments
michael@0 449 */
michael@0 450 rotateEnd: function() {
michael@0 451 if (!(content.document instanceof ImageDocument))
michael@0 452 return;
michael@0 453
michael@0 454 let contentElement = content.document.body.firstElementChild;
michael@0 455 if (!contentElement)
michael@0 456 return;
michael@0 457
michael@0 458 let transitionRotation = 0;
michael@0 459
michael@0 460 // The reason that 360 is allowed here is because when rotating between
michael@0 461 // 315 and 360, setting rotate(0deg) will cause it to rotate the wrong
michael@0 462 // direction around--spinning wildly.
michael@0 463 if (this.rotation <= 45)
michael@0 464 transitionRotation = 0;
michael@0 465 else if (this.rotation > 45 && this.rotation <= 135)
michael@0 466 transitionRotation = 90;
michael@0 467 else if (this.rotation > 135 && this.rotation <= 225)
michael@0 468 transitionRotation = 180;
michael@0 469 else if (this.rotation > 225 && this.rotation <= 315)
michael@0 470 transitionRotation = 270;
michael@0 471 else
michael@0 472 transitionRotation = 360;
michael@0 473
michael@0 474 // If we're going fast enough, and we didn't already snap ahead of rotation,
michael@0 475 // then snap ahead of rotation to simulate momentum
michael@0 476 if (this._lastRotateDelta > this._rotateMomentumThreshold &&
michael@0 477 this.rotation > transitionRotation)
michael@0 478 transitionRotation += 90;
michael@0 479 else if (this._lastRotateDelta < -1 * this._rotateMomentumThreshold &&
michael@0 480 this.rotation < transitionRotation)
michael@0 481 transitionRotation -= 90;
michael@0 482
michael@0 483 // Only add the completeRotation class if it is is necessary
michael@0 484 if (transitionRotation != this.rotation) {
michael@0 485 contentElement.classList.add("completeRotation");
michael@0 486 contentElement.addEventListener("transitionend", this._clearCompleteRotation);
michael@0 487 }
michael@0 488
michael@0 489 contentElement.style.transform = "rotate(" + transitionRotation + "deg)";
michael@0 490 this.rotation = transitionRotation;
michael@0 491 },
michael@0 492
michael@0 493 /**
michael@0 494 * Gets the current rotation for the ImageDocument
michael@0 495 */
michael@0 496 get rotation() {
michael@0 497 return this._currentRotation;
michael@0 498 },
michael@0 499
michael@0 500 /**
michael@0 501 * Sets the current rotation for the ImageDocument
michael@0 502 *
michael@0 503 * @param aVal
michael@0 504 * The new value to take. Can be any value, but it will be bounded to
michael@0 505 * 0 inclusive to 360 exclusive.
michael@0 506 */
michael@0 507 set rotation(aVal) {
michael@0 508 this._currentRotation = aVal % 360;
michael@0 509 if (this._currentRotation < 0)
michael@0 510 this._currentRotation += 360;
michael@0 511 return this._currentRotation;
michael@0 512 },
michael@0 513
michael@0 514 /**
michael@0 515 * When the location/tab changes, need to reload the current rotation for the
michael@0 516 * image
michael@0 517 */
michael@0 518 restoreRotationState: function() {
michael@0 519 // Bug 863514 - Make gesture support work in electrolysis
michael@0 520 if (gMultiProcessBrowser)
michael@0 521 return;
michael@0 522
michael@0 523 if (!(content.document instanceof ImageDocument))
michael@0 524 return;
michael@0 525
michael@0 526 let contentElement = content.document.body.firstElementChild;
michael@0 527 let transformValue = content.window.getComputedStyle(contentElement, null)
michael@0 528 .transform;
michael@0 529
michael@0 530 if (transformValue == "none") {
michael@0 531 this.rotation = 0;
michael@0 532 return;
michael@0 533 }
michael@0 534
michael@0 535 // transformValue is a rotation matrix--split it and do mathemagic to
michael@0 536 // obtain the real rotation value
michael@0 537 transformValue = transformValue.split("(")[1]
michael@0 538 .split(")")[0]
michael@0 539 .split(",");
michael@0 540 this.rotation = Math.round(Math.atan2(transformValue[1], transformValue[0]) *
michael@0 541 (180 / Math.PI));
michael@0 542 },
michael@0 543
michael@0 544 /**
michael@0 545 * Removes the transition rule by removing the completeRotation class
michael@0 546 */
michael@0 547 _clearCompleteRotation: function() {
michael@0 548 let contentElement = content.document &&
michael@0 549 content.document instanceof ImageDocument &&
michael@0 550 content.document.body &&
michael@0 551 content.document.body.firstElementChild;
michael@0 552 if (!contentElement)
michael@0 553 return;
michael@0 554 contentElement.classList.remove("completeRotation");
michael@0 555 contentElement.removeEventListener("transitionend", this._clearCompleteRotation);
michael@0 556 },
michael@0 557 };
michael@0 558
michael@0 559 // History Swipe Animation Support (bug 678392)
michael@0 560 let gHistorySwipeAnimation = {
michael@0 561
michael@0 562 active: false,
michael@0 563 isLTR: false,
michael@0 564
michael@0 565 /**
michael@0 566 * Initializes the support for history swipe animations, if it is supported
michael@0 567 * by the platform/configuration.
michael@0 568 */
michael@0 569 init: function HSA_init() {
michael@0 570 if (!this._isSupported())
michael@0 571 return;
michael@0 572
michael@0 573 this.active = false;
michael@0 574 this.isLTR = document.documentElement.mozMatchesSelector(
michael@0 575 ":-moz-locale-dir(ltr)");
michael@0 576 this._trackedSnapshots = [];
michael@0 577 this._startingIndex = -1;
michael@0 578 this._historyIndex = -1;
michael@0 579 this._boxWidth = -1;
michael@0 580 this._boxHeight = -1;
michael@0 581 this._maxSnapshots = this._getMaxSnapshots();
michael@0 582 this._lastSwipeDir = "";
michael@0 583 this._direction = "horizontal";
michael@0 584
michael@0 585 // We only want to activate history swipe animations if we store snapshots.
michael@0 586 // If we don't store any, we handle horizontal swipes without animations.
michael@0 587 if (this._maxSnapshots > 0) {
michael@0 588 this.active = true;
michael@0 589 gBrowser.addEventListener("pagehide", this, false);
michael@0 590 gBrowser.addEventListener("pageshow", this, false);
michael@0 591 gBrowser.addEventListener("popstate", this, false);
michael@0 592 gBrowser.addEventListener("DOMModalDialogClosed", this, false);
michael@0 593 gBrowser.tabContainer.addEventListener("TabClose", this, false);
michael@0 594 }
michael@0 595 },
michael@0 596
michael@0 597 /**
michael@0 598 * Uninitializes the support for history swipe animations.
michael@0 599 */
michael@0 600 uninit: function HSA_uninit() {
michael@0 601 gBrowser.removeEventListener("pagehide", this, false);
michael@0 602 gBrowser.removeEventListener("pageshow", this, false);
michael@0 603 gBrowser.removeEventListener("popstate", this, false);
michael@0 604 gBrowser.removeEventListener("DOMModalDialogClosed", this, false);
michael@0 605 gBrowser.tabContainer.removeEventListener("TabClose", this, false);
michael@0 606
michael@0 607 this.active = false;
michael@0 608 this.isLTR = false;
michael@0 609 },
michael@0 610
michael@0 611 /**
michael@0 612 * Starts the swipe animation and handles fast swiping (i.e. a swipe animation
michael@0 613 * is already in progress when a new one is initiated).
michael@0 614 *
michael@0 615 * @param aIsVerticalSwipe
michael@0 616 * Whether we're dealing with a vertical swipe or not.
michael@0 617 */
michael@0 618 startAnimation: function HSA_startAnimation(aIsVerticalSwipe) {
michael@0 619 this._direction = aIsVerticalSwipe ? "vertical" : "horizontal";
michael@0 620
michael@0 621 if (this.isAnimationRunning()) {
michael@0 622 // If this is a horizontal scroll, or if this is a vertical scroll that
michael@0 623 // was started while a horizontal scroll was still running, handle it as
michael@0 624 // as a fast swipe. In the case of the latter scenario, this allows us to
michael@0 625 // start the vertical animation without first loading the final page, or
michael@0 626 // taking another snapshot. If vertical scrolls are initiated repeatedly
michael@0 627 // without prior horizontal scroll we skip this and restart the animation
michael@0 628 // from 0.
michael@0 629 if (this._direction == "horizontal" || this._lastSwipeDir != "") {
michael@0 630 gBrowser.stop();
michael@0 631 this._lastSwipeDir = "RELOAD"; // just ensure that != ""
michael@0 632 this._canGoBack = this.canGoBack();
michael@0 633 this._canGoForward = this.canGoForward();
michael@0 634 this._handleFastSwiping();
michael@0 635 }
michael@0 636 }
michael@0 637 else {
michael@0 638 this._startingIndex = gBrowser.webNavigation.sessionHistory.index;
michael@0 639 this._historyIndex = this._startingIndex;
michael@0 640 this._canGoBack = this.canGoBack();
michael@0 641 this._canGoForward = this.canGoForward();
michael@0 642 if (this.active) {
michael@0 643 this._addBoxes();
michael@0 644 this._takeSnapshot();
michael@0 645 this._installPrevAndNextSnapshots();
michael@0 646 this._lastSwipeDir = "";
michael@0 647 }
michael@0 648 }
michael@0 649 this.updateAnimation(0);
michael@0 650 },
michael@0 651
michael@0 652 /**
michael@0 653 * Stops the swipe animation.
michael@0 654 */
michael@0 655 stopAnimation: function HSA_stopAnimation() {
michael@0 656 gHistorySwipeAnimation._removeBoxes();
michael@0 657 this._historyIndex = gBrowser.webNavigation.sessionHistory.index;
michael@0 658 },
michael@0 659
michael@0 660 /**
michael@0 661 * Updates the animation between two pages in history.
michael@0 662 *
michael@0 663 * @param aVal
michael@0 664 * A floating point value that represents the progress of the
michael@0 665 * swipe gesture.
michael@0 666 */
michael@0 667 updateAnimation: function HSA_updateAnimation(aVal) {
michael@0 668 if (!this.isAnimationRunning()) {
michael@0 669 return;
michael@0 670 }
michael@0 671
michael@0 672 // We use the following value to decrease the bounce effect when scrolling
michael@0 673 // to the top or bottom of the page, or when swiping back/forward past the
michael@0 674 // browsing history. This value was determined experimentally.
michael@0 675 let dampValue = 4;
michael@0 676 if (this._direction == "vertical") {
michael@0 677 this._prevBox.collapsed = true;
michael@0 678 this._nextBox.collapsed = true;
michael@0 679 this._positionBox(this._curBox, -1 * aVal / dampValue);
michael@0 680 } else if ((aVal >= 0 && this.isLTR) ||
michael@0 681 (aVal <= 0 && !this.isLTR)) {
michael@0 682 let tempDampValue = 1;
michael@0 683 if (this._canGoBack) {
michael@0 684 this._prevBox.collapsed = false;
michael@0 685 } else {
michael@0 686 tempDampValue = dampValue;
michael@0 687 this._prevBox.collapsed = true;
michael@0 688 }
michael@0 689
michael@0 690 // The current page is pushed to the right (LTR) or left (RTL),
michael@0 691 // the intention is to go back.
michael@0 692 // If there is a page to go back to, it should show in the background.
michael@0 693 this._positionBox(this._curBox, aVal / tempDampValue);
michael@0 694
michael@0 695 // The forward page should be pushed offscreen all the way to the right.
michael@0 696 this._positionBox(this._nextBox, 1);
michael@0 697 } else {
michael@0 698 // The intention is to go forward. If there is a page to go forward to,
michael@0 699 // it should slide in from the right (LTR) or left (RTL).
michael@0 700 // Otherwise, the current page should slide to the left (LTR) or
michael@0 701 // right (RTL) and the backdrop should appear in the background.
michael@0 702 // For the backdrop to be visible in that case, the previous page needs
michael@0 703 // to be hidden (if it exists).
michael@0 704 if (this._canGoForward) {
michael@0 705 this._nextBox.collapsed = false;
michael@0 706 let offset = this.isLTR ? 1 : -1;
michael@0 707 this._positionBox(this._curBox, 0);
michael@0 708 this._positionBox(this._nextBox, offset + aVal);
michael@0 709 } else {
michael@0 710 this._prevBox.collapsed = true;
michael@0 711 this._positionBox(this._curBox, aVal / dampValue);
michael@0 712 }
michael@0 713 }
michael@0 714 },
michael@0 715
michael@0 716 /**
michael@0 717 * Event handler for events relevant to the history swipe animation.
michael@0 718 *
michael@0 719 * @param aEvent
michael@0 720 * An event to process.
michael@0 721 */
michael@0 722 handleEvent: function HSA_handleEvent(aEvent) {
michael@0 723 let browser = gBrowser.selectedBrowser;
michael@0 724 switch (aEvent.type) {
michael@0 725 case "TabClose":
michael@0 726 let browserForTab = gBrowser.getBrowserForTab(aEvent.target);
michael@0 727 this._removeTrackedSnapshot(-1, browserForTab);
michael@0 728 break;
michael@0 729 case "DOMModalDialogClosed":
michael@0 730 this.stopAnimation();
michael@0 731 break;
michael@0 732 case "pageshow":
michael@0 733 if (aEvent.target == browser.contentDocument) {
michael@0 734 this.stopAnimation();
michael@0 735 }
michael@0 736 break;
michael@0 737 case "popstate":
michael@0 738 if (aEvent.target == browser.contentDocument.defaultView) {
michael@0 739 this.stopAnimation();
michael@0 740 }
michael@0 741 break;
michael@0 742 case "pagehide":
michael@0 743 if (aEvent.target == browser.contentDocument) {
michael@0 744 // Take and compress a snapshot of a page whenever it's about to be
michael@0 745 // navigated away from. We already have a snapshot of the page if an
michael@0 746 // animation is running, so we're left with compressing it.
michael@0 747 if (!this.isAnimationRunning()) {
michael@0 748 this._takeSnapshot();
michael@0 749 }
michael@0 750 this._compressSnapshotAtCurrentIndex();
michael@0 751 }
michael@0 752 break;
michael@0 753 }
michael@0 754 },
michael@0 755
michael@0 756 /**
michael@0 757 * Checks whether the history swipe animation is currently running or not.
michael@0 758 *
michael@0 759 * @return true if the animation is currently running, false otherwise.
michael@0 760 */
michael@0 761 isAnimationRunning: function HSA_isAnimationRunning() {
michael@0 762 return !!this._container;
michael@0 763 },
michael@0 764
michael@0 765 /**
michael@0 766 * Process a swipe event based on the given direction.
michael@0 767 *
michael@0 768 * @param aEvent
michael@0 769 * The swipe event to handle
michael@0 770 * @param aDir
michael@0 771 * The direction for the swipe event
michael@0 772 */
michael@0 773 processSwipeEvent: function HSA_processSwipeEvent(aEvent, aDir) {
michael@0 774 if (aDir == "RIGHT")
michael@0 775 this._historyIndex += this.isLTR ? 1 : -1;
michael@0 776 else if (aDir == "LEFT")
michael@0 777 this._historyIndex += this.isLTR ? -1 : 1;
michael@0 778 else
michael@0 779 return;
michael@0 780 this._lastSwipeDir = aDir;
michael@0 781 },
michael@0 782
michael@0 783 /**
michael@0 784 * Checks if there is a page in the browser history to go back to.
michael@0 785 *
michael@0 786 * @return true if there is a previous page in history, false otherwise.
michael@0 787 */
michael@0 788 canGoBack: function HSA_canGoBack() {
michael@0 789 if (this.isAnimationRunning())
michael@0 790 return this._doesIndexExistInHistory(this._historyIndex - 1);
michael@0 791 return gBrowser.webNavigation.canGoBack;
michael@0 792 },
michael@0 793
michael@0 794 /**
michael@0 795 * Checks if there is a page in the browser history to go forward to.
michael@0 796 *
michael@0 797 * @return true if there is a next page in history, false otherwise.
michael@0 798 */
michael@0 799 canGoForward: function HSA_canGoForward() {
michael@0 800 if (this.isAnimationRunning())
michael@0 801 return this._doesIndexExistInHistory(this._historyIndex + 1);
michael@0 802 return gBrowser.webNavigation.canGoForward;
michael@0 803 },
michael@0 804
michael@0 805 /**
michael@0 806 * Used to notify the history swipe animation that the OS sent a swipe end
michael@0 807 * event and that we should navigate to the page that the user swiped to, if
michael@0 808 * any. This will also result in the animation overlay to be torn down.
michael@0 809 */
michael@0 810 swipeEndEventReceived: function HSA_swipeEndEventReceived() {
michael@0 811 if (this._lastSwipeDir != "" && this._historyIndex != this._startingIndex)
michael@0 812 this._navigateToHistoryIndex();
michael@0 813 else
michael@0 814 this.stopAnimation();
michael@0 815 },
michael@0 816
michael@0 817 /**
michael@0 818 * Checks whether a particular index exists in the browser history or not.
michael@0 819 *
michael@0 820 * @param aIndex
michael@0 821 * The index to check for availability for in the history.
michael@0 822 * @return true if the index exists in the browser history, false otherwise.
michael@0 823 */
michael@0 824 _doesIndexExistInHistory: function HSA__doesIndexExistInHistory(aIndex) {
michael@0 825 try {
michael@0 826 gBrowser.webNavigation.sessionHistory.getEntryAtIndex(aIndex, false);
michael@0 827 }
michael@0 828 catch(ex) {
michael@0 829 return false;
michael@0 830 }
michael@0 831 return true;
michael@0 832 },
michael@0 833
michael@0 834 /**
michael@0 835 * Navigates to the index in history that is currently being tracked by
michael@0 836 * |this|.
michael@0 837 */
michael@0 838 _navigateToHistoryIndex: function HSA__navigateToHistoryIndex() {
michael@0 839 if (this._doesIndexExistInHistory(this._historyIndex))
michael@0 840 gBrowser.webNavigation.gotoIndex(this._historyIndex);
michael@0 841 else
michael@0 842 this.stopAnimation();
michael@0 843 },
michael@0 844
michael@0 845 /**
michael@0 846 * Checks to see if history swipe animations are supported by this
michael@0 847 * platform/configuration.
michael@0 848 *
michael@0 849 * return true if supported, false otherwise.
michael@0 850 */
michael@0 851 _isSupported: function HSA__isSupported() {
michael@0 852 return window.matchMedia("(-moz-swipe-animation-enabled)").matches;
michael@0 853 },
michael@0 854
michael@0 855 /**
michael@0 856 * Handle fast swiping (i.e. a swipe animation is already in
michael@0 857 * progress when a new one is initiated). This will swap out the snapshots
michael@0 858 * used in the previous animation with the appropriate new ones.
michael@0 859 */
michael@0 860 _handleFastSwiping: function HSA__handleFastSwiping() {
michael@0 861 this._installCurrentPageSnapshot(null);
michael@0 862 this._installPrevAndNextSnapshots();
michael@0 863 },
michael@0 864
michael@0 865 /**
michael@0 866 * Adds the boxes that contain the snapshots used during the swipe animation.
michael@0 867 */
michael@0 868 _addBoxes: function HSA__addBoxes() {
michael@0 869 let browserStack =
michael@0 870 document.getAnonymousElementByAttribute(gBrowser.getNotificationBox(),
michael@0 871 "class", "browserStack");
michael@0 872 this._container = this._createElement("historySwipeAnimationContainer",
michael@0 873 "stack");
michael@0 874 browserStack.appendChild(this._container);
michael@0 875
michael@0 876 this._prevBox = this._createElement("historySwipeAnimationPreviousPage",
michael@0 877 "box");
michael@0 878 this._container.appendChild(this._prevBox);
michael@0 879
michael@0 880 this._curBox = this._createElement("historySwipeAnimationCurrentPage",
michael@0 881 "box");
michael@0 882 this._container.appendChild(this._curBox);
michael@0 883
michael@0 884 this._nextBox = this._createElement("historySwipeAnimationNextPage",
michael@0 885 "box");
michael@0 886 this._container.appendChild(this._nextBox);
michael@0 887
michael@0 888 // Cache width and height.
michael@0 889 this._boxWidth = this._curBox.getBoundingClientRect().width;
michael@0 890 this._boxHeight = this._curBox.getBoundingClientRect().height;
michael@0 891 },
michael@0 892
michael@0 893 /**
michael@0 894 * Removes the boxes.
michael@0 895 */
michael@0 896 _removeBoxes: function HSA__removeBoxes() {
michael@0 897 this._curBox = null;
michael@0 898 this._prevBox = null;
michael@0 899 this._nextBox = null;
michael@0 900 if (this._container)
michael@0 901 this._container.parentNode.removeChild(this._container);
michael@0 902 this._container = null;
michael@0 903 this._boxWidth = -1;
michael@0 904 this._boxHeight = -1;
michael@0 905 },
michael@0 906
michael@0 907 /**
michael@0 908 * Creates an element with a given identifier and tag name.
michael@0 909 *
michael@0 910 * @param aID
michael@0 911 * An identifier to create the element with.
michael@0 912 * @param aTagName
michael@0 913 * The name of the tag to create the element for.
michael@0 914 * @return the newly created element.
michael@0 915 */
michael@0 916 _createElement: function HSA__createElement(aID, aTagName) {
michael@0 917 let XULNS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
michael@0 918 let element = document.createElementNS(XULNS, aTagName);
michael@0 919 element.id = aID;
michael@0 920 return element;
michael@0 921 },
michael@0 922
michael@0 923 /**
michael@0 924 * Moves a given box to a given X coordinate position.
michael@0 925 *
michael@0 926 * @param aBox
michael@0 927 * The box element to position.
michael@0 928 * @param aPosition
michael@0 929 * The position (in X coordinates) to move the box element to.
michael@0 930 */
michael@0 931 _positionBox: function HSA__positionBox(aBox, aPosition) {
michael@0 932 let transform = "";
michael@0 933
michael@0 934 if (this._direction == "vertical")
michael@0 935 transform = "translateY(" + this._boxHeight * aPosition + "px)";
michael@0 936 else
michael@0 937 transform = "translateX(" + this._boxWidth * aPosition + "px)";
michael@0 938
michael@0 939 aBox.style.transform = transform;
michael@0 940 },
michael@0 941
michael@0 942 /**
michael@0 943 * Verifies that we're ready to take snapshots based on the global pref and
michael@0 944 * the current index in history.
michael@0 945 *
michael@0 946 * @return true if we're ready to take snapshots, false otherwise.
michael@0 947 */
michael@0 948 _readyToTakeSnapshots: function HSA__readyToTakeSnapshots() {
michael@0 949 if ((this._maxSnapshots < 1) ||
michael@0 950 (gBrowser.webNavigation.sessionHistory.index < 0)) {
michael@0 951 return false;
michael@0 952 }
michael@0 953 return true;
michael@0 954 },
michael@0 955
michael@0 956 /**
michael@0 957 * Takes a snapshot of the page the browser is currently on.
michael@0 958 */
michael@0 959 _takeSnapshot: function HSA__takeSnapshot() {
michael@0 960 if (!this._readyToTakeSnapshots()) {
michael@0 961 return;
michael@0 962 }
michael@0 963
michael@0 964 let canvas = null;
michael@0 965
michael@0 966 TelemetryStopwatch.start("FX_GESTURE_TAKE_SNAPSHOT_OF_PAGE");
michael@0 967 try {
michael@0 968 let browser = gBrowser.selectedBrowser;
michael@0 969 let r = browser.getBoundingClientRect();
michael@0 970 canvas = document.createElementNS("http://www.w3.org/1999/xhtml",
michael@0 971 "canvas");
michael@0 972 canvas.mozOpaque = true;
michael@0 973 let scale = window.devicePixelRatio;
michael@0 974 canvas.width = r.width * scale;
michael@0 975 canvas.height = r.height * scale;
michael@0 976 let ctx = canvas.getContext("2d");
michael@0 977 let zoom = browser.markupDocumentViewer.fullZoom * scale;
michael@0 978 ctx.scale(zoom, zoom);
michael@0 979 ctx.drawWindow(browser.contentWindow,
michael@0 980 0, 0, canvas.width / zoom, canvas.height / zoom, "white",
michael@0 981 ctx.DRAWWINDOW_DO_NOT_FLUSH | ctx.DRAWWINDOW_DRAW_VIEW |
michael@0 982 ctx.DRAWWINDOW_ASYNC_DECODE_IMAGES |
michael@0 983 ctx.DRAWWINDOW_USE_WIDGET_LAYERS);
michael@0 984 } finally {
michael@0 985 TelemetryStopwatch.finish("FX_GESTURE_TAKE_SNAPSHOT_OF_PAGE");
michael@0 986 }
michael@0 987
michael@0 988 TelemetryStopwatch.start("FX_GESTURE_INSTALL_SNAPSHOT_OF_PAGE");
michael@0 989 try {
michael@0 990 this._installCurrentPageSnapshot(canvas);
michael@0 991 this._assignSnapshotToCurrentBrowser(canvas);
michael@0 992 } finally {
michael@0 993 TelemetryStopwatch.finish("FX_GESTURE_INSTALL_SNAPSHOT_OF_PAGE");
michael@0 994 }
michael@0 995 },
michael@0 996
michael@0 997 /**
michael@0 998 * Retrieves the maximum number of snapshots that should be kept in memory.
michael@0 999 * This limit is a global limit and is valid across all open tabs.
michael@0 1000 */
michael@0 1001 _getMaxSnapshots: function HSA__getMaxSnapshots() {
michael@0 1002 return gPrefService.getIntPref("browser.snapshots.limit");
michael@0 1003 },
michael@0 1004
michael@0 1005 /**
michael@0 1006 * Adds a snapshot to the list and initiates the compression of said snapshot.
michael@0 1007 * Once the compression is completed, it will replace the uncompressed
michael@0 1008 * snapshot in the list.
michael@0 1009 *
michael@0 1010 * @param aCanvas
michael@0 1011 * The snapshot to add to the list and compress.
michael@0 1012 */
michael@0 1013 _assignSnapshotToCurrentBrowser:
michael@0 1014 function HSA__assignSnapshotToCurrentBrowser(aCanvas) {
michael@0 1015 let browser = gBrowser.selectedBrowser;
michael@0 1016 let currIndex = browser.webNavigation.sessionHistory.index;
michael@0 1017
michael@0 1018 this._removeTrackedSnapshot(currIndex, browser);
michael@0 1019 this._addSnapshotRefToArray(currIndex, browser);
michael@0 1020
michael@0 1021 if (!("snapshots" in browser))
michael@0 1022 browser.snapshots = [];
michael@0 1023 let snapshots = browser.snapshots;
michael@0 1024 // Temporarily store the canvas as the compressed snapshot.
michael@0 1025 // This avoids a blank page if the user swipes quickly
michael@0 1026 // between pages before the compression could complete.
michael@0 1027 snapshots[currIndex] = {
michael@0 1028 image: aCanvas,
michael@0 1029 scale: window.devicePixelRatio
michael@0 1030 };
michael@0 1031 },
michael@0 1032
michael@0 1033 /**
michael@0 1034 * Compresses the HTMLCanvasElement that's stored at the current history
michael@0 1035 * index in the snapshot array and stores the compressed image in its place.
michael@0 1036 */
michael@0 1037 _compressSnapshotAtCurrentIndex:
michael@0 1038 function HSA__compressSnapshotAtCurrentIndex() {
michael@0 1039 if (!this._readyToTakeSnapshots()) {
michael@0 1040 // We didn't take a snapshot earlier because we weren't ready to, so
michael@0 1041 // there's nothing to compress.
michael@0 1042 return;
michael@0 1043 }
michael@0 1044
michael@0 1045 TelemetryStopwatch.start("FX_GESTURE_COMPRESS_SNAPSHOT_OF_PAGE");
michael@0 1046 try {
michael@0 1047 let browser = gBrowser.selectedBrowser;
michael@0 1048 let snapshots = browser.snapshots;
michael@0 1049 let currIndex = browser.webNavigation.sessionHistory.index;
michael@0 1050
michael@0 1051 // Kick off snapshot compression.
michael@0 1052 let canvas = snapshots[currIndex].image;
michael@0 1053 canvas.toBlob(function(aBlob) {
michael@0 1054 if (snapshots[currIndex]) {
michael@0 1055 snapshots[currIndex].image = aBlob;
michael@0 1056 }
michael@0 1057 }, "image/png"
michael@0 1058 );
michael@0 1059 } finally {
michael@0 1060 TelemetryStopwatch.finish("FX_GESTURE_COMPRESS_SNAPSHOT_OF_PAGE");
michael@0 1061 }
michael@0 1062 },
michael@0 1063
michael@0 1064 /**
michael@0 1065 * Removes a snapshot identified by the browser and index in the array of
michael@0 1066 * snapshots for that browser, if present. If no snapshot could be identified
michael@0 1067 * the method simply returns without taking any action. If aIndex is negative,
michael@0 1068 * all snapshots for a particular browser will be removed.
michael@0 1069 *
michael@0 1070 * @param aIndex
michael@0 1071 * The index in history of the new snapshot, or negative value if all
michael@0 1072 * snapshots for a browser should be removed.
michael@0 1073 * @param aBrowser
michael@0 1074 * The browser the new snapshot was taken in.
michael@0 1075 */
michael@0 1076 _removeTrackedSnapshot: function HSA__removeTrackedSnapshot(aIndex, aBrowser) {
michael@0 1077 let arr = this._trackedSnapshots;
michael@0 1078 let requiresExactIndexMatch = aIndex >= 0;
michael@0 1079 for (let i = 0; i < arr.length; i++) {
michael@0 1080 if ((arr[i].browser == aBrowser) &&
michael@0 1081 (aIndex < 0 || aIndex == arr[i].index)) {
michael@0 1082 delete aBrowser.snapshots[arr[i].index];
michael@0 1083 arr.splice(i, 1);
michael@0 1084 if (requiresExactIndexMatch)
michael@0 1085 return; // Found and removed the only element.
michael@0 1086 i--; // Make sure to revisit the index that we just removed an
michael@0 1087 // element at.
michael@0 1088 }
michael@0 1089 }
michael@0 1090 },
michael@0 1091
michael@0 1092 /**
michael@0 1093 * Adds a new snapshot reference for a given index and browser to the array
michael@0 1094 * of references to tracked snapshots.
michael@0 1095 *
michael@0 1096 * @param aIndex
michael@0 1097 * The index in history of the new snapshot.
michael@0 1098 * @param aBrowser
michael@0 1099 * The browser the new snapshot was taken in.
michael@0 1100 */
michael@0 1101 _addSnapshotRefToArray:
michael@0 1102 function HSA__addSnapshotRefToArray(aIndex, aBrowser) {
michael@0 1103 let id = { index: aIndex,
michael@0 1104 browser: aBrowser };
michael@0 1105 let arr = this._trackedSnapshots;
michael@0 1106 arr.unshift(id);
michael@0 1107
michael@0 1108 while (arr.length > this._maxSnapshots) {
michael@0 1109 let lastElem = arr[arr.length - 1];
michael@0 1110 delete lastElem.browser.snapshots[lastElem.index].image;
michael@0 1111 delete lastElem.browser.snapshots[lastElem.index];
michael@0 1112 arr.splice(-1, 1);
michael@0 1113 }
michael@0 1114 },
michael@0 1115
michael@0 1116 /**
michael@0 1117 * Converts a compressed blob to an Image object. In some situations
michael@0 1118 * (especially during fast swiping) aBlob may still be a canvas, not a
michael@0 1119 * compressed blob. In this case, we simply return the canvas.
michael@0 1120 *
michael@0 1121 * @param aBlob
michael@0 1122 * The compressed blob to convert, or a canvas if a blob compression
michael@0 1123 * couldn't complete before this method was called.
michael@0 1124 * @return A new Image object representing the converted blob.
michael@0 1125 */
michael@0 1126 _convertToImg: function HSA__convertToImg(aBlob) {
michael@0 1127 if (!aBlob)
michael@0 1128 return null;
michael@0 1129
michael@0 1130 // Return aBlob if it's still a canvas and not a compressed blob yet.
michael@0 1131 if (aBlob instanceof HTMLCanvasElement)
michael@0 1132 return aBlob;
michael@0 1133
michael@0 1134 let img = new Image();
michael@0 1135 let url = "";
michael@0 1136 try {
michael@0 1137 url = URL.createObjectURL(aBlob);
michael@0 1138 img.onload = function() {
michael@0 1139 URL.revokeObjectURL(url);
michael@0 1140 };
michael@0 1141 }
michael@0 1142 finally {
michael@0 1143 img.src = url;
michael@0 1144 return img;
michael@0 1145 }
michael@0 1146 },
michael@0 1147
michael@0 1148 /**
michael@0 1149 * Scales the background of a given box element (which uses a given snapshot
michael@0 1150 * as background) based on a given scale factor.
michael@0 1151 * @param aSnapshot
michael@0 1152 * The snapshot that is used as background of aBox.
michael@0 1153 * @param aScale
michael@0 1154 * The scale factor to use.
michael@0 1155 * @param aBox
michael@0 1156 * The box element that uses aSnapshot as background.
michael@0 1157 */
michael@0 1158 _scaleSnapshot: function HSA__scaleSnapshot(aSnapshot, aScale, aBox) {
michael@0 1159 if (aSnapshot && aScale != 1 && aBox) {
michael@0 1160 if (aSnapshot instanceof HTMLCanvasElement) {
michael@0 1161 aBox.style.backgroundSize =
michael@0 1162 aSnapshot.width / aScale + "px " + aSnapshot.height / aScale + "px";
michael@0 1163 } else {
michael@0 1164 // snapshot is instanceof HTMLImageElement
michael@0 1165 aSnapshot.addEventListener("load", function() {
michael@0 1166 aBox.style.backgroundSize =
michael@0 1167 aSnapshot.width / aScale + "px " + aSnapshot.height / aScale + "px";
michael@0 1168 });
michael@0 1169 }
michael@0 1170 }
michael@0 1171 },
michael@0 1172
michael@0 1173 /**
michael@0 1174 * Sets the snapshot of the current page to the snapshot passed as parameter,
michael@0 1175 * or to the one previously stored for the current index in history if the
michael@0 1176 * parameter is null.
michael@0 1177 *
michael@0 1178 * @param aCanvas
michael@0 1179 * The snapshot to set the current page to. If this parameter is null,
michael@0 1180 * the previously stored snapshot for this index (if any) will be used.
michael@0 1181 */
michael@0 1182 _installCurrentPageSnapshot:
michael@0 1183 function HSA__installCurrentPageSnapshot(aCanvas) {
michael@0 1184 let currSnapshot = aCanvas;
michael@0 1185 let scale = window.devicePixelRatio;
michael@0 1186 if (!currSnapshot) {
michael@0 1187 let snapshots = gBrowser.selectedBrowser.snapshots || {};
michael@0 1188 let currIndex = this._historyIndex;
michael@0 1189 if (currIndex in snapshots) {
michael@0 1190 currSnapshot = this._convertToImg(snapshots[currIndex].image);
michael@0 1191 scale = snapshots[currIndex].scale;
michael@0 1192 }
michael@0 1193 }
michael@0 1194 this._scaleSnapshot(currSnapshot, scale, this._curBox ? this._curBox :
michael@0 1195 null);
michael@0 1196 document.mozSetImageElement("historySwipeAnimationCurrentPageSnapshot",
michael@0 1197 currSnapshot);
michael@0 1198 },
michael@0 1199
michael@0 1200 /**
michael@0 1201 * Sets the snapshots of the previous and next pages to the snapshots
michael@0 1202 * previously stored for their respective indeces.
michael@0 1203 */
michael@0 1204 _installPrevAndNextSnapshots:
michael@0 1205 function HSA__installPrevAndNextSnapshots() {
michael@0 1206 let snapshots = gBrowser.selectedBrowser.snapshots || [];
michael@0 1207 let currIndex = this._historyIndex;
michael@0 1208 let prevIndex = currIndex - 1;
michael@0 1209 let prevSnapshot = null;
michael@0 1210 if (prevIndex in snapshots) {
michael@0 1211 prevSnapshot = this._convertToImg(snapshots[prevIndex].image);
michael@0 1212 this._scaleSnapshot(prevSnapshot, snapshots[prevIndex].scale,
michael@0 1213 this._prevBox);
michael@0 1214 }
michael@0 1215 document.mozSetImageElement("historySwipeAnimationPreviousPageSnapshot",
michael@0 1216 prevSnapshot);
michael@0 1217
michael@0 1218 let nextIndex = currIndex + 1;
michael@0 1219 let nextSnapshot = null;
michael@0 1220 if (nextIndex in snapshots) {
michael@0 1221 nextSnapshot = this._convertToImg(snapshots[nextIndex].image);
michael@0 1222 this._scaleSnapshot(nextSnapshot, snapshots[nextIndex].scale,
michael@0 1223 this._nextBox);
michael@0 1224 }
michael@0 1225 document.mozSetImageElement("historySwipeAnimationNextPageSnapshot",
michael@0 1226 nextSnapshot);
michael@0 1227 },
michael@0 1228 };

mercurial