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