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