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