forked from mirrors/gecko-dev
		
	
		
			
				
	
	
		
			1226 lines
		
	
	
	
		
			40 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			1226 lines
		
	
	
	
		
			40 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| # This Source Code Form is subject to the terms of the Mozilla Public
 | |
| # License, v. 2.0. If a copy of the MPL was not distributed with this
 | |
| # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 | |
| 
 | |
| // Simple gestures support
 | |
| //
 | |
| // As per bug #412486, web content must not be allowed to receive any
 | |
| // simple gesture events.  Multi-touch gesture APIs are in their
 | |
| // infancy and we do NOT want to be forced into supporting an API that
 | |
| // will probably have to change in the future.  (The current Mac OS X
 | |
| // API is undocumented and was reverse-engineered.)  Until support is
 | |
| // implemented in the event dispatcher to keep these events as
 | |
| // chrome-only, we must listen for the simple gesture events during
 | |
| // the capturing phase and call stopPropagation on every event.
 | |
| 
 | |
| let gGestureSupport = {
 | |
|   _currentRotation: 0,
 | |
|   _lastRotateDelta: 0,
 | |
|   _rotateMomentumThreshold: .75,
 | |
| 
 | |
|   /**
 | |
|    * Add or remove mouse gesture event listeners
 | |
|    *
 | |
|    * @param aAddListener
 | |
|    *        True to add/init listeners and false to remove/uninit
 | |
|    */
 | |
|   init: function GS_init(aAddListener) {
 | |
|     const gestureEvents = ["SwipeGestureStart",
 | |
|       "SwipeGestureUpdate", "SwipeGestureEnd", "SwipeGesture",
 | |
|       "MagnifyGestureStart", "MagnifyGestureUpdate", "MagnifyGesture",
 | |
|       "RotateGestureStart", "RotateGestureUpdate", "RotateGesture",
 | |
|       "TapGesture", "PressTapGesture"];
 | |
| 
 | |
|     let addRemove = aAddListener ? window.addEventListener :
 | |
|       window.removeEventListener;
 | |
| 
 | |
|     for (let event of gestureEvents) {
 | |
|       addRemove("Moz" + event, this, true);
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Dispatch events based on the type of mouse gesture event. For now, make
 | |
|    * sure to stop propagation of every gesture event so that web content cannot
 | |
|    * receive gesture events.
 | |
|    *
 | |
|    * @param aEvent
 | |
|    *        The gesture event to handle
 | |
|    */
 | |
|   handleEvent: function GS_handleEvent(aEvent) {
 | |
|     if (!Services.prefs.getBoolPref(
 | |
|            "dom.debug.propagate_gesture_events_through_content")) {
 | |
|       aEvent.stopPropagation();
 | |
|     }
 | |
| 
 | |
|     // Create a preference object with some defaults
 | |
|     let def = function(aThreshold, aLatched)
 | |
|       ({ threshold: aThreshold, latched: !!aLatched });
 | |
| 
 | |
|     switch (aEvent.type) {
 | |
|       case "MozSwipeGestureStart":
 | |
|         if (this._setupSwipeGesture(aEvent)) {
 | |
|           aEvent.preventDefault();
 | |
|         }
 | |
|         break;
 | |
|       case "MozSwipeGestureUpdate":
 | |
|         aEvent.preventDefault();
 | |
|         this._doUpdate(aEvent);
 | |
|         break;
 | |
|       case "MozSwipeGestureEnd":
 | |
|         aEvent.preventDefault();
 | |
|         this._doEnd(aEvent);
 | |
|         break;
 | |
|       case "MozSwipeGesture":
 | |
|         aEvent.preventDefault();
 | |
|         this.onSwipe(aEvent);
 | |
|         break;
 | |
|       case "MozMagnifyGestureStart":
 | |
|         aEvent.preventDefault();
 | |
| #ifdef XP_WIN
 | |
|         this._setupGesture(aEvent, "pinch", def(25, 0), "out", "in");
 | |
| #else
 | |
|         this._setupGesture(aEvent, "pinch", def(150, 1), "out", "in");
 | |
| #endif
 | |
|         break;
 | |
|       case "MozRotateGestureStart":
 | |
|         aEvent.preventDefault();
 | |
|         this._setupGesture(aEvent, "twist", def(25, 0), "right", "left");
 | |
|         break;
 | |
|       case "MozMagnifyGestureUpdate":
 | |
|       case "MozRotateGestureUpdate":
 | |
|         aEvent.preventDefault();
 | |
|         this._doUpdate(aEvent);
 | |
|         break;
 | |
|       case "MozTapGesture":
 | |
|         aEvent.preventDefault();
 | |
|         this._doAction(aEvent, ["tap"]);
 | |
|         break;
 | |
|       case "MozRotateGesture":
 | |
|         aEvent.preventDefault();
 | |
|         this._doAction(aEvent, ["twist", "end"]);
 | |
|         break;
 | |
|       /* case "MozPressTapGesture":
 | |
|         break; */
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Called at the start of "pinch" and "twist" gestures to setup all of the
 | |
|    * information needed to process the gesture
 | |
|    *
 | |
|    * @param aEvent
 | |
|    *        The continual motion start event to handle
 | |
|    * @param aGesture
 | |
|    *        Name of the gesture to handle
 | |
|    * @param aPref
 | |
|    *        Preference object with the names of preferences and defaults
 | |
|    * @param aInc
 | |
|    *        Command to trigger for increasing motion (without gesture name)
 | |
|    * @param aDec
 | |
|    *        Command to trigger for decreasing motion (without gesture name)
 | |
|    */
 | |
|   _setupGesture: function GS__setupGesture(aEvent, aGesture, aPref, aInc, aDec) {
 | |
|     // Try to load user-set values from preferences
 | |
|     for (let [pref, def] in Iterator(aPref))
 | |
|       aPref[pref] = this._getPref(aGesture + "." + pref, def);
 | |
| 
 | |
|     // Keep track of the total deltas and latching behavior
 | |
|     let offset = 0;
 | |
|     let latchDir = aEvent.delta > 0 ? 1 : -1;
 | |
|     let isLatched = false;
 | |
| 
 | |
|     // Create the update function here to capture closure state
 | |
|     this._doUpdate = function GS__doUpdate(aEvent) {
 | |
|       // Update the offset with new event data
 | |
|       offset += aEvent.delta;
 | |
| 
 | |
|       // Check if the cumulative deltas exceed the threshold
 | |
|       if (Math.abs(offset) > aPref["threshold"]) {
 | |
|         // Trigger the action if we don't care about latching; otherwise, make
 | |
|         // sure either we're not latched and going the same direction of the
 | |
|         // initial motion; or we're latched and going the opposite way
 | |
|         let sameDir = (latchDir ^ offset) >= 0;
 | |
|         if (!aPref["latched"] || (isLatched ^ sameDir)) {
 | |
|           this._doAction(aEvent, [aGesture, offset > 0 ? aInc : aDec]);
 | |
| 
 | |
|           // We must be getting latched or leaving it, so just toggle
 | |
|           isLatched = !isLatched;
 | |
|         }
 | |
| 
 | |
|         // Reset motion counter to prepare for more of the same gesture
 | |
|         offset = 0;
 | |
|       }
 | |
|     };
 | |
| 
 | |
|     // The start event also contains deltas, so handle an update right away
 | |
|     this._doUpdate(aEvent);
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Checks whether a swipe gesture event can navigate the browser history or
 | |
|    * not.
 | |
|    *
 | |
|    * @param aEvent
 | |
|    *        The swipe gesture event.
 | |
|    * @return true if the swipe event may navigate the history, false othwerwise.
 | |
|    */
 | |
|   _swipeNavigatesHistory: function GS__swipeNavigatesHistory(aEvent) {
 | |
|     return this._getCommand(aEvent, ["swipe", "left"])
 | |
|               == "Browser:BackOrBackDuplicate" &&
 | |
|            this._getCommand(aEvent, ["swipe", "right"])
 | |
|               == "Browser:ForwardOrForwardDuplicate";
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Sets up swipe gestures. This includes setting up swipe animations for the
 | |
|    * gesture, if enabled.
 | |
|    *
 | |
|    * @param aEvent
 | |
|    *        The swipe gesture start event.
 | |
|    * @return true if swipe gestures could successfully be set up, false
 | |
|    *         othwerwise.
 | |
|    */
 | |
|   _setupSwipeGesture: function GS__setupSwipeGesture(aEvent) {
 | |
|     if (!this._swipeNavigatesHistory(aEvent)) {
 | |
|       return false;
 | |
|     }
 | |
| 
 | |
|     let isVerticalSwipe = false;
 | |
|     if (aEvent.direction == aEvent.DIRECTION_UP) {
 | |
|       if (gMultiProcessBrowser || content.pageYOffset > 0) {
 | |
|         return false;
 | |
|       }
 | |
|       isVerticalSwipe = true;
 | |
|     } else if (aEvent.direction == aEvent.DIRECTION_DOWN) {
 | |
|       if (gMultiProcessBrowser || content.pageYOffset < content.scrollMaxY) {
 | |
|         return false;
 | |
|       }
 | |
|       isVerticalSwipe = true;
 | |
|     }
 | |
|     if (isVerticalSwipe) {
 | |
|       // Vertical overscroll has been temporarily disabled until bug 939480 is
 | |
|       // fixed.
 | |
|       return false;
 | |
|     }
 | |
| 
 | |
|     let canGoBack = gHistorySwipeAnimation.canGoBack();
 | |
|     let canGoForward = gHistorySwipeAnimation.canGoForward();
 | |
|     let isLTR = gHistorySwipeAnimation.isLTR;
 | |
| 
 | |
|     if (canGoBack) {
 | |
|       aEvent.allowedDirections |= isLTR ? aEvent.DIRECTION_LEFT :
 | |
|                                           aEvent.DIRECTION_RIGHT;
 | |
|     }
 | |
|     if (canGoForward) {
 | |
|       aEvent.allowedDirections |= isLTR ? aEvent.DIRECTION_RIGHT :
 | |
|                                           aEvent.DIRECTION_LEFT;
 | |
|     }
 | |
| 
 | |
|     gHistorySwipeAnimation.startAnimation(isVerticalSwipe);
 | |
| 
 | |
|     this._doUpdate = function GS__doUpdate(aEvent) {
 | |
|       gHistorySwipeAnimation.updateAnimation(aEvent.delta);
 | |
|     };
 | |
| 
 | |
|     this._doEnd = function GS__doEnd(aEvent) {
 | |
|       gHistorySwipeAnimation.swipeEndEventReceived();
 | |
| 
 | |
|       this._doUpdate = function (aEvent) {};
 | |
|       this._doEnd = function (aEvent) {};
 | |
|     }
 | |
| 
 | |
|     return true;
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Generator producing the powerset of the input array where the first result
 | |
|    * is the complete set and the last result (before StopIteration) is empty.
 | |
|    *
 | |
|    * @param aArray
 | |
|    *        Source array containing any number of elements
 | |
|    * @yield Array that is a subset of the input array from full set to empty
 | |
|    */
 | |
|   _power: function GS__power(aArray) {
 | |
|     // Create a bitmask based on the length of the array
 | |
|     let num = 1 << aArray.length;
 | |
|     while (--num >= 0) {
 | |
|       // Only select array elements where the current bit is set
 | |
|       yield aArray.reduce(function (aPrev, aCurr, aIndex) {
 | |
|         if (num & 1 << aIndex)
 | |
|           aPrev.push(aCurr);
 | |
|         return aPrev;
 | |
|       }, []);
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Determine what action to do for the gesture based on which keys are
 | |
|    * pressed and which commands are set, and execute the command.
 | |
|    *
 | |
|    * @param aEvent
 | |
|    *        The original gesture event to convert into a fake click event
 | |
|    * @param aGesture
 | |
|    *        Array of gesture name parts (to be joined by periods)
 | |
|    * @return Name of the executed command. Returns null if no command is
 | |
|    *         found.
 | |
|    */
 | |
|   _doAction: function GS__doAction(aEvent, aGesture) {
 | |
|     let command = this._getCommand(aEvent, aGesture);
 | |
|     return command && this._doCommand(aEvent, command);
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Determine what action to do for the gesture based on which keys are
 | |
|    * pressed and which commands are set
 | |
|    *
 | |
|    * @param aEvent
 | |
|    *        The original gesture event to convert into a fake click event
 | |
|    * @param aGesture
 | |
|    *        Array of gesture name parts (to be joined by periods)
 | |
|    */
 | |
|   _getCommand: function GS__getCommand(aEvent, aGesture) {
 | |
|     // Create an array of pressed keys in a fixed order so that a command for
 | |
|     // "meta" is preferred over "ctrl" when both buttons are pressed (and a
 | |
|     // command for both don't exist)
 | |
|     let keyCombos = [];
 | |
|     for (let key of ["shift", "alt", "ctrl", "meta"]) {
 | |
|       if (aEvent[key + "Key"])
 | |
|         keyCombos.push(key);
 | |
|     }
 | |
| 
 | |
|     // Try each combination of key presses in decreasing order for commands
 | |
|     for (let subCombo of this._power(keyCombos)) {
 | |
|       // Convert a gesture and pressed keys into the corresponding command
 | |
|       // action where the preference has the gesture before "shift" before
 | |
|       // "alt" before "ctrl" before "meta" all separated by periods
 | |
|       let command;
 | |
|       try {
 | |
|         command = this._getPref(aGesture.concat(subCombo).join("."));
 | |
|       } catch (e) {}
 | |
| 
 | |
|       if (command)
 | |
|         return command;
 | |
|     }
 | |
|     return null;
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Execute the specified command.
 | |
|    *
 | |
|    * @param aEvent
 | |
|    *        The original gesture event to convert into a fake click event
 | |
|    * @param aCommand
 | |
|    *        Name of the command found for the event's keys and gesture.
 | |
|    */
 | |
|   _doCommand: function GS__doCommand(aEvent, aCommand) {
 | |
|     let node = document.getElementById(aCommand);
 | |
|     if (node) {
 | |
|       if (node.getAttribute("disabled") != "true") {
 | |
|         let cmdEvent = document.createEvent("xulcommandevent");
 | |
|         cmdEvent.initCommandEvent("command", true, true, window, 0,
 | |
|                                   aEvent.ctrlKey, aEvent.altKey,
 | |
|                                   aEvent.shiftKey, aEvent.metaKey, aEvent);
 | |
|         node.dispatchEvent(cmdEvent);
 | |
|       }
 | |
| 
 | |
|     }
 | |
|     else {
 | |
|       goDoCommand(aCommand);
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Handle continual motion events.  This function will be set by
 | |
|    * _setupGesture or _setupSwipe.
 | |
|    *
 | |
|    * @param aEvent
 | |
|    *        The continual motion update event to handle
 | |
|    */
 | |
|   _doUpdate: function(aEvent) {},
 | |
| 
 | |
|   /**
 | |
|    * Handle gesture end events.  This function will be set by _setupSwipe.
 | |
|    *
 | |
|    * @param aEvent
 | |
|    *        The gesture end event to handle
 | |
|    */
 | |
|   _doEnd: function(aEvent) {},
 | |
| 
 | |
|   /**
 | |
|    * Convert the swipe gesture into a browser action based on the direction.
 | |
|    *
 | |
|    * @param aEvent
 | |
|    *        The swipe event to handle
 | |
|    */
 | |
|   onSwipe: function GS_onSwipe(aEvent) {
 | |
|     // Figure out which one (and only one) direction was triggered
 | |
|     for (let dir of ["UP", "RIGHT", "DOWN", "LEFT"]) {
 | |
|       if (aEvent.direction == aEvent["DIRECTION_" + dir]) {
 | |
|         this._coordinateSwipeEventWithAnimation(aEvent, dir);
 | |
|         break;
 | |
|       }
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Process a swipe event based on the given direction.
 | |
|    *
 | |
|    * @param aEvent
 | |
|    *        The swipe event to handle
 | |
|    * @param aDir
 | |
|    *        The direction for the swipe event
 | |
|    */
 | |
|   processSwipeEvent: function GS_processSwipeEvent(aEvent, aDir) {
 | |
|     this._doAction(aEvent, ["swipe", aDir.toLowerCase()]);
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Coordinates the swipe event with the swipe animation, if any.
 | |
|    * If an animation is currently running, the swipe event will be
 | |
|    * processed once the animation stops. This will guarantee a fluid
 | |
|    * motion of the animation.
 | |
|    *
 | |
|    * @param aEvent
 | |
|    *        The swipe event to handle
 | |
|    * @param aDir
 | |
|    *        The direction for the swipe event
 | |
|    */
 | |
|   _coordinateSwipeEventWithAnimation:
 | |
|   function GS__coordinateSwipeEventWithAnimation(aEvent, aDir) {
 | |
|     if ((gHistorySwipeAnimation.isAnimationRunning()) &&
 | |
|         (aDir == "RIGHT" || aDir == "LEFT")) {
 | |
|       gHistorySwipeAnimation.processSwipeEvent(aEvent, aDir);
 | |
|     }
 | |
|     else {
 | |
|       this.processSwipeEvent(aEvent, aDir);
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Get a gesture preference or use a default if it doesn't exist
 | |
|    *
 | |
|    * @param aPref
 | |
|    *        Name of the preference to load under the gesture branch
 | |
|    * @param aDef
 | |
|    *        Default value if the preference doesn't exist
 | |
|    */
 | |
|   _getPref: function GS__getPref(aPref, aDef) {
 | |
|     // Preferences branch under which all gestures preferences are stored
 | |
|     const branch = "browser.gesture.";
 | |
| 
 | |
|     try {
 | |
|       // Determine what type of data to load based on default value's type
 | |
|       let type = typeof aDef;
 | |
|       let getFunc = "get" + (type == "boolean" ? "Bool" :
 | |
|                              type == "number" ? "Int" : "Char") + "Pref";
 | |
|       return gPrefService[getFunc](branch + aPref);
 | |
|     }
 | |
|     catch (e) {
 | |
|       return aDef;
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Perform rotation for ImageDocuments
 | |
|    *
 | |
|    * @param aEvent
 | |
|    *        The MozRotateGestureUpdate event triggering this call
 | |
|    */
 | |
|   rotate: function(aEvent) {
 | |
|     if (!(content.document instanceof ImageDocument))
 | |
|       return;
 | |
| 
 | |
|     let contentElement = content.document.body.firstElementChild;
 | |
|     if (!contentElement)
 | |
|       return;
 | |
|     // If we're currently snapping, cancel that snap
 | |
|     if (contentElement.classList.contains("completeRotation"))
 | |
|       this._clearCompleteRotation();
 | |
| 
 | |
|     this.rotation = Math.round(this.rotation + aEvent.delta);
 | |
|     contentElement.style.transform = "rotate(" + this.rotation + "deg)";
 | |
|     this._lastRotateDelta = aEvent.delta;
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Perform a rotation end for ImageDocuments
 | |
|    */
 | |
|   rotateEnd: function() {
 | |
|     if (!(content.document instanceof ImageDocument))
 | |
|       return;
 | |
| 
 | |
|     let contentElement = content.document.body.firstElementChild;
 | |
|     if (!contentElement)
 | |
|       return;
 | |
| 
 | |
|     let transitionRotation = 0;
 | |
| 
 | |
|     // The reason that 360 is allowed here is because when rotating between
 | |
|     // 315 and 360, setting rotate(0deg) will cause it to rotate the wrong
 | |
|     // direction around--spinning wildly.
 | |
|     if (this.rotation <= 45)
 | |
|       transitionRotation = 0;
 | |
|     else if (this.rotation > 45 && this.rotation <= 135)
 | |
|       transitionRotation = 90;
 | |
|     else if (this.rotation > 135 && this.rotation <= 225)
 | |
|       transitionRotation = 180;
 | |
|     else if (this.rotation > 225 && this.rotation <= 315)
 | |
|       transitionRotation = 270;
 | |
|     else
 | |
|       transitionRotation = 360;
 | |
| 
 | |
|     // If we're going fast enough, and we didn't already snap ahead of rotation,
 | |
|     // then snap ahead of rotation to simulate momentum
 | |
|     if (this._lastRotateDelta > this._rotateMomentumThreshold &&
 | |
|         this.rotation > transitionRotation)
 | |
|       transitionRotation += 90;
 | |
|     else if (this._lastRotateDelta < -1 * this._rotateMomentumThreshold &&
 | |
|              this.rotation < transitionRotation)
 | |
|       transitionRotation -= 90;
 | |
| 
 | |
|     // Only add the completeRotation class if it is is necessary
 | |
|     if (transitionRotation != this.rotation) {
 | |
|       contentElement.classList.add("completeRotation");
 | |
|       contentElement.addEventListener("transitionend", this._clearCompleteRotation);
 | |
|     }
 | |
| 
 | |
|     contentElement.style.transform = "rotate(" + transitionRotation + "deg)";
 | |
|     this.rotation = transitionRotation;
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Gets the current rotation for the ImageDocument
 | |
|    */
 | |
|   get rotation() {
 | |
|     return this._currentRotation;
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Sets the current rotation for the ImageDocument
 | |
|    *
 | |
|    * @param aVal
 | |
|    *        The new value to take.  Can be any value, but it will be bounded to
 | |
|    *        0 inclusive to 360 exclusive.
 | |
|    */
 | |
|   set rotation(aVal) {
 | |
|     this._currentRotation = aVal % 360;
 | |
|     if (this._currentRotation < 0)
 | |
|       this._currentRotation += 360;
 | |
|     return this._currentRotation;
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * When the location/tab changes, need to reload the current rotation for the
 | |
|    * image
 | |
|    */
 | |
|   restoreRotationState: function() {
 | |
|     // Bug 863514 - Make gesture support work in electrolysis
 | |
|     if (gMultiProcessBrowser)
 | |
|       return;
 | |
| 
 | |
|     if (!(content.document instanceof ImageDocument))
 | |
|       return;
 | |
| 
 | |
|     let contentElement = content.document.body.firstElementChild;
 | |
|     let transformValue = content.window.getComputedStyle(contentElement, null)
 | |
|                                        .transform;
 | |
| 
 | |
|     if (transformValue == "none") {
 | |
|       this.rotation = 0;
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     // transformValue is a rotation matrix--split it and do mathemagic to
 | |
|     // obtain the real rotation value
 | |
|     transformValue = transformValue.split("(")[1]
 | |
|                                    .split(")")[0]
 | |
|                                    .split(",");
 | |
|     this.rotation = Math.round(Math.atan2(transformValue[1], transformValue[0]) *
 | |
|                                (180 / Math.PI));
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Removes the transition rule by removing the completeRotation class
 | |
|    */
 | |
|   _clearCompleteRotation: function() {
 | |
|     let contentElement = content.document &&
 | |
|                          content.document instanceof ImageDocument &&
 | |
|                          content.document.body &&
 | |
|                          content.document.body.firstElementChild;
 | |
|     if (!contentElement)
 | |
|       return;
 | |
|     contentElement.classList.remove("completeRotation");
 | |
|     contentElement.removeEventListener("transitionend", this._clearCompleteRotation);
 | |
|   },
 | |
| };
 | |
| 
 | |
| // History Swipe Animation Support (bug 678392)
 | |
| let gHistorySwipeAnimation = {
 | |
| 
 | |
|   active: false,
 | |
|   isLTR: false,
 | |
| 
 | |
|   /**
 | |
|    * Initializes the support for history swipe animations, if it is supported
 | |
|    * by the platform/configuration.
 | |
|    */
 | |
|   init: function HSA_init() {
 | |
|     if (!this._isSupported())
 | |
|       return;
 | |
| 
 | |
|     this.active = false;
 | |
|     this.isLTR = document.documentElement.matches(":-moz-locale-dir(ltr)");
 | |
|     this._trackedSnapshots = [];
 | |
|     this._startingIndex = -1;
 | |
|     this._historyIndex = -1;
 | |
|     this._boxWidth = -1;
 | |
|     this._boxHeight = -1;
 | |
|     this._maxSnapshots = this._getMaxSnapshots();
 | |
|     this._lastSwipeDir = "";
 | |
|     this._direction = "horizontal";
 | |
| 
 | |
|     // We only want to activate history swipe animations if we store snapshots.
 | |
|     // If we don't store any, we handle horizontal swipes without animations.
 | |
|     if (this._maxSnapshots > 0) {
 | |
|       this.active = true;
 | |
|       gBrowser.addEventListener("pagehide", this, false);
 | |
|       gBrowser.addEventListener("pageshow", this, false);
 | |
|       gBrowser.addEventListener("popstate", this, false);
 | |
|       gBrowser.addEventListener("DOMModalDialogClosed", this, false);
 | |
|       gBrowser.tabContainer.addEventListener("TabClose", this, false);
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Uninitializes the support for history swipe animations.
 | |
|    */
 | |
|   uninit: function HSA_uninit() {
 | |
|     gBrowser.removeEventListener("pagehide", this, false);
 | |
|     gBrowser.removeEventListener("pageshow", this, false);
 | |
|     gBrowser.removeEventListener("popstate", this, false);
 | |
|     gBrowser.removeEventListener("DOMModalDialogClosed", this, false);
 | |
|     gBrowser.tabContainer.removeEventListener("TabClose", this, false);
 | |
| 
 | |
|     this.active = false;
 | |
|     this.isLTR = false;
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Starts the swipe animation and handles fast swiping (i.e. a swipe animation
 | |
|    * is already in progress when a new one is initiated).
 | |
|    *
 | |
|    * @param aIsVerticalSwipe
 | |
|    *        Whether we're dealing with a vertical swipe or not.
 | |
|    */
 | |
|   startAnimation: function HSA_startAnimation(aIsVerticalSwipe) {
 | |
|     this._direction = aIsVerticalSwipe ? "vertical" : "horizontal";
 | |
| 
 | |
|     if (this.isAnimationRunning()) {
 | |
|       // If this is a horizontal scroll, or if this is a vertical scroll that
 | |
|       // was started while a horizontal scroll was still running, handle it as
 | |
|       // as a fast swipe. In the case of the latter scenario, this allows us to
 | |
|       // start the vertical animation without first loading the final page, or
 | |
|       // taking another snapshot. If vertical scrolls are initiated repeatedly
 | |
|       // without prior horizontal scroll we skip this and restart the animation
 | |
|       // from 0.
 | |
|       if (this._direction == "horizontal" || this._lastSwipeDir != "") {
 | |
|         gBrowser.stop();
 | |
|         this._lastSwipeDir = "RELOAD"; // just ensure that != ""
 | |
|         this._canGoBack = this.canGoBack();
 | |
|         this._canGoForward = this.canGoForward();
 | |
|         this._handleFastSwiping();
 | |
|       }
 | |
|     }
 | |
|     else {
 | |
|       this._startingIndex = gBrowser.webNavigation.sessionHistory.index;
 | |
|       this._historyIndex = this._startingIndex;
 | |
|       this._canGoBack = this.canGoBack();
 | |
|       this._canGoForward = this.canGoForward();
 | |
|       if (this.active) {
 | |
|         this._addBoxes();
 | |
|         this._takeSnapshot();
 | |
|         this._installPrevAndNextSnapshots();
 | |
|         this._lastSwipeDir = "";
 | |
|       }
 | |
|     }
 | |
|     this.updateAnimation(0);
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Stops the swipe animation.
 | |
|    */
 | |
|   stopAnimation: function HSA_stopAnimation() {
 | |
|     gHistorySwipeAnimation._removeBoxes();
 | |
|     this._historyIndex = gBrowser.webNavigation.sessionHistory.index;
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Updates the animation between two pages in history.
 | |
|    *
 | |
|    * @param aVal
 | |
|    *        A floating point value that represents the progress of the
 | |
|    *        swipe gesture.
 | |
|    */
 | |
|   updateAnimation: function HSA_updateAnimation(aVal) {
 | |
|     if (!this.isAnimationRunning()) {
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     // We use the following value to decrease the bounce effect when scrolling
 | |
|     // to the top or bottom of the page, or when swiping back/forward past the
 | |
|     // browsing history. This value was determined experimentally.
 | |
|     let dampValue = 4;
 | |
|     if (this._direction == "vertical") {
 | |
|       this._prevBox.collapsed = true;
 | |
|       this._nextBox.collapsed = true;
 | |
|       this._positionBox(this._curBox, -1 * aVal / dampValue);
 | |
|     } else if ((aVal >= 0 && this.isLTR) ||
 | |
|                (aVal <= 0 && !this.isLTR)) {
 | |
|       let tempDampValue = 1;
 | |
|       if (this._canGoBack) {
 | |
|         this._prevBox.collapsed = false;
 | |
|       } else {
 | |
|         tempDampValue = dampValue;
 | |
|         this._prevBox.collapsed = true;
 | |
|       }
 | |
| 
 | |
|       // The current page is pushed to the right (LTR) or left (RTL),
 | |
|       // the intention is to go back.
 | |
|       // If there is a page to go back to, it should show in the background.
 | |
|       this._positionBox(this._curBox, aVal / tempDampValue);
 | |
| 
 | |
|       // The forward page should be pushed offscreen all the way to the right.
 | |
|       this._positionBox(this._nextBox, 1);
 | |
|     } else {
 | |
|       // The intention is to go forward. If there is a page to go forward to,
 | |
|       // it should slide in from the right (LTR) or left (RTL).
 | |
|       // Otherwise, the current page should slide to the left (LTR) or
 | |
|       // right (RTL) and the backdrop should appear in the background.
 | |
|       // For the backdrop to be visible in that case, the previous page needs
 | |
|       // to be hidden (if it exists).
 | |
|       if (this._canGoForward) {
 | |
|         this._nextBox.collapsed = false;
 | |
|         let offset = this.isLTR ? 1 : -1;
 | |
|         this._positionBox(this._curBox, 0);
 | |
|         this._positionBox(this._nextBox, offset + aVal);
 | |
|       } else {
 | |
|         this._prevBox.collapsed = true;
 | |
|         this._positionBox(this._curBox, aVal / dampValue);
 | |
|       }
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Event handler for events relevant to the history swipe animation.
 | |
|    *
 | |
|    * @param aEvent
 | |
|    *        An event to process.
 | |
|    */
 | |
|   handleEvent: function HSA_handleEvent(aEvent) {
 | |
|     let browser = gBrowser.selectedBrowser;
 | |
|     switch (aEvent.type) {
 | |
|       case "TabClose":
 | |
|         let browserForTab = gBrowser.getBrowserForTab(aEvent.target);
 | |
|         this._removeTrackedSnapshot(-1, browserForTab);
 | |
|         break;
 | |
|       case "DOMModalDialogClosed":
 | |
|         this.stopAnimation();
 | |
|         break;
 | |
|       case "pageshow":
 | |
|         if (aEvent.target == browser.contentDocument) {
 | |
|           this.stopAnimation();
 | |
|         }
 | |
|         break;
 | |
|       case "popstate":
 | |
|         if (aEvent.target == browser.contentDocument.defaultView) {
 | |
|           this.stopAnimation();
 | |
|         }
 | |
|         break;
 | |
|       case "pagehide":
 | |
|         if (aEvent.target == browser.contentDocument) {
 | |
|           // Take and compress a snapshot of a page whenever it's about to be
 | |
|           // navigated away from. We already have a snapshot of the page if an
 | |
|           // animation is running, so we're left with compressing it.
 | |
|           if (!this.isAnimationRunning()) {
 | |
|             this._takeSnapshot();
 | |
|           }
 | |
|           this._compressSnapshotAtCurrentIndex();
 | |
|         }
 | |
|         break;
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Checks whether the history swipe animation is currently running or not.
 | |
|    *
 | |
|    * @return true if the animation is currently running, false otherwise.
 | |
|    */
 | |
|   isAnimationRunning: function HSA_isAnimationRunning() {
 | |
|     return !!this._container;
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Process a swipe event based on the given direction.
 | |
|    *
 | |
|    * @param aEvent
 | |
|    *        The swipe event to handle
 | |
|    * @param aDir
 | |
|    *        The direction for the swipe event
 | |
|    */
 | |
|   processSwipeEvent: function HSA_processSwipeEvent(aEvent, aDir) {
 | |
|     if (aDir == "RIGHT")
 | |
|       this._historyIndex += this.isLTR ? 1 : -1;
 | |
|     else if (aDir == "LEFT")
 | |
|       this._historyIndex += this.isLTR ? -1 : 1;
 | |
|     else
 | |
|       return;
 | |
|     this._lastSwipeDir = aDir;
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Checks if there is a page in the browser history to go back to.
 | |
|    *
 | |
|    * @return true if there is a previous page in history, false otherwise.
 | |
|    */
 | |
|   canGoBack: function HSA_canGoBack() {
 | |
|     if (this.isAnimationRunning())
 | |
|       return this._doesIndexExistInHistory(this._historyIndex - 1);
 | |
|     return gBrowser.webNavigation.canGoBack;
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Checks if there is a page in the browser history to go forward to.
 | |
|    *
 | |
|    * @return true if there is a next page in history, false otherwise.
 | |
|    */
 | |
|   canGoForward: function HSA_canGoForward() {
 | |
|     if (this.isAnimationRunning())
 | |
|       return this._doesIndexExistInHistory(this._historyIndex + 1);
 | |
|     return gBrowser.webNavigation.canGoForward;
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Used to notify the history swipe animation that the OS sent a swipe end
 | |
|    * event and that we should navigate to the page that the user swiped to, if
 | |
|    * any. This will also result in the animation overlay to be torn down.
 | |
|    */
 | |
|   swipeEndEventReceived: function HSA_swipeEndEventReceived() {
 | |
|     if (this._lastSwipeDir != "" && this._historyIndex != this._startingIndex)
 | |
|       this._navigateToHistoryIndex();
 | |
|     else
 | |
|       this.stopAnimation();
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Checks whether a particular index exists in the browser history or not.
 | |
|    *
 | |
|    * @param aIndex
 | |
|    *        The index to check for availability for in the history.
 | |
|    * @return true if the index exists in the browser history, false otherwise.
 | |
|    */
 | |
|   _doesIndexExistInHistory: function HSA__doesIndexExistInHistory(aIndex) {
 | |
|     try {
 | |
|       gBrowser.webNavigation.sessionHistory.getEntryAtIndex(aIndex, false);
 | |
|     }
 | |
|     catch(ex) {
 | |
|       return false;
 | |
|     }
 | |
|     return true;
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Navigates to the index in history that is currently being tracked by
 | |
|    * |this|.
 | |
|    */
 | |
|   _navigateToHistoryIndex: function HSA__navigateToHistoryIndex() {
 | |
|     if (this._doesIndexExistInHistory(this._historyIndex))
 | |
|       gBrowser.webNavigation.gotoIndex(this._historyIndex);
 | |
|     else
 | |
|       this.stopAnimation();
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Checks to see if history swipe animations are supported by this
 | |
|    * platform/configuration.
 | |
|    *
 | |
|    * return true if supported, false otherwise.
 | |
|    */
 | |
|   _isSupported: function HSA__isSupported() {
 | |
|     return window.matchMedia("(-moz-swipe-animation-enabled)").matches;
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Handle fast swiping (i.e. a swipe animation is already in
 | |
|    * progress when a new one is initiated). This will swap out the snapshots
 | |
|    * used in the previous animation with the appropriate new ones.
 | |
|    */
 | |
|   _handleFastSwiping: function HSA__handleFastSwiping() {
 | |
|     this._installCurrentPageSnapshot(null);
 | |
|     this._installPrevAndNextSnapshots();
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Adds the boxes that contain the snapshots used during the swipe animation.
 | |
|    */
 | |
|   _addBoxes: function HSA__addBoxes() {
 | |
|     let browserStack =
 | |
|       document.getAnonymousElementByAttribute(gBrowser.getNotificationBox(),
 | |
|                                               "class", "browserStack");
 | |
|     this._container = this._createElement("historySwipeAnimationContainer",
 | |
|                                           "stack");
 | |
|     browserStack.appendChild(this._container);
 | |
| 
 | |
|     this._prevBox = this._createElement("historySwipeAnimationPreviousPage",
 | |
|                                         "box");
 | |
|     this._container.appendChild(this._prevBox);
 | |
| 
 | |
|     this._curBox = this._createElement("historySwipeAnimationCurrentPage",
 | |
|                                        "box");
 | |
|     this._container.appendChild(this._curBox);
 | |
| 
 | |
|     this._nextBox = this._createElement("historySwipeAnimationNextPage",
 | |
|                                         "box");
 | |
|     this._container.appendChild(this._nextBox);
 | |
| 
 | |
|     // Cache width and height.
 | |
|     this._boxWidth = this._curBox.getBoundingClientRect().width;
 | |
|     this._boxHeight = this._curBox.getBoundingClientRect().height;
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Removes the boxes.
 | |
|    */
 | |
|   _removeBoxes: function HSA__removeBoxes() {
 | |
|     this._curBox = null;
 | |
|     this._prevBox = null;
 | |
|     this._nextBox = null;
 | |
|     if (this._container)
 | |
|       this._container.parentNode.removeChild(this._container);
 | |
|     this._container = null;
 | |
|     this._boxWidth = -1;
 | |
|     this._boxHeight = -1;
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Creates an element with a given identifier and tag name.
 | |
|    *
 | |
|    * @param aID
 | |
|    *        An identifier to create the element with.
 | |
|    * @param aTagName
 | |
|    *        The name of the tag to create the element for.
 | |
|    * @return the newly created element.
 | |
|    */
 | |
|   _createElement: function HSA__createElement(aID, aTagName) {
 | |
|     let XULNS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
 | |
|     let element = document.createElementNS(XULNS, aTagName);
 | |
|     element.id = aID;
 | |
|     return element;
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Moves a given box to a given X coordinate position.
 | |
|    *
 | |
|    * @param aBox
 | |
|    *        The box element to position.
 | |
|    * @param aPosition
 | |
|    *        The position (in X coordinates) to move the box element to.
 | |
|    */
 | |
|   _positionBox: function HSA__positionBox(aBox, aPosition) {
 | |
|     let transform = "";
 | |
| 
 | |
|     if (this._direction == "vertical")
 | |
|       transform = "translateY(" + this._boxHeight * aPosition + "px)";
 | |
|     else
 | |
|       transform = "translateX(" + this._boxWidth * aPosition + "px)";
 | |
| 
 | |
|     aBox.style.transform = transform;
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Verifies that we're ready to take snapshots based on the global pref and
 | |
|    * the current index in history.
 | |
|    *
 | |
|    * @return true if we're ready to take snapshots, false otherwise.
 | |
|    */
 | |
|   _readyToTakeSnapshots: function HSA__readyToTakeSnapshots() {
 | |
|     if ((this._maxSnapshots < 1) ||
 | |
|         (gBrowser.webNavigation.sessionHistory.index < 0)) {
 | |
|       return false;
 | |
|     }
 | |
|     return true;
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Takes a snapshot of the page the browser is currently on.
 | |
|    */
 | |
|   _takeSnapshot: function HSA__takeSnapshot() {
 | |
|     if (!this._readyToTakeSnapshots()) {
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     let canvas = null;
 | |
| 
 | |
|     TelemetryStopwatch.start("FX_GESTURE_TAKE_SNAPSHOT_OF_PAGE");
 | |
|     try {
 | |
|       let browser = gBrowser.selectedBrowser;
 | |
|       let r = browser.getBoundingClientRect();
 | |
|       canvas = document.createElementNS("http://www.w3.org/1999/xhtml",
 | |
|                                         "canvas");
 | |
|       canvas.mozOpaque = true;
 | |
|       let scale = window.devicePixelRatio;
 | |
|       canvas.width = r.width * scale;
 | |
|       canvas.height = r.height * scale;
 | |
|       let ctx = canvas.getContext("2d");
 | |
|       let zoom = browser.markupDocumentViewer.fullZoom * scale;
 | |
|       ctx.scale(zoom, zoom);
 | |
|       ctx.drawWindow(browser.contentWindow,
 | |
|                      0, 0, canvas.width / zoom, canvas.height / zoom, "white",
 | |
|                      ctx.DRAWWINDOW_DO_NOT_FLUSH | ctx.DRAWWINDOW_DRAW_VIEW |
 | |
|                      ctx.DRAWWINDOW_ASYNC_DECODE_IMAGES |
 | |
|                      ctx.DRAWWINDOW_USE_WIDGET_LAYERS);
 | |
|     } finally {
 | |
|       TelemetryStopwatch.finish("FX_GESTURE_TAKE_SNAPSHOT_OF_PAGE");
 | |
|     }
 | |
| 
 | |
|     TelemetryStopwatch.start("FX_GESTURE_INSTALL_SNAPSHOT_OF_PAGE");
 | |
|     try {
 | |
|       this._installCurrentPageSnapshot(canvas);
 | |
|       this._assignSnapshotToCurrentBrowser(canvas);
 | |
|     } finally {
 | |
|       TelemetryStopwatch.finish("FX_GESTURE_INSTALL_SNAPSHOT_OF_PAGE");
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Retrieves the maximum number of snapshots that should be kept in memory.
 | |
|    * This limit is a global limit and is valid across all open tabs.
 | |
|    */
 | |
|   _getMaxSnapshots: function HSA__getMaxSnapshots() {
 | |
|     return gPrefService.getIntPref("browser.snapshots.limit");
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Adds a snapshot to the list and initiates the compression of said snapshot.
 | |
|    * Once the compression is completed, it will replace the uncompressed
 | |
|    * snapshot in the list.
 | |
|    *
 | |
|    * @param aCanvas
 | |
|    *        The snapshot to add to the list and compress.
 | |
|    */
 | |
|   _assignSnapshotToCurrentBrowser:
 | |
|   function HSA__assignSnapshotToCurrentBrowser(aCanvas) {
 | |
|     let browser = gBrowser.selectedBrowser;
 | |
|     let currIndex = browser.webNavigation.sessionHistory.index;
 | |
| 
 | |
|     this._removeTrackedSnapshot(currIndex, browser);
 | |
|     this._addSnapshotRefToArray(currIndex, browser);
 | |
| 
 | |
|     if (!("snapshots" in browser))
 | |
|       browser.snapshots = [];
 | |
|     let snapshots = browser.snapshots;
 | |
|     // Temporarily store the canvas as the compressed snapshot.
 | |
|     // This avoids a blank page if the user swipes quickly
 | |
|     // between pages before the compression could complete.
 | |
|     snapshots[currIndex] = {
 | |
|       image: aCanvas,
 | |
|       scale: window.devicePixelRatio
 | |
|     };
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Compresses the HTMLCanvasElement that's stored at the current history
 | |
|    * index in the snapshot array and stores the compressed image in its place.
 | |
|    */
 | |
|   _compressSnapshotAtCurrentIndex:
 | |
|   function HSA__compressSnapshotAtCurrentIndex() {
 | |
|     if (!this._readyToTakeSnapshots()) {
 | |
|       // We didn't take a snapshot earlier because we weren't ready to, so
 | |
|       // there's nothing to compress.
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     TelemetryStopwatch.start("FX_GESTURE_COMPRESS_SNAPSHOT_OF_PAGE");
 | |
|     try {
 | |
|       let browser = gBrowser.selectedBrowser;
 | |
|       let snapshots = browser.snapshots;
 | |
|       let currIndex = browser.webNavigation.sessionHistory.index;
 | |
| 
 | |
|       // Kick off snapshot compression.
 | |
|       let canvas = snapshots[currIndex].image;
 | |
|       canvas.toBlob(function(aBlob) {
 | |
|           if (snapshots[currIndex]) {
 | |
|             snapshots[currIndex].image = aBlob;
 | |
|           }
 | |
|         }, "image/png"
 | |
|       );
 | |
|     } finally {
 | |
|       TelemetryStopwatch.finish("FX_GESTURE_COMPRESS_SNAPSHOT_OF_PAGE");
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Removes a snapshot identified by the browser and index in the array of
 | |
|    * snapshots for that browser, if present. If no snapshot could be identified
 | |
|    * the method simply returns without taking any action. If aIndex is negative,
 | |
|    * all snapshots for a particular browser will be removed.
 | |
|    *
 | |
|    * @param aIndex
 | |
|    *        The index in history of the new snapshot, or negative value if all
 | |
|    *        snapshots for a browser should be removed.
 | |
|    * @param aBrowser
 | |
|    *        The browser the new snapshot was taken in.
 | |
|    */
 | |
|   _removeTrackedSnapshot: function HSA__removeTrackedSnapshot(aIndex, aBrowser) {
 | |
|     let arr = this._trackedSnapshots;
 | |
|     let requiresExactIndexMatch = aIndex >= 0;
 | |
|     for (let i = 0; i < arr.length; i++) {
 | |
|       if ((arr[i].browser == aBrowser) &&
 | |
|           (aIndex < 0 || aIndex == arr[i].index)) {
 | |
|         delete aBrowser.snapshots[arr[i].index];
 | |
|         arr.splice(i, 1);
 | |
|         if (requiresExactIndexMatch)
 | |
|           return; // Found and removed the only element.
 | |
|         i--; // Make sure to revisit the index that we just removed an
 | |
|              // element at.
 | |
|       }
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Adds a new snapshot reference for a given index and browser to the array
 | |
|    * of references to tracked snapshots.
 | |
|    *
 | |
|    * @param aIndex
 | |
|    *        The index in history of the new snapshot.
 | |
|    * @param aBrowser
 | |
|    *        The browser the new snapshot was taken in.
 | |
|    */
 | |
|   _addSnapshotRefToArray:
 | |
|   function HSA__addSnapshotRefToArray(aIndex, aBrowser) {
 | |
|     let id = { index: aIndex,
 | |
|                browser: aBrowser };
 | |
|     let arr = this._trackedSnapshots;
 | |
|     arr.unshift(id);
 | |
| 
 | |
|     while (arr.length > this._maxSnapshots) {
 | |
|       let lastElem = arr[arr.length - 1];
 | |
|       delete lastElem.browser.snapshots[lastElem.index].image;
 | |
|       delete lastElem.browser.snapshots[lastElem.index];
 | |
|       arr.splice(-1, 1);
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Converts a compressed blob to an Image object. In some situations
 | |
|    * (especially during fast swiping) aBlob may still be a canvas, not a
 | |
|    * compressed blob. In this case, we simply return the canvas.
 | |
|    *
 | |
|    * @param aBlob
 | |
|    *        The compressed blob to convert, or a canvas if a blob compression
 | |
|    *        couldn't complete before this method was called.
 | |
|    * @return A new Image object representing the converted blob.
 | |
|    */
 | |
|   _convertToImg: function HSA__convertToImg(aBlob) {
 | |
|     if (!aBlob)
 | |
|       return null;
 | |
| 
 | |
|     // Return aBlob if it's still a canvas and not a compressed blob yet.
 | |
|     if (aBlob instanceof HTMLCanvasElement)
 | |
|       return aBlob;
 | |
| 
 | |
|     let img = new Image();
 | |
|     let url = "";
 | |
|     try {
 | |
|       url = URL.createObjectURL(aBlob);
 | |
|       img.onload = function() {
 | |
|         URL.revokeObjectURL(url);
 | |
|       };
 | |
|     }
 | |
|     finally {
 | |
|       img.src = url;
 | |
|       return img;
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Scales the background of a given box element (which uses a given snapshot
 | |
|    * as background) based on a given scale factor.
 | |
|    * @param aSnapshot
 | |
|    *        The snapshot that is used as background of aBox.
 | |
|    * @param aScale
 | |
|    *        The scale factor to use.
 | |
|    * @param aBox
 | |
|    *        The box element that uses aSnapshot as background.
 | |
|    */
 | |
|   _scaleSnapshot: function HSA__scaleSnapshot(aSnapshot, aScale, aBox) {
 | |
|     if (aSnapshot && aScale != 1 && aBox) {
 | |
|       if (aSnapshot instanceof HTMLCanvasElement) {
 | |
|         aBox.style.backgroundSize =
 | |
|           aSnapshot.width / aScale + "px " + aSnapshot.height / aScale + "px";
 | |
|       } else {
 | |
|         // snapshot is instanceof HTMLImageElement
 | |
|         aSnapshot.addEventListener("load", function() {
 | |
|           aBox.style.backgroundSize =
 | |
|             aSnapshot.width / aScale + "px " + aSnapshot.height / aScale + "px";
 | |
|         });
 | |
|       }
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Sets the snapshot of the current page to the snapshot passed as parameter,
 | |
|    * or to the one previously stored for the current index in history if the
 | |
|    * parameter is null.
 | |
|    *
 | |
|    * @param aCanvas
 | |
|    *        The snapshot to set the current page to. If this parameter is null,
 | |
|    *        the previously stored snapshot for this index (if any) will be used.
 | |
|    */
 | |
|   _installCurrentPageSnapshot:
 | |
|   function HSA__installCurrentPageSnapshot(aCanvas) {
 | |
|     let currSnapshot = aCanvas;
 | |
|     let scale = window.devicePixelRatio;
 | |
|     if (!currSnapshot) {
 | |
|       let snapshots = gBrowser.selectedBrowser.snapshots || {};
 | |
|       let currIndex = this._historyIndex;
 | |
|       if (currIndex in snapshots) {
 | |
|         currSnapshot = this._convertToImg(snapshots[currIndex].image);
 | |
|         scale = snapshots[currIndex].scale;
 | |
|       }
 | |
|     }
 | |
|     this._scaleSnapshot(currSnapshot, scale, this._curBox ? this._curBox :
 | |
|                                                             null);
 | |
|     document.mozSetImageElement("historySwipeAnimationCurrentPageSnapshot",
 | |
|                                 currSnapshot);
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Sets the snapshots of the previous and next pages to the snapshots
 | |
|    * previously stored for their respective indeces.
 | |
|    */
 | |
|   _installPrevAndNextSnapshots:
 | |
|   function HSA__installPrevAndNextSnapshots() {
 | |
|     let snapshots = gBrowser.selectedBrowser.snapshots || [];
 | |
|     let currIndex = this._historyIndex;
 | |
|     let prevIndex = currIndex - 1;
 | |
|     let prevSnapshot = null;
 | |
|     if (prevIndex in snapshots) {
 | |
|       prevSnapshot = this._convertToImg(snapshots[prevIndex].image);
 | |
|       this._scaleSnapshot(prevSnapshot, snapshots[prevIndex].scale,
 | |
|                           this._prevBox);
 | |
|     }
 | |
|     document.mozSetImageElement("historySwipeAnimationPreviousPageSnapshot",
 | |
|                                 prevSnapshot);
 | |
| 
 | |
|     let nextIndex = currIndex + 1;
 | |
|     let nextSnapshot = null;
 | |
|     if (nextIndex in snapshots) {
 | |
|       nextSnapshot = this._convertToImg(snapshots[nextIndex].image);
 | |
|       this._scaleSnapshot(nextSnapshot, snapshots[nextIndex].scale,
 | |
|                           this._nextBox);
 | |
|     }
 | |
|     document.mozSetImageElement("historySwipeAnimationNextPageSnapshot",
 | |
|                                 nextSnapshot);
 | |
|   },
 | |
| };
 | 
