Bumping manifests a=b2g-bump
[gecko.git] / browser / base / content / browser-gestureSupport.js
blob2b700dfd4c40c70e770cd6fcd8b1af005d315751
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   },
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.matches(":-moz-locale-dir(ltr)");
575     this._trackedSnapshots = [];
576     this._startingIndex = -1;
577     this._historyIndex = -1;
578     this._boxWidth = -1;
579     this._boxHeight = -1;
580     this._maxSnapshots = this._getMaxSnapshots();
581     this._lastSwipeDir = "";
582     this._direction = "horizontal";
584     // We only want to activate history swipe animations if we store snapshots.
585     // If we don't store any, we handle horizontal swipes without animations.
586     if (this._maxSnapshots > 0) {
587       this.active = true;
588       gBrowser.addEventListener("pagehide", this, false);
589       gBrowser.addEventListener("pageshow", this, false);
590       gBrowser.addEventListener("popstate", this, false);
591       gBrowser.addEventListener("DOMModalDialogClosed", this, false);
592       gBrowser.tabContainer.addEventListener("TabClose", this, false);
593     }
594   },
596   /**
597    * Uninitializes the support for history swipe animations.
598    */
599   uninit: function HSA_uninit() {
600     gBrowser.removeEventListener("pagehide", this, false);
601     gBrowser.removeEventListener("pageshow", this, false);
602     gBrowser.removeEventListener("popstate", this, false);
603     gBrowser.removeEventListener("DOMModalDialogClosed", this, false);
604     gBrowser.tabContainer.removeEventListener("TabClose", this, false);
606     this.active = false;
607     this.isLTR = false;
608   },
610   /**
611    * Starts the swipe animation and handles fast swiping (i.e. a swipe animation
612    * is already in progress when a new one is initiated).
613    *
614    * @param aIsVerticalSwipe
615    *        Whether we're dealing with a vertical swipe or not.
616    */
617   startAnimation: function HSA_startAnimation(aIsVerticalSwipe) {
618     this._direction = aIsVerticalSwipe ? "vertical" : "horizontal";
620     if (this.isAnimationRunning()) {
621       // If this is a horizontal scroll, or if this is a vertical scroll that
622       // was started while a horizontal scroll was still running, handle it as
623       // as a fast swipe. In the case of the latter scenario, this allows us to
624       // start the vertical animation without first loading the final page, or
625       // taking another snapshot. If vertical scrolls are initiated repeatedly
626       // without prior horizontal scroll we skip this and restart the animation
627       // from 0.
628       if (this._direction == "horizontal" || this._lastSwipeDir != "") {
629         gBrowser.stop();
630         this._lastSwipeDir = "RELOAD"; // just ensure that != ""
631         this._canGoBack = this.canGoBack();
632         this._canGoForward = this.canGoForward();
633         this._handleFastSwiping();
634       }
635     }
636     else {
637       this._startingIndex = gBrowser.webNavigation.sessionHistory.index;
638       this._historyIndex = this._startingIndex;
639       this._canGoBack = this.canGoBack();
640       this._canGoForward = this.canGoForward();
641       if (this.active) {
642         this._addBoxes();
643         this._takeSnapshot();
644         this._installPrevAndNextSnapshots();
645         this._lastSwipeDir = "";
646       }
647     }
648     this.updateAnimation(0);
649   },
651   /**
652    * Stops the swipe animation.
653    */
654   stopAnimation: function HSA_stopAnimation() {
655     gHistorySwipeAnimation._removeBoxes();
656     this._historyIndex = gBrowser.webNavigation.sessionHistory.index;
657   },
659   /**
660    * Updates the animation between two pages in history.
661    *
662    * @param aVal
663    *        A floating point value that represents the progress of the
664    *        swipe gesture.
665    */
666   updateAnimation: function HSA_updateAnimation(aVal) {
667     if (!this.isAnimationRunning()) {
668       return;
669     }
671     // We use the following value to decrease the bounce effect when scrolling
672     // to the top or bottom of the page, or when swiping back/forward past the
673     // browsing history. This value was determined experimentally.
674     let dampValue = 4;
675     if (this._direction == "vertical") {
676       this._prevBox.collapsed = true;
677       this._nextBox.collapsed = true;
678       this._positionBox(this._curBox, -1 * aVal / dampValue);
679     } else if ((aVal >= 0 && this.isLTR) ||
680                (aVal <= 0 && !this.isLTR)) {
681       let tempDampValue = 1;
682       if (this._canGoBack) {
683         this._prevBox.collapsed = false;
684       } else {
685         tempDampValue = dampValue;
686         this._prevBox.collapsed = true;
687       }
689       // The current page is pushed to the right (LTR) or left (RTL),
690       // the intention is to go back.
691       // If there is a page to go back to, it should show in the background.
692       this._positionBox(this._curBox, aVal / tempDampValue);
694       // The forward page should be pushed offscreen all the way to the right.
695       this._positionBox(this._nextBox, 1);
696     } else {
697       // The intention is to go forward. If there is a page to go forward to,
698       // it should slide in from the right (LTR) or left (RTL).
699       // Otherwise, the current page should slide to the left (LTR) or
700       // right (RTL) and the backdrop should appear in the background.
701       // For the backdrop to be visible in that case, the previous page needs
702       // to be hidden (if it exists).
703       if (this._canGoForward) {
704         this._nextBox.collapsed = false;
705         let offset = this.isLTR ? 1 : -1;
706         this._positionBox(this._curBox, 0);
707         this._positionBox(this._nextBox, offset + aVal);
708       } else {
709         this._prevBox.collapsed = true;
710         this._positionBox(this._curBox, aVal / dampValue);
711       }
712     }
713   },
715   /**
716    * Event handler for events relevant to the history swipe animation.
717    *
718    * @param aEvent
719    *        An event to process.
720    */
721   handleEvent: function HSA_handleEvent(aEvent) {
722     let browser = gBrowser.selectedBrowser;
723     switch (aEvent.type) {
724       case "TabClose":
725         let browserForTab = gBrowser.getBrowserForTab(aEvent.target);
726         this._removeTrackedSnapshot(-1, browserForTab);
727         break;
728       case "DOMModalDialogClosed":
729         this.stopAnimation();
730         break;
731       case "pageshow":
732         if (aEvent.target == browser.contentDocument) {
733           this.stopAnimation();
734         }
735         break;
736       case "popstate":
737         if (aEvent.target == browser.contentDocument.defaultView) {
738           this.stopAnimation();
739         }
740         break;
741       case "pagehide":
742         if (aEvent.target == browser.contentDocument) {
743           // Take and compress a snapshot of a page whenever it's about to be
744           // navigated away from. We already have a snapshot of the page if an
745           // animation is running, so we're left with compressing it.
746           if (!this.isAnimationRunning()) {
747             this._takeSnapshot();
748           }
749           this._compressSnapshotAtCurrentIndex();
750         }
751         break;
752     }
753   },
755   /**
756    * Checks whether the history swipe animation is currently running or not.
757    *
758    * @return true if the animation is currently running, false otherwise.
759    */
760   isAnimationRunning: function HSA_isAnimationRunning() {
761     return !!this._container;
762   },
764   /**
765    * Process a swipe event based on the given direction.
766    *
767    * @param aEvent
768    *        The swipe event to handle
769    * @param aDir
770    *        The direction for the swipe event
771    */
772   processSwipeEvent: function HSA_processSwipeEvent(aEvent, aDir) {
773     if (aDir == "RIGHT")
774       this._historyIndex += this.isLTR ? 1 : -1;
775     else if (aDir == "LEFT")
776       this._historyIndex += this.isLTR ? -1 : 1;
777     else
778       return;
779     this._lastSwipeDir = aDir;
780   },
782   /**
783    * Checks if there is a page in the browser history to go back to.
784    *
785    * @return true if there is a previous page in history, false otherwise.
786    */
787   canGoBack: function HSA_canGoBack() {
788     if (this.isAnimationRunning())
789       return this._doesIndexExistInHistory(this._historyIndex - 1);
790     return gBrowser.webNavigation.canGoBack;
791   },
793   /**
794    * Checks if there is a page in the browser history to go forward to.
795    *
796    * @return true if there is a next page in history, false otherwise.
797    */
798   canGoForward: function HSA_canGoForward() {
799     if (this.isAnimationRunning())
800       return this._doesIndexExistInHistory(this._historyIndex + 1);
801     return gBrowser.webNavigation.canGoForward;
802   },
804   /**
805    * Used to notify the history swipe animation that the OS sent a swipe end
806    * event and that we should navigate to the page that the user swiped to, if
807    * any. This will also result in the animation overlay to be torn down.
808    */
809   swipeEndEventReceived: function HSA_swipeEndEventReceived() {
810     if (this._lastSwipeDir != "" && this._historyIndex != this._startingIndex)
811       this._navigateToHistoryIndex();
812     else
813       this.stopAnimation();
814   },
816   /**
817    * Checks whether a particular index exists in the browser history or not.
818    *
819    * @param aIndex
820    *        The index to check for availability for in the history.
821    * @return true if the index exists in the browser history, false otherwise.
822    */
823   _doesIndexExistInHistory: function HSA__doesIndexExistInHistory(aIndex) {
824     try {
825       gBrowser.webNavigation.sessionHistory.getEntryAtIndex(aIndex, false);
826     }
827     catch(ex) {
828       return false;
829     }
830     return true;
831   },
833   /**
834    * Navigates to the index in history that is currently being tracked by
835    * |this|.
836    */
837   _navigateToHistoryIndex: function HSA__navigateToHistoryIndex() {
838     if (this._doesIndexExistInHistory(this._historyIndex))
839       gBrowser.webNavigation.gotoIndex(this._historyIndex);
840     else
841       this.stopAnimation();
842   },
844   /**
845    * Checks to see if history swipe animations are supported by this
846    * platform/configuration.
847    *
848    * return true if supported, false otherwise.
849    */
850   _isSupported: function HSA__isSupported() {
851     return window.matchMedia("(-moz-swipe-animation-enabled)").matches;
852   },
854   /**
855    * Handle fast swiping (i.e. a swipe animation is already in
856    * progress when a new one is initiated). This will swap out the snapshots
857    * used in the previous animation with the appropriate new ones.
858    */
859   _handleFastSwiping: function HSA__handleFastSwiping() {
860     this._installCurrentPageSnapshot(null);
861     this._installPrevAndNextSnapshots();
862   },
864   /**
865    * Adds the boxes that contain the snapshots used during the swipe animation.
866    */
867   _addBoxes: function HSA__addBoxes() {
868     let browserStack =
869       document.getAnonymousElementByAttribute(gBrowser.getNotificationBox(),
870                                               "class", "browserStack");
871     this._container = this._createElement("historySwipeAnimationContainer",
872                                           "stack");
873     browserStack.appendChild(this._container);
875     this._prevBox = this._createElement("historySwipeAnimationPreviousPage",
876                                         "box");
877     this._container.appendChild(this._prevBox);
879     this._curBox = this._createElement("historySwipeAnimationCurrentPage",
880                                        "box");
881     this._container.appendChild(this._curBox);
883     this._nextBox = this._createElement("historySwipeAnimationNextPage",
884                                         "box");
885     this._container.appendChild(this._nextBox);
887     // Cache width and height.
888     this._boxWidth = this._curBox.getBoundingClientRect().width;
889     this._boxHeight = this._curBox.getBoundingClientRect().height;
890   },
892   /**
893    * Removes the boxes.
894    */
895   _removeBoxes: function HSA__removeBoxes() {
896     this._curBox = null;
897     this._prevBox = null;
898     this._nextBox = null;
899     if (this._container)
900       this._container.parentNode.removeChild(this._container);
901     this._container = null;
902     this._boxWidth = -1;
903     this._boxHeight = -1;
904   },
906   /**
907    * Creates an element with a given identifier and tag name.
908    *
909    * @param aID
910    *        An identifier to create the element with.
911    * @param aTagName
912    *        The name of the tag to create the element for.
913    * @return the newly created element.
914    */
915   _createElement: function HSA__createElement(aID, aTagName) {
916     let XULNS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
917     let element = document.createElementNS(XULNS, aTagName);
918     element.id = aID;
919     return element;
920   },
922   /**
923    * Moves a given box to a given X coordinate position.
924    *
925    * @param aBox
926    *        The box element to position.
927    * @param aPosition
928    *        The position (in X coordinates) to move the box element to.
929    */
930   _positionBox: function HSA__positionBox(aBox, aPosition) {
931     let transform = "";
933     if (this._direction == "vertical")
934       transform = "translateY(" + this._boxHeight * aPosition + "px)";
935     else
936       transform = "translateX(" + this._boxWidth * aPosition + "px)";
938     aBox.style.transform = transform;
939   },
941   /**
942    * Verifies that we're ready to take snapshots based on the global pref and
943    * the current index in history.
944    *
945    * @return true if we're ready to take snapshots, false otherwise.
946    */
947   _readyToTakeSnapshots: function HSA__readyToTakeSnapshots() {
948     if ((this._maxSnapshots < 1) ||
949         (gBrowser.webNavigation.sessionHistory.index < 0)) {
950       return false;
951     }
952     return true;
953   },
955   /**
956    * Takes a snapshot of the page the browser is currently on.
957    */
958   _takeSnapshot: function HSA__takeSnapshot() {
959     if (!this._readyToTakeSnapshots()) {
960       return;
961     }
963     let canvas = null;
965     TelemetryStopwatch.start("FX_GESTURE_TAKE_SNAPSHOT_OF_PAGE");
966     try {
967       let browser = gBrowser.selectedBrowser;
968       let r = browser.getBoundingClientRect();
969       canvas = document.createElementNS("http://www.w3.org/1999/xhtml",
970                                         "canvas");
971       canvas.mozOpaque = true;
972       let scale = window.devicePixelRatio;
973       canvas.width = r.width * scale;
974       canvas.height = r.height * scale;
975       let ctx = canvas.getContext("2d");
976       let zoom = browser.markupDocumentViewer.fullZoom * scale;
977       ctx.scale(zoom, zoom);
978       ctx.drawWindow(browser.contentWindow,
979                      0, 0, canvas.width / zoom, canvas.height / zoom, "white",
980                      ctx.DRAWWINDOW_DO_NOT_FLUSH | ctx.DRAWWINDOW_DRAW_VIEW |
981                      ctx.DRAWWINDOW_ASYNC_DECODE_IMAGES |
982                      ctx.DRAWWINDOW_USE_WIDGET_LAYERS);
983     } finally {
984       TelemetryStopwatch.finish("FX_GESTURE_TAKE_SNAPSHOT_OF_PAGE");
985     }
987     TelemetryStopwatch.start("FX_GESTURE_INSTALL_SNAPSHOT_OF_PAGE");
988     try {
989       this._installCurrentPageSnapshot(canvas);
990       this._assignSnapshotToCurrentBrowser(canvas);
991     } finally {
992       TelemetryStopwatch.finish("FX_GESTURE_INSTALL_SNAPSHOT_OF_PAGE");
993     }
994   },
996   /**
997    * Retrieves the maximum number of snapshots that should be kept in memory.
998    * This limit is a global limit and is valid across all open tabs.
999    */
1000   _getMaxSnapshots: function HSA__getMaxSnapshots() {
1001     return gPrefService.getIntPref("browser.snapshots.limit");
1002   },
1004   /**
1005    * Adds a snapshot to the list and initiates the compression of said snapshot.
1006    * Once the compression is completed, it will replace the uncompressed
1007    * snapshot in the list.
1008    *
1009    * @param aCanvas
1010    *        The snapshot to add to the list and compress.
1011    */
1012   _assignSnapshotToCurrentBrowser:
1013   function HSA__assignSnapshotToCurrentBrowser(aCanvas) {
1014     let browser = gBrowser.selectedBrowser;
1015     let currIndex = browser.webNavigation.sessionHistory.index;
1017     this._removeTrackedSnapshot(currIndex, browser);
1018     this._addSnapshotRefToArray(currIndex, browser);
1020     if (!("snapshots" in browser))
1021       browser.snapshots = [];
1022     let snapshots = browser.snapshots;
1023     // Temporarily store the canvas as the compressed snapshot.
1024     // This avoids a blank page if the user swipes quickly
1025     // between pages before the compression could complete.
1026     snapshots[currIndex] = {
1027       image: aCanvas,
1028       scale: window.devicePixelRatio
1029     };
1030   },
1032   /**
1033    * Compresses the HTMLCanvasElement that's stored at the current history
1034    * index in the snapshot array and stores the compressed image in its place.
1035    */
1036   _compressSnapshotAtCurrentIndex:
1037   function HSA__compressSnapshotAtCurrentIndex() {
1038     if (!this._readyToTakeSnapshots()) {
1039       // We didn't take a snapshot earlier because we weren't ready to, so
1040       // there's nothing to compress.
1041       return;
1042     }
1044     TelemetryStopwatch.start("FX_GESTURE_COMPRESS_SNAPSHOT_OF_PAGE");
1045     try {
1046       let browser = gBrowser.selectedBrowser;
1047       let snapshots = browser.snapshots;
1048       let currIndex = browser.webNavigation.sessionHistory.index;
1050       // Kick off snapshot compression.
1051       let canvas = snapshots[currIndex].image;
1052       canvas.toBlob(function(aBlob) {
1053           if (snapshots[currIndex]) {
1054             snapshots[currIndex].image = aBlob;
1055           }
1056         }, "image/png"
1057       );
1058     } finally {
1059       TelemetryStopwatch.finish("FX_GESTURE_COMPRESS_SNAPSHOT_OF_PAGE");
1060     }
1061   },
1063   /**
1064    * Removes a snapshot identified by the browser and index in the array of
1065    * snapshots for that browser, if present. If no snapshot could be identified
1066    * the method simply returns without taking any action. If aIndex is negative,
1067    * all snapshots for a particular browser will be removed.
1068    *
1069    * @param aIndex
1070    *        The index in history of the new snapshot, or negative value if all
1071    *        snapshots for a browser should be removed.
1072    * @param aBrowser
1073    *        The browser the new snapshot was taken in.
1074    */
1075   _removeTrackedSnapshot: function HSA__removeTrackedSnapshot(aIndex, aBrowser) {
1076     let arr = this._trackedSnapshots;
1077     let requiresExactIndexMatch = aIndex >= 0;
1078     for (let i = 0; i < arr.length; i++) {
1079       if ((arr[i].browser == aBrowser) &&
1080           (aIndex < 0 || aIndex == arr[i].index)) {
1081         delete aBrowser.snapshots[arr[i].index];
1082         arr.splice(i, 1);
1083         if (requiresExactIndexMatch)
1084           return; // Found and removed the only element.
1085         i--; // Make sure to revisit the index that we just removed an
1086              // element at.
1087       }
1088     }
1089   },
1091   /**
1092    * Adds a new snapshot reference for a given index and browser to the array
1093    * of references to tracked snapshots.
1094    *
1095    * @param aIndex
1096    *        The index in history of the new snapshot.
1097    * @param aBrowser
1098    *        The browser the new snapshot was taken in.
1099    */
1100   _addSnapshotRefToArray:
1101   function HSA__addSnapshotRefToArray(aIndex, aBrowser) {
1102     let id = { index: aIndex,
1103                browser: aBrowser };
1104     let arr = this._trackedSnapshots;
1105     arr.unshift(id);
1107     while (arr.length > this._maxSnapshots) {
1108       let lastElem = arr[arr.length - 1];
1109       delete lastElem.browser.snapshots[lastElem.index].image;
1110       delete lastElem.browser.snapshots[lastElem.index];
1111       arr.splice(-1, 1);
1112     }
1113   },
1115   /**
1116    * Converts a compressed blob to an Image object. In some situations
1117    * (especially during fast swiping) aBlob may still be a canvas, not a
1118    * compressed blob. In this case, we simply return the canvas.
1119    *
1120    * @param aBlob
1121    *        The compressed blob to convert, or a canvas if a blob compression
1122    *        couldn't complete before this method was called.
1123    * @return A new Image object representing the converted blob.
1124    */
1125   _convertToImg: function HSA__convertToImg(aBlob) {
1126     if (!aBlob)
1127       return null;
1129     // Return aBlob if it's still a canvas and not a compressed blob yet.
1130     if (aBlob instanceof HTMLCanvasElement)
1131       return aBlob;
1133     let img = new Image();
1134     let url = "";
1135     try {
1136       url = URL.createObjectURL(aBlob);
1137       img.onload = function() {
1138         URL.revokeObjectURL(url);
1139       };
1140     }
1141     finally {
1142       img.src = url;
1143       return img;
1144     }
1145   },
1147   /**
1148    * Scales the background of a given box element (which uses a given snapshot
1149    * as background) based on a given scale factor.
1150    * @param aSnapshot
1151    *        The snapshot that is used as background of aBox.
1152    * @param aScale
1153    *        The scale factor to use.
1154    * @param aBox
1155    *        The box element that uses aSnapshot as background.
1156    */
1157   _scaleSnapshot: function HSA__scaleSnapshot(aSnapshot, aScale, aBox) {
1158     if (aSnapshot && aScale != 1 && aBox) {
1159       if (aSnapshot instanceof HTMLCanvasElement) {
1160         aBox.style.backgroundSize =
1161           aSnapshot.width / aScale + "px " + aSnapshot.height / aScale + "px";
1162       } else {
1163         // snapshot is instanceof HTMLImageElement
1164         aSnapshot.addEventListener("load", function() {
1165           aBox.style.backgroundSize =
1166             aSnapshot.width / aScale + "px " + aSnapshot.height / aScale + "px";
1167         });
1168       }
1169     }
1170   },
1172   /**
1173    * Sets the snapshot of the current page to the snapshot passed as parameter,
1174    * or to the one previously stored for the current index in history if the
1175    * parameter is null.
1176    *
1177    * @param aCanvas
1178    *        The snapshot to set the current page to. If this parameter is null,
1179    *        the previously stored snapshot for this index (if any) will be used.
1180    */
1181   _installCurrentPageSnapshot:
1182   function HSA__installCurrentPageSnapshot(aCanvas) {
1183     let currSnapshot = aCanvas;
1184     let scale = window.devicePixelRatio;
1185     if (!currSnapshot) {
1186       let snapshots = gBrowser.selectedBrowser.snapshots || {};
1187       let currIndex = this._historyIndex;
1188       if (currIndex in snapshots) {
1189         currSnapshot = this._convertToImg(snapshots[currIndex].image);
1190         scale = snapshots[currIndex].scale;
1191       }
1192     }
1193     this._scaleSnapshot(currSnapshot, scale, this._curBox ? this._curBox :
1194                                                             null);
1195     document.mozSetImageElement("historySwipeAnimationCurrentPageSnapshot",
1196                                 currSnapshot);
1197   },
1199   /**
1200    * Sets the snapshots of the previous and next pages to the snapshots
1201    * previously stored for their respective indeces.
1202    */
1203   _installPrevAndNextSnapshots:
1204   function HSA__installPrevAndNextSnapshots() {
1205     let snapshots = gBrowser.selectedBrowser.snapshots || [];
1206     let currIndex = this._historyIndex;
1207     let prevIndex = currIndex - 1;
1208     let prevSnapshot = null;
1209     if (prevIndex in snapshots) {
1210       prevSnapshot = this._convertToImg(snapshots[prevIndex].image);
1211       this._scaleSnapshot(prevSnapshot, snapshots[prevIndex].scale,
1212                           this._prevBox);
1213     }
1214     document.mozSetImageElement("historySwipeAnimationPreviousPageSnapshot",
1215                                 prevSnapshot);
1217     let nextIndex = currIndex + 1;
1218     let nextSnapshot = null;
1219     if (nextIndex in snapshots) {
1220       nextSnapshot = this._convertToImg(snapshots[nextIndex].image);
1221       this._scaleSnapshot(nextSnapshot, snapshots[nextIndex].scale,
1222                           this._nextBox);
1223     }
1224     document.mozSetImageElement("historySwipeAnimationNextPageSnapshot",
1225                                 nextSnapshot);
1226   },