browser/base/content/browser-gestureSupport.js

Wed, 31 Dec 2014 06:09:35 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 06:09:35 +0100
changeset 0
6474c204b198
permissions
-rw-r--r--

Cloned upstream origin tor-browser at tor-browser-31.3.0esr-4.5-1-build1
revision ID fc1c9ff7c1b2defdbc039f12214767608f46423f for hacking purpose.

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

mercurial