diff --git a/CLOBBER b/CLOBBER index e3f33ae2d04..67396c55773 100644 --- a/CLOBBER +++ b/CLOBBER @@ -17,4 +17,4 @@ # # Modifying this file will now automatically clobber the buildbot machines \o/ # -Bug 856349 broke Windows b2g builds. +Bug 678392 - Added new attribute to nsIDOMSimpleGestureEvent interface. diff --git a/browser/app/profile/firefox.js b/browser/app/profile/firefox.js index bcae0167be3..ec05c759ef9 100644 --- a/browser/app/profile/firefox.js +++ b/browser/app/profile/firefox.js @@ -540,6 +540,8 @@ pref("browser.gesture.twist.left", "cmd_gestureRotateLeft"); pref("browser.gesture.twist.end", "cmd_gestureRotateEnd"); pref("browser.gesture.tap", "cmd_fullZoomReset"); +pref("browser.snapshots.limit", 0); + // 0: Nothing happens // 1: Scrolling contents // 2: Go back or go forward, in your history diff --git a/browser/base/content/browser-gestureSupport.js b/browser/base/content/browser-gestureSupport.js new file mode 100644 index 00000000000..3b391711239 --- /dev/null +++ b/browser/base/content/browser-gestureSupport.js @@ -0,0 +1,1044 @@ +# 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; + + gestureEvents.forEach(function (event) addRemove("Moz" + event, this, true), + this); + }, + + /** + * 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": + aEvent.preventDefault(); + this._setupSwipeGesture(aEvent); + 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 the history swipe animations for a swipe gesture event, if enabled. + * + * @param aEvent + * The swipe gesture start event. + */ + _setupSwipeGesture: function GS__setupSwipeGesture(aEvent) { + if (!this._swipeNavigatesHistory(aEvent) || !gHistorySwipeAnimation.active) + return; + + 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(); + + 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) {}; + } + }, + + /** + * 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 = []; + ["shift", "alt", "ctrl", "meta"].forEach(function (key) { + 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() { + 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() || this._getMaxSnapshots() < 1) + return; + + gBrowser.addEventListener("pagehide", this, false); + gBrowser.addEventListener("pageshow", this, false); + gBrowser.addEventListener("popstate", this, false); + gBrowser.tabContainer.addEventListener("TabClose", this, false); + + this.active = true; + this.isLTR = document.documentElement.mozMatchesSelector( + ":-moz-locale-dir(ltr)"); + this._trackedSnapshots = []; + this._historyIndex = -1; + this._boxWidth = -1; + this._maxSnapshots = this._getMaxSnapshots(); + this._lastSwipeDir = ""; + }, + + /** + * 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.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). + */ + startAnimation: function HSA_startAnimation() { + if (this.isAnimationRunning()) { + gBrowser.stop(); + this._lastSwipeDir = "RELOAD"; // just ensure that != "" + this._canGoBack = this.canGoBack(); + this._canGoForward = this.canGoForward(); + this._handleFastSwiping(); + } + else { + this._historyIndex = gBrowser.webNavigation.sessionHistory.index; + this._canGoBack = this.canGoBack(); + this._canGoForward = this.canGoForward(); + this._takeSnapshot(); + this._installPrevAndNextSnapshots(); + this._addBoxes(); + this._lastSwipeDir = ""; + } + this.updateAnimation(0); + }, + + /** + * Stops the swipe animation. + */ + stopAnimation: function HSA_stopAnimation() { + gHistorySwipeAnimation._removeBoxes(); + }, + + /** + * 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; + + if ((aVal >= 0 && this.isLTR) || + (aVal <= 0 && !this.isLTR)) { + if (aVal > 1) + aVal = 1; // Cap value to avoid sliding the page further than allowed. + + if (this._canGoBack) + this._prevBox.collapsed = false; + else + 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); + + // The forward page should be pushed offscreen all the way to the right. + this._positionBox(this._nextBox, 1); + } + else { + if (aVal < -1) + aVal = -1; // Cap value to avoid sliding the page further than allowed. + + // 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) { + let offset = this.isLTR ? 1 : -1; + this._positionBox(this._curBox, 0); + this._positionBox(this._nextBox, offset + aVal); // aVal is negative + } + else { + this._prevBox.collapsed = true; + this._positionBox(this._curBox, aVal); + } + } + }, + + /** + * Event handler for events relevant to the history swipe animation. + * + * @param aEvent + * An event to process. + */ + handleEvent: function HSA_handleEvent(aEvent) { + switch (aEvent.type) { + case "TabClose": + let browser = gBrowser.getBrowserForTab(aEvent.target); + this._removeTrackedSnapshot(-1, browser); + break; + case "pageshow": + case "popstate": + if (this.isAnimationRunning()) { + if (aEvent.target != gBrowser.selectedBrowser.contentDocument) + break; + this.stopAnimation(); + } + this._historyIndex = gBrowser.webNavigation.sessionHistory.index; + break; + case "pagehide": + if (aEvent.target == gBrowser.selectedBrowser.contentDocument) { + // Take a snapshot of a page whenever it's about to be navigated away + // from. + this._takeSnapshot(); + } + 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._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); + } + }, + + /** + * 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); + + this._boxWidth = this._curBox.getBoundingClientRect().width; // cache width + }, + + /** + * 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; + }, + + /** + * 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) { + aBox.style.transform = "translateX(" + this._boxWidth * aPosition + "px)"; + }, + + /** + * Takes a snapshot of the page the browser is currently on. + */ + _takeSnapshot: function HSA__takeSnapshot() { + if ((this._maxSnapshots < 1) || + (gBrowser.webNavigation.sessionHistory.index < 0)) + return; + + let browser = gBrowser.selectedBrowser; + let r = browser.getBoundingClientRect(); + let canvas = document.createElementNS("http://www.w3.org/1999/xhtml", + "canvas"); + canvas.mozOpaque = true; + canvas.width = r.width; + canvas.height = r.height; + let ctx = canvas.getContext("2d"); + let zoom = browser.markupDocumentViewer.fullZoom; + ctx.scale(zoom, zoom); + ctx.drawWindow(browser.contentWindow, 0, 0, r.width, r.height, "white", + ctx.DRAWWINDOW_DO_NOT_FLUSH | ctx.DRAWWINDOW_DRAW_VIEW | + ctx.DRAWWINDOW_ASYNC_DECODE_IMAGES | + ctx.DRAWWINDOW_USE_WIDGET_LAYERS); + + this._installCurrentPageSnapshot(canvas); + this._assignSnapshotToCurrentBrowser(canvas); + }, + + /** + * 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] = aCanvas; + + // Kick off snapshot compression. + aCanvas.toBlob(function(aBlob) { + snapshots[currIndex] = aBlob; + }, "image/png" + ); + }, + + /** + * 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]; + 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 = URL.createObjectURL(aBlob); + img.onload = function() { + URL.revokeObjectURL(url); + }; + img.src = url; + return img; + }, + + /** + * 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; + if (!currSnapshot) { + let snapshots = gBrowser.selectedBrowser.snapshots || {}; + let currIndex = this._historyIndex; + if (currIndex in snapshots) + currSnapshot = this._convertToImg(snapshots[currIndex]); + } + 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]); + document.mozSetImageElement("historySwipeAnimationPreviousPageSnapshot", + prevSnapshot); + + let nextIndex = currIndex + 1; + let nextSnapshot = null; + if (nextIndex in snapshots) + nextSnapshot = this._convertToImg(snapshots[nextIndex]); + document.mozSetImageElement("historySwipeAnimationNextPageSnapshot", + nextSnapshot); + }, +}; diff --git a/browser/base/content/browser.css b/browser/base/content/browser.css index 3f868a83d09..d1e127efcec 100644 --- a/browser/base/content/browser.css +++ b/browser/base/content/browser.css @@ -358,6 +358,30 @@ window[chromehidden~="toolbar"] toolbar:not(.toolbar-primary):not(.chromeclass-m } %endif +/* History Swipe Animation */ + +#historySwipeAnimationContainer { + overflow: hidden; +} + +#historySwipeAnimationPreviousPage, +#historySwipeAnimationCurrentPage, +#historySwipeAnimationNextPage { + background: none top left no-repeat white; +} + +#historySwipeAnimationPreviousPage { + background-image: -moz-element(#historySwipeAnimationPreviousPageSnapshot); +} + +#historySwipeAnimationCurrentPage { + background-image: -moz-element(#historySwipeAnimationCurrentPageSnapshot); +} + +#historySwipeAnimationNextPage { + background-image: -moz-element(#historySwipeAnimationNextPageSnapshot); +} + /* Identity UI */ #identity-popup-content-box.unknownIdentity > #identity-popup-connectedToLabel , #identity-popup-content-box.unknownIdentity > #identity-popup-runByLabel , diff --git a/browser/base/content/browser.js b/browser/base/content/browser.js index 5bc9d3fe83e..481d77a7ae7 100644 --- a/browser/base/content/browser.js +++ b/browser/base/content/browser.js @@ -157,6 +157,7 @@ let gInitialPages = [ #include browser-tabview.js #include browser-thumbnails.js #include browser-webrtcUI.js +#include browser-gestureSupport.js #ifdef MOZ_DATA_REPORTING #include browser-data-submission-info-bar.js @@ -724,396 +725,6 @@ const gFormSubmitObserver = { } }; -// 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 = ["SwipeGesture", - "MagnifyGestureStart", "MagnifyGestureUpdate", "MagnifyGesture", - "RotateGestureStart", "RotateGestureUpdate", "RotateGesture", - "TapGesture", "PressTapGesture"]; - - let addRemove = aAddListener ? window.addEventListener : - window.removeEventListener; - - gestureEvents.forEach(function (event) addRemove("Moz" + event, this, true), - this); - }, - - /** - * 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 "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); - }, - - /** - * 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 - * - * @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) - */ - _doAction: function GS__doAction(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 = []; - ["shift", "alt", "ctrl", "meta"].forEach(function (key) { - 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) - continue; - - let node = document.getElementById(command); - 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(command); - } - - break; - } - }, - - /** - * Convert continual motion events into an action if it exceeds a threshold - * in a given direction. This function will be set by _setupGesture to - * capture state that needs to be shared across multiple gesture updates. - * - * @param aEvent - * The continual motion update event to handle - */ - _doUpdate: 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._doAction(aEvent, ["swipe", dir.toLowerCase()]); - break; - } - } - }, - - /** - * 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() { - 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); - }, -}; - var gBrowserInit = { onLoad: function() { // window.arguments[0]: URI to load (string), or an nsISupportsArray of @@ -1212,6 +823,9 @@ var gBrowserInit = { // setup simple gestures support gGestureSupport.init(true); + // setup history swipe animation + gHistorySwipeAnimation.init(); + if (window.opener && !window.opener.closed) { let openerSidebarBox = window.opener.document.getElementById("sidebar-box"); // If the opener had a sidebar, open the same sidebar in our window. @@ -1689,6 +1303,8 @@ var gBrowserInit = { gGestureSupport.init(false); + gHistorySwipeAnimation.uninit(); + FullScreen.cleanup(); Services.obs.removeObserver(gPluginHandler.pluginCrashed, "plugin-crashed"); @@ -1908,7 +1524,6 @@ var nonBrowserWindowDelayedStartup = gBrowserInit.nonBrowserWindowDelayedStartup var nonBrowserWindowShutdown = gBrowserInit.nonBrowserWindowShutdown.bind(gBrowserInit); #endif - function HandleAppCommandEvent(evt) { switch (evt.command) { case "Back": @@ -2774,7 +2389,7 @@ let BrowserOnClick = { // we can fetch a site-specific report, for phishing, we redirect // to the generic page describing phishing protection. - // We log even if malware/phishing info URL couldn't be found: + // We log even if malware/phishing info URL couldn't be found: // the measurement is for how many users clicked the WHY BLOCKED button secHistogram.add(nsISecTel[bucketName + "WHY_BLOCKED"]); @@ -3257,7 +2872,7 @@ const DOMLinkHandler = { aLink, aLink.type, null) != Ci.nsIContentPolicy.ACCEPT) return null; - + try { uri.userPass = ""; } catch(e) { @@ -3914,7 +3529,7 @@ function updateCharacterEncodingMenuState() let appDevCharsetMenu = document.getElementById("appmenu_developer_charsetMenu"); // gBrowser is null on Mac when the menubar shows in the context of - // non-browser windows. The above elements may be null depending on + // non-browser windows. The above elements may be null depending on // what parts of the menubar are present. E.g. no app menu on Mac. if (gBrowser && gBrowser.docShell && diff --git a/browser/base/content/test/Makefile.in b/browser/base/content/test/Makefile.in index 6ed74fd997a..ef46beba26e 100644 --- a/browser/base/content/test/Makefile.in +++ b/browser/base/content/test/Makefile.in @@ -145,6 +145,9 @@ _BROWSER_FILES = \ browser_bug647886.js \ browser_bug655584.js \ browser_bug664672.js \ + browser_bug678392.js \ + browser_bug678392-1.html \ + browser_bug678392-2.html \ browser_bug710878.js \ browser_bug719271.js \ browser_bug724239.js \ diff --git a/browser/base/content/test/browser_bug678392-1.html b/browser/base/content/test/browser_bug678392-1.html new file mode 100644 index 00000000000..c3b235dd062 --- /dev/null +++ b/browser/base/content/test/browser_bug678392-1.html @@ -0,0 +1,12 @@ + + + + + + bug678392 - 1 + + +bug 678392 test page 1 + + \ No newline at end of file diff --git a/browser/base/content/test/browser_bug678392-2.html b/browser/base/content/test/browser_bug678392-2.html new file mode 100644 index 00000000000..9b18efcf74d --- /dev/null +++ b/browser/base/content/test/browser_bug678392-2.html @@ -0,0 +1,12 @@ + + + + + + bug678392 - 2 + + +bug 678392 test page 2 + + \ No newline at end of file diff --git a/browser/base/content/test/browser_bug678392.js b/browser/base/content/test/browser_bug678392.js new file mode 100644 index 00000000000..a9123cabba7 --- /dev/null +++ b/browser/base/content/test/browser_bug678392.js @@ -0,0 +1,192 @@ +/* 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/. */ + +let HTTPROOT = "http://example.com/browser/browser/base/content/test/"; + +function maxSnapshotOverride() { + return 5; +} + +function test() { + waitForExplicitFinish(); + + BrowserOpenTab(); + let tab = gBrowser.selectedTab; + registerCleanupFunction(function () { gBrowser.removeTab(tab); }); + + ok(gHistorySwipeAnimation, "gHistorySwipeAnimation exists."); + + if (!gHistorySwipeAnimation._isSupported()) { + is(gHistorySwipeAnimation.active, false, "History swipe animation is not " + + "active when not supported by the platform."); + finish(); + return; + } + + gHistorySwipeAnimation._getMaxSnapshots = maxSnapshotOverride; + gHistorySwipeAnimation.init(); + + is(gHistorySwipeAnimation.active, true, "History swipe animation support " + + "was successfully initialized when supported."); + + cleanupArray(); + load(gBrowser.selectedTab, HTTPROOT + "browser_bug678392-2.html", test0); +} + +function load(aTab, aUrl, aCallback) { + aTab.linkedBrowser.addEventListener("pageshow", function onpageshow(aEvent) { + aEvent.currentTarget.removeEventListener("pageshow", onpageshow, false); + waitForFocus(aCallback, content); + }, false); + aTab.linkedBrowser.loadURI(aUrl); +} + +function cleanupArray() { + let arr = gHistorySwipeAnimation._trackedSnapshots; + while (arr.length > 0) { + delete arr[0].browser.snapshots[arr[0].index]; // delete actual snapshot + arr.splice(0, 1); + } +} + +function testArrayCleanup() { + // Test cleanup of array of tracked snapshots. + let arr = gHistorySwipeAnimation._trackedSnapshots; + is(arr.length, 0, "Snapshots were removed correctly from the array of " + + "tracked snapshots."); +} + +function test0() { + // Test growing of array of tracked snapshots. + let tab = gBrowser.selectedTab; + + load(tab, HTTPROOT + "browser_bug678392-1.html", function() { + ok(gHistorySwipeAnimation._trackedSnapshots, "Array for snapshot " + + "tracking is initialized."); + is(gHistorySwipeAnimation._trackedSnapshots.length, 1, "Snapshot array " + + "has correct length of 1 after loading one page."); + load(tab, HTTPROOT + "browser_bug678392-2.html", function() { + is(gHistorySwipeAnimation._trackedSnapshots.length, 2, "Snapshot array " + + " has correct length of 2 after loading two pages."); + load(tab, HTTPROOT + "browser_bug678392-1.html", function() { + is(gHistorySwipeAnimation._trackedSnapshots.length, 3, "Snapshot " + + "array has correct length of 3 after loading three pages."); + load(tab, HTTPROOT + "browser_bug678392-2.html", function() { + is(gHistorySwipeAnimation._trackedSnapshots.length, 4, "Snapshot " + + "array has correct length of 4 after loading four pages."); + cleanupArray(); + testArrayCleanup(); + test1(); + }); + }); + }); + }); +} + +function verifyRefRemoved(aIndex, aBrowser) { + let wasFound = false; + let arr = gHistorySwipeAnimation._trackedSnapshots; + for (let i = 0; i < arr.length; i++) { + if (arr[i].index == aIndex && arr[i].browser == aBrowser) + wasFound = true; + } + is(wasFound, false, "The reference that was previously removed was " + + "still found in the array of tracked snapshots."); +} + +function test1() { + // Test presence of snpashots in per-tab array of snapshots and removal of + // individual snapshots (and corresponding references in the array of + // tracked snapshots). + let tab = gBrowser.selectedTab; + + load(tab, HTTPROOT + "browser_bug678392-1.html", function() { + var historyIndex = gBrowser.webNavigation.sessionHistory.index - 1; + load(tab, HTTPROOT + "browser_bug678392-2.html", function() { + load(tab, HTTPROOT + "browser_bug678392-1.html", function() { + load(tab, HTTPROOT + "browser_bug678392-2.html", function() { + let browser = gBrowser.selectedBrowser; + ok(browser.snapshots, "Array of snapshots exists in browser."); + ok(browser.snapshots[historyIndex], "First page exists in snapshot " + + "array."); + ok(browser.snapshots[historyIndex + 1], "Second page exists in " + + "snapshot array."); + ok(browser.snapshots[historyIndex + 2], "Third page exists in " + + "snapshot array."); + ok(browser.snapshots[historyIndex + 3], "Fourth page exists in " + + "snapshot array."); + is(gHistorySwipeAnimation._trackedSnapshots.length, 4, "Length of " + + "array of tracked snapshots is equal to 4 after loading four " + + "pages."); + + // Test removal of reference in the middle of the array. + gHistorySwipeAnimation._removeTrackedSnapshot(historyIndex + 1, + browser); + verifyRefRemoved(historyIndex + 1, browser); + is(gHistorySwipeAnimation._trackedSnapshots.length, 3, "Length of " + + "array of tracked snapshots is equal to 3 after removing one" + + "reference from the array with length 4."); + + // Test removal of reference at end of array. + gHistorySwipeAnimation._removeTrackedSnapshot(historyIndex + 3, + browser); + verifyRefRemoved(historyIndex + 3, browser); + is(gHistorySwipeAnimation._trackedSnapshots.length, 2, "Length of " + + "array of tracked snapshots is equal to 2 after removing two" + + "references from the array with length 4."); + + // Test removal of reference at head of array. + gHistorySwipeAnimation._removeTrackedSnapshot(historyIndex, + browser); + verifyRefRemoved(historyIndex, browser); + is(gHistorySwipeAnimation._trackedSnapshots.length, 1, "Length of " + + "array of tracked snapshots is equal to 1 after removing three" + + "references from the array with length 4."); + + cleanupArray(); + test2(); + }); + }); + }); + }); +} + +function test2() { + // Test growing of snapshot array across tabs. + let tab = gBrowser.selectedTab; + + load(tab, HTTPROOT + "browser_bug678392-1.html", function() { + var historyIndex = gBrowser.webNavigation.sessionHistory.index - 1; + load(tab, HTTPROOT + "browser_bug678392-2.html", function() { + is(gHistorySwipeAnimation._trackedSnapshots.length, 2, "Length of " + + "snapshot array is equal to 2 after loading two pages"); + let prevTab = tab; + tab = gBrowser.addTab("about:newtab"); + gBrowser.selectedTab = tab; + load(tab, HTTPROOT + "browser_bug678392-2.html" /* initial page */, + function() { + load(tab, HTTPROOT + "browser_bug678392-1.html", function() { + load(tab, HTTPROOT + "browser_bug678392-2.html", function() { + is(gHistorySwipeAnimation._trackedSnapshots.length, 4, "Length " + + "of snapshot array is equal to 4 after loading two pages in " + + "two tabs each."); + gBrowser.removeCurrentTab(); + gBrowser.selectedTab = prevTab; + cleanupArray(); + test3(); + }); + }); + }); + }); + }); +} + +function test3() { + // Test uninit of gHistorySwipeAnimation. + // This test MUST be the last one to execute. + gHistorySwipeAnimation.uninit(); + is(gHistorySwipeAnimation.active, false, "History swipe animation support " + + "was successfully uninitialized"); + finish(); +} diff --git a/browser/base/content/test/browser_gestureSupport.js b/browser/base/content/test/browser_gestureSupport.js index 00f58c46d9a..24c7c0241a4 100644 --- a/browser/base/content/test/browser_gestureSupport.js +++ b/browser/base/content/test/browser_gestureSupport.js @@ -122,6 +122,14 @@ function test_TestEventListeners() { let e = test_helper1; // easier to type this name + // Swipe gesture animation events + e("MozSwipeGestureStart", 0, -0.7, 0); + e("MozSwipeGestureUpdate", 0, -0.4, 0); + e("MozSwipeGestureEnd", 0, 0, 0); + e("MozSwipeGestureStart", 0, 0.6, 0); + e("MozSwipeGestureUpdate", 0, 0.3, 0); + e("MozSwipeGestureEnd", 0, 1, 0); + // Swipe gesture event e("MozSwipeGesture", SimpleGestureEvent.DIRECTION_LEFT, 0.0, 0); e("MozSwipeGesture", SimpleGestureEvent.DIRECTION_RIGHT, 0.0, 0); @@ -199,7 +207,7 @@ function test_helper2(type, direction, delta, altKey, ctrlKey, shiftKey, metaKey 10, 10, 10, 10, ctrlKey, altKey, shiftKey, metaKey, 1, window, - direction, delta, 0); + 0, direction, delta, 0); successful = true; } catch (ex) { diff --git a/browser/themes/osx/browser.css b/browser/themes/osx/browser.css index 1246209c612..0cb7145ef60 100644 --- a/browser/themes/osx/browser.css +++ b/browser/themes/osx/browser.css @@ -2056,6 +2056,17 @@ window[tabsontop="false"] richlistitem[type~="action"][actiontype="switchtab"][s } } +/* History Swipe Animation */ + +#historySwipeAnimationCurrentPage, +#historySwipeAnimationNextPage { + box-shadow: 0 3px 6px rgba(0, 0, 0, 0.6); +} + +#historySwipeAnimationContainer { + background: url("chrome://browser/skin/linen-pattern.png") #B3B9C1; +} + /* ----- SIDEBAR ELEMENTS ----- */ #sidebar, diff --git a/browser/themes/osx/jar.mn b/browser/themes/osx/jar.mn index 5a33c6f8078..eb3f6d431fd 100644 --- a/browser/themes/osx/jar.mn +++ b/browser/themes/osx/jar.mn @@ -39,6 +39,7 @@ browser.jar: skin/classic/browser/Info.png skin/classic/browser/keyhole-circle.png skin/classic/browser/KUI-background.png + skin/classic/browser/linen-pattern.png skin/classic/browser/menu-back.png skin/classic/browser/menu-forward.png skin/classic/browser/notification-16.png diff --git a/browser/themes/osx/linen-pattern.png b/browser/themes/osx/linen-pattern.png new file mode 100644 index 00000000000..b7cd2abe2d8 Binary files /dev/null and b/browser/themes/osx/linen-pattern.png differ diff --git a/content/base/src/nsGkAtomList.h b/content/base/src/nsGkAtomList.h index 3c46860f2f1..27a9c672de5 100644 --- a/content/base/src/nsGkAtomList.h +++ b/content/base/src/nsGkAtomList.h @@ -1723,6 +1723,9 @@ GK_ATOM(seconds, "seconds") GK_ATOM(secondsFromDateTime, "seconds-from-dateTime") // Simple gestures support +GK_ATOM(onMozSwipeGestureStart, "onMozSwipeGestureStart") +GK_ATOM(onMozSwipeGestureUpdate, "onMozSwipeGestureUpdate") +GK_ATOM(onMozSwipeGestureEnd, "onMozSwipeGestureEnd") GK_ATOM(onMozSwipeGesture, "onMozSwipeGesture") GK_ATOM(onMozMagnifyGestureStart, "onMozMagnifyGestureStart") GK_ATOM(onMozMagnifyGestureUpdate, "onMozMagnifyGestureUpdate") @@ -1984,6 +1987,7 @@ GK_ATOM(windows_glass, "windows-glass") GK_ATOM(touch_enabled, "touch-enabled") GK_ATOM(maemo_classic, "maemo-classic") GK_ATOM(menubar_drag, "menubar-drag") +GK_ATOM(swipe_animation_enabled, "swipe-animation-enabled") // windows theme selector metrics GK_ATOM(windows_classic, "windows-classic") @@ -2016,6 +2020,7 @@ GK_ATOM(_moz_menubar_drag, "-moz-menubar-drag") GK_ATOM(_moz_device_pixel_ratio, "-moz-device-pixel-ratio") GK_ATOM(_moz_device_orientation, "-moz-device-orientation") GK_ATOM(_moz_is_resource_document, "-moz-is-resource-document") +GK_ATOM(_moz_swipe_animation_enabled, "-moz-swipe-animation-enabled") // application commands GK_ATOM(Back, "Back") diff --git a/content/events/public/nsEventNameList.h b/content/events/public/nsEventNameList.h index 0e32d3f74b9..d415c01cedb 100644 --- a/content/events/public/nsEventNameList.h +++ b/content/events/public/nsEventNameList.h @@ -778,6 +778,18 @@ NON_IDL_EVENT(gamepaddisconnected, #endif // Simple gesture events +NON_IDL_EVENT(MozSwipeGestureStart, + NS_SIMPLE_GESTURE_SWIPE_START, + EventNameType_None, + NS_SIMPLE_GESTURE_EVENT) +NON_IDL_EVENT(MozSwipeGestureUpdate, + NS_SIMPLE_GESTURE_SWIPE_UPDATE, + EventNameType_None, + NS_SIMPLE_GESTURE_EVENT) +NON_IDL_EVENT(MozSwipeGestureEnd, + NS_SIMPLE_GESTURE_SWIPE_END, + EventNameType_None, + NS_SIMPLE_GESTURE_EVENT) NON_IDL_EVENT(MozSwipeGesture, NS_SIMPLE_GESTURE_SWIPE, EventNameType_None, diff --git a/content/events/src/nsDOMSimpleGestureEvent.cpp b/content/events/src/nsDOMSimpleGestureEvent.cpp index f87ee12c928..9bcc15b38ca 100644 --- a/content/events/src/nsDOMSimpleGestureEvent.cpp +++ b/content/events/src/nsDOMSimpleGestureEvent.cpp @@ -43,6 +43,24 @@ NS_INTERFACE_MAP_BEGIN(nsDOMSimpleGestureEvent) NS_DOM_INTERFACE_MAP_ENTRY_CLASSINFO(SimpleGestureEvent) NS_INTERFACE_MAP_END_INHERITING(nsDOMMouseEvent) +/* attribute unsigned long allowedDirections; */ +NS_IMETHODIMP +nsDOMSimpleGestureEvent::GetAllowedDirections(PRUint32 *aAllowedDirections) +{ + NS_ENSURE_ARG_POINTER(aAllowedDirections); + *aAllowedDirections = + static_cast(mEvent)->allowedDirections; + return NS_OK; +} + +NS_IMETHODIMP +nsDOMSimpleGestureEvent::SetAllowedDirections(PRUint32 aAllowedDirections) +{ + static_cast(mEvent)->allowedDirections = + aAllowedDirections; + return NS_OK; +} + /* readonly attribute unsigned long direction; */ NS_IMETHODIMP nsDOMSimpleGestureEvent::GetDirection(uint32_t *aDirection) @@ -86,6 +104,7 @@ nsDOMSimpleGestureEvent::InitSimpleGestureEvent(const nsAString& aTypeArg, bool aMetaKeyArg, uint16_t aButton, nsIDOMEventTarget* aRelatedTarget, + uint32_t aAllowedDirectionsArg, uint32_t aDirectionArg, double aDeltaArg, uint32_t aClickCountArg) @@ -108,6 +127,7 @@ nsDOMSimpleGestureEvent::InitSimpleGestureEvent(const nsAString& aTypeArg, NS_ENSURE_SUCCESS(rv, rv); nsSimpleGestureEvent* simpleGestureEvent = static_cast(mEvent); + simpleGestureEvent->allowedDirections = aAllowedDirectionsArg; simpleGestureEvent->direction = aDirectionArg; simpleGestureEvent->delta = aDeltaArg; simpleGestureEvent->clickCount = aClickCountArg; diff --git a/content/events/src/nsDOMSimpleGestureEvent.h b/content/events/src/nsDOMSimpleGestureEvent.h index dde5a71c7a2..6aa7eda9bbc 100644 --- a/content/events/src/nsDOMSimpleGestureEvent.h +++ b/content/events/src/nsDOMSimpleGestureEvent.h @@ -31,6 +31,11 @@ public: return mozilla::dom::SimpleGestureEventBinding::Wrap(aCx, aScope, this); } + uint32_t AllowedDirections() + { + return static_cast(mEvent)->allowedDirections; + } + uint32_t Direction() { return static_cast(mEvent)->direction; @@ -61,6 +66,7 @@ public: bool aMetaKey, uint16_t aButton, mozilla::dom::EventTarget* aRelatedTarget, + uint32_t aAllowedDirections, uint32_t aDirection, double aDelta, uint32_t aClickCount, @@ -70,8 +76,8 @@ public: aView, aDetail, aScreenX, aScreenY, aClientX, aClientY, aCtrlKey, aAltKey, aShiftKey, aMetaKey, aButton, - aRelatedTarget, aDirection, - aDelta, aClickCount); + aRelatedTarget, aAllowedDirections, + aDirection, aDelta, aClickCount); } }; diff --git a/dom/base/nsDOMWindowUtils.cpp b/dom/base/nsDOMWindowUtils.cpp index fa4c622b4d7..14a074a5076 100644 --- a/dom/base/nsDOMWindowUtils.cpp +++ b/dom/base/nsDOMWindowUtils.cpp @@ -1190,7 +1190,13 @@ nsDOMWindowUtils::SendSimpleGestureEvent(const nsAString& aType, return NS_ERROR_FAILURE; int32_t msg; - if (aType.EqualsLiteral("MozSwipeGesture")) + if (aType.EqualsLiteral("MozSwipeGestureStart")) + msg = NS_SIMPLE_GESTURE_SWIPE_START; + else if (aType.EqualsLiteral("MozSwipeGestureUpdate")) + msg = NS_SIMPLE_GESTURE_SWIPE_UPDATE; + else if (aType.EqualsLiteral("MozSwipeGestureEnd")) + msg = NS_SIMPLE_GESTURE_SWIPE_END; + else if (aType.EqualsLiteral("MozSwipeGesture")) msg = NS_SIMPLE_GESTURE_SWIPE; else if (aType.EqualsLiteral("MozMagnifyGestureStart")) msg = NS_SIMPLE_GESTURE_MAGNIFY_START; diff --git a/dom/interfaces/base/nsIDOMWindowUtils.idl b/dom/interfaces/base/nsIDOMWindowUtils.idl index 8492169586a..af9c66a7396 100644 --- a/dom/interfaces/base/nsIDOMWindowUtils.idl +++ b/dom/interfaces/base/nsIDOMWindowUtils.idl @@ -539,7 +539,8 @@ interface nsIDOMWindowUtils : nsISupports { [optional] in long aExtraForgetSkippableCalls); /** Synthesize a simple gesture event for a window. The event types - * supported are: MozSwipeGesture, MozMagnifyGestureStart, + * supported are: MozSwipeGestureStart, MozSwipeGestureUpdate, + * MozSwipeGestureEnd, MozSwipeGesture, MozMagnifyGestureStart, * MozMagnifyGestureUpdate, MozMagnifyGesture, MozRotateGestureStart, * MozRotateGestureUpdate, MozRotateGesture, MozPressTapGesture, * MozTapGesture, and MozEdgeUIGesture. diff --git a/dom/interfaces/events/nsIDOMSimpleGestureEvent.idl b/dom/interfaces/events/nsIDOMSimpleGestureEvent.idl index bf9688d5dd7..88d6a2ad79b 100644 --- a/dom/interfaces/events/nsIDOMSimpleGestureEvent.idl +++ b/dom/interfaces/events/nsIDOMSimpleGestureEvent.idl @@ -12,8 +12,38 @@ * * The following events are generated: * - * MozSwipeGesture - Generated when the user completes a swipe across - * across the input device. + * MozSwipeGestureStart - Generated when the user starts a horizontal + * swipe across the input device. This event not only acts as a signal, + * but also asks two questions: Should a swipe really be started, and + * in which directions should the user be able to swipe? The first + * question is answered by event listeners by calling or not calling + * preventDefault() on the event. Since a swipe swallows all scroll + * events, the default action of the swipe start event is *not* to + * start a swipe. Call preventDefault() if you want a swipe to be + * started. + * The second question (swipe-able directions) is answered in the + * allowedDirections field. + * If this event has preventDefault() called on it (and thus starts + * a swipe), it guarantees a future MozSwipeGestureEnd event that + * will signal the end of a swipe animation. + * + * MozSwipeGestureUpdate - Generated periodically while the user is + * continuing a horizontal swipe gesture. The "delta" value represents + * the current absolute gesture amount. This event may even be sent + * after a MozSwipeGesture event fired in order to allow for fluid + * completion of a swipe animation. The direction value is meaningless + * on swipe update events. + * + * MozSwipeGestureEnd - Generated when the swipe animation is completed. + * + * MozSwipeGesture - Generated when the user releases a swipe across + * across the input device. This event signals that the actual swipe + * operation is complete, even though the animation might not be finished + * yet. This event can be sent without accompanying start / update / end + * events, and it can also be handled on its own if the consumer doesn't + * want to handle swipe animation events. + * Only the direction value has any significance, the delta value is + * meaningless. * * MozMagnifyGestureStart - Generated when the user begins the magnify * ("pinch") gesture. The "delta" value represents the initial @@ -69,7 +99,7 @@ * consuming events. */ -[scriptable, builtinclass, uuid(0cd3fde1-0c99-49cc-a74e-9a9348864307)] +[scriptable, builtinclass, uuid(d78656ab-9d68-4f03-83f9-7c7bee071aa7)] interface nsIDOMSimpleGestureEvent : nsIDOMMouseEvent { /* Swipe direction constants */ @@ -77,11 +107,28 @@ interface nsIDOMSimpleGestureEvent : nsIDOMMouseEvent const unsigned long DIRECTION_DOWN = 2; const unsigned long DIRECTION_LEFT = 4; const unsigned long DIRECTION_RIGHT = 8; - + /* Rotational direction constants */ const unsigned long ROTATION_COUNTERCLOCKWISE = 1; const unsigned long ROTATION_CLOCKWISE = 2; + /* Read-write value for swipe events. + * + * Reports the directions that can be swiped to; multiple directions + * should be OR'ed together. + * + * The allowedDirections field is designed to be set on SwipeGestureStart + * events by event listeners. Its value after event dispatch determines + * the behavior of the swipe animation that is about to begin. + * Specifically, if the user swipes in a direction that can't be swiped + * to, the animation will have a bounce effect. + * Future SwipeGestureUpdate, SwipeGesture and SwipeGestureEnd events + * will carry the allowDirections value that was set on the SwipeStart + * event. Changing this field on non-SwipeGestureStart events doesn't + * have any effect. + */ + attribute unsigned long allowedDirections; + /* Direction of a gesture. Diagonals are indicated by OR'ing the * applicable constants together. * @@ -94,8 +141,8 @@ interface nsIDOMSimpleGestureEvent : nsIDOMMouseEvent */ readonly attribute unsigned long direction; - /* Delta value for magnify and rotate gestures. - * + /* Delta value for magnify, rotate and swipe gestures. + * * For rotation, the value is in degrees and is positive for * clockwise rotation and negative for counterclockwise * rotation. @@ -111,6 +158,14 @@ interface nsIDOMSimpleGestureEvent : nsIDOMMouseEvent * 100.0, but it is only safe currently to rely on the delta being * positive or negative. * + * For swipe start, update and end events, the value is a fraction + * of one "page". If the resulting swipe will have DIRECTION_LEFT, the + * delta value will be positive; for DIRECTION_RIGHT, delta is negative. + * If this seems backwards to you, look at it this way: If the current + * page is pushed to the right during the animation (positive delta), + * the page left to the current page will be visible after the swipe + * (DIRECTION_LEFT). + * * Units on Windows represent the difference between the initial * and current/final width between the two touch points on the input * device and are measured in pixels. @@ -135,6 +190,7 @@ interface nsIDOMSimpleGestureEvent : nsIDOMMouseEvent in boolean metaKeyArg, in unsigned short buttonArg, in nsIDOMEventTarget relatedTargetArg, + in unsigned long allowedDirectionsArg, in unsigned long directionArg, in double deltaArg, in unsigned long clickCount); diff --git a/dom/webidl/SimpleGestureEvent.webidl b/dom/webidl/SimpleGestureEvent.webidl index 123f05daf85..8ceb359671d 100644 --- a/dom/webidl/SimpleGestureEvent.webidl +++ b/dom/webidl/SimpleGestureEvent.webidl @@ -18,6 +18,8 @@ interface SimpleGestureEvent : MouseEvent const unsigned long ROTATION_COUNTERCLOCKWISE = 1; const unsigned long ROTATION_CLOCKWISE = 2; + attribute unsigned long allowedDirections; + readonly attribute unsigned long direction; readonly attribute double delta; @@ -40,6 +42,7 @@ interface SimpleGestureEvent : MouseEvent boolean metaKeyArg, unsigned short buttonArg, EventTarget? relatedTargetArg, + unsigned long allowedDirectionsArg, unsigned long directionArg, double deltaArg, unsigned long clickCount); diff --git a/layout/style/nsCSSRuleProcessor.cpp b/layout/style/nsCSSRuleProcessor.cpp index d4f4bcb1338..b76fffa4a1d 100644 --- a/layout/style/nsCSSRuleProcessor.cpp +++ b/layout/style/nsCSSRuleProcessor.cpp @@ -1180,6 +1180,12 @@ InitSystemMetrics() sSystemMetrics->AppendElement(nsGkAtoms::maemo_classic); } + rv = LookAndFeel::GetInt(LookAndFeel::eIntID_SwipeAnimationEnabled, + &metricResult); + if (NS_SUCCEEDED(rv) && metricResult) { + sSystemMetrics->AppendElement(nsGkAtoms::swipe_animation_enabled); + } + #ifdef XP_WIN if (NS_SUCCEEDED( LookAndFeel::GetInt(LookAndFeel::eIntID_WindowsThemeIdentifier, diff --git a/layout/style/nsMediaFeatures.cpp b/layout/style/nsMediaFeatures.cpp index b00286c1b58..1f326579e20 100644 --- a/layout/style/nsMediaFeatures.cpp +++ b/layout/style/nsMediaFeatures.cpp @@ -579,6 +579,14 @@ nsMediaFeatures::features[] = { GetWindowsTheme }, + { + &nsGkAtoms::_moz_swipe_animation_enabled, + nsMediaFeature::eMinMaxNotAllowed, + nsMediaFeature::eBoolInteger, + { &nsGkAtoms::swipe_animation_enabled }, + GetSystemMetric + }, + // Internal -moz-is-glyph media feature: applies only inside SVG glyphs. // Internal because it is really only useful in the user agent anyway // and therefore not worth standardizing. diff --git a/layout/style/test/test_media_queries.html b/layout/style/test/test_media_queries.html index 79a619eab76..e8d5a0bce8a 100644 --- a/layout/style/test/test_media_queries.html +++ b/layout/style/test/test_media_queries.html @@ -569,6 +569,7 @@ function run() { expression_should_be_parseable("-moz-windows-glass"); expression_should_be_parseable("-moz-touch-enabled"); expression_should_be_parseable("-moz-maemo-classic"); + expression_should_be_parseable("-moz-swipe-animation-enabled"); expression_should_be_parseable("-moz-scrollbar-start-backward: 0"); expression_should_be_parseable("-moz-scrollbar-start-forward: 0"); @@ -585,6 +586,7 @@ function run() { expression_should_be_parseable("-moz-windows-glass: 0"); expression_should_be_parseable("-moz-touch-enabled: 0"); expression_should_be_parseable("-moz-maemo-classic: 0"); + expression_should_be_parseable("-moz-swipe-animation-enabled: 0"); expression_should_be_parseable("-moz-scrollbar-start-backward: 1"); expression_should_be_parseable("-moz-scrollbar-start-forward: 1"); @@ -601,6 +603,7 @@ function run() { expression_should_be_parseable("-moz-windows-glass: 1"); expression_should_be_parseable("-moz-touch-enabled: 1"); expression_should_be_parseable("-moz-maemo-classic: 1"); + expression_should_be_parseable("-moz-swipe-animation-enabled: 1"); expression_should_not_be_parseable("-moz-scrollbar-start-backward: -1"); expression_should_not_be_parseable("-moz-scrollbar-start-forward: -1"); @@ -617,6 +620,7 @@ function run() { expression_should_not_be_parseable("-moz-windows-glass: -1"); expression_should_not_be_parseable("-moz-touch-enabled: -1"); expression_should_not_be_parseable("-moz-maemo-classic: -1"); + expression_should_not_be_parseable("-moz-swipe-animation-enabled: -1"); expression_should_not_be_parseable("-moz-scrollbar-start-backward: true"); expression_should_not_be_parseable("-moz-scrollbar-start-forward: true"); @@ -633,6 +637,7 @@ function run() { expression_should_not_be_parseable("-moz-windows-glass: true"); expression_should_not_be_parseable("-moz-touch-enabled: true"); expression_should_not_be_parseable("-moz-maemo-classic: true"); + expression_should_not_be_parseable("-moz-swipe-animation-enabled: true"); // windows theme media queries expression_should_be_parseable("-moz-windows-theme: aero"); diff --git a/widget/LookAndFeel.h b/widget/LookAndFeel.h index 847ea2f7469..e2a9d5e2299 100644 --- a/widget/LookAndFeel.h +++ b/widget/LookAndFeel.h @@ -355,7 +355,13 @@ public: /** * Dealy before showing a tooltip. */ - eIntID_TooltipDelay + eIntID_TooltipDelay, + + /* + * A Boolean value to determine whether Mac OS X Lion style swipe animations + * should be used. + */ + eIntID_SwipeAnimationEnabled }; /** diff --git a/widget/cocoa/nsChildView.h b/widget/cocoa/nsChildView.h index 42d53b82d03..d96a97b39a7 100644 --- a/widget/cocoa/nsChildView.h +++ b/widget/cocoa/nsChildView.h @@ -261,9 +261,9 @@ typedef NSInteger NSEventGestureAxis; BOOL mDidForceRefreshOpenGL; BOOL mWaitingForPaint; - // Support for fluid swipe tracking. #ifdef __LP64__ - BOOL *mSwipeAnimationCancelled; + // Support for fluid swipe tracking. + void (^mCancelSwipeAnimation)(); #endif // Whether this uses off-main-thread compositing. diff --git a/widget/cocoa/nsChildView.mm b/widget/cocoa/nsChildView.mm index ff1d905bc4f..41d97001eb1 100644 --- a/widget/cocoa/nsChildView.mm +++ b/widget/cocoa/nsChildView.mm @@ -2205,10 +2205,10 @@ NSEvent* gLastDragMouseDownEvent = nil; [self setFocusRingType:NSFocusRingTypeNone]; #ifdef __LP64__ - mSwipeAnimationCancelled = nil; + mCancelSwipeAnimation = nil; #endif } - + // register for things we'll take from other applications [ChildView registerViewForDraggedTypes:self]; @@ -3292,6 +3292,48 @@ NSEvent* gLastDragMouseDownEvent = nil; NS_OBJC_END_TRY_ABORT_BLOCK; } +#ifdef __LP64__ +- (bool)sendSwipeEvent:(NSEvent*)aEvent + withKind:(PRUint32)aMsg + allowedDirections:(PRUint32*)aAllowedDirections + direction:(PRUint32)aDirection + delta:(PRFloat64)aDelta +{ + if (!mGeckoChild) + return false; + + nsSimpleGestureEvent geckoEvent(true, aMsg, mGeckoChild, aDirection, aDelta); + geckoEvent.allowedDirections = *aAllowedDirections; + [self convertCocoaMouseEvent:aEvent toGeckoEvent:&geckoEvent]; + bool eventCancelled = mGeckoChild->DispatchWindowEvent(geckoEvent); + *aAllowedDirections = geckoEvent.allowedDirections; + return eventCancelled; // event cancelled == swipe should start +} + +- (void)cancelSwipeIfRunning +{ + // Clear gesture state. + mGestureState = eGestureState_None; + + if (mCancelSwipeAnimation) { + mCancelSwipeAnimation(); + [mCancelSwipeAnimation release]; + mCancelSwipeAnimation = nil; + } +} + +- (void)sendSwipeEndEvent:(NSEvent *)anEvent + allowedDirections:(PRUint32)aAllowedDirections +{ + // Tear down animation overlay by sending a swipe end event. + PRUint32 allowedDirectionsCopy = aAllowedDirections; + [self sendSwipeEvent:anEvent + withKind:NS_SIMPLE_GESTURE_SWIPE_END + allowedDirections:&allowedDirectionsCopy + direction:0 + delta:0.0]; +} + // Support fluid swipe tracking on OS X 10.7 and higher. We must be careful // to only invoke this support on a horizontal two-finger gesture that really // is a swipe (and not a scroll) -- in other words, the app is responsible @@ -3302,7 +3344,6 @@ NSEvent* gLastDragMouseDownEvent = nil; // partly based on Apple sample code available at // http://developer.apple.com/library/mac/#releasenotes/Cocoa/AppKit.html // (under Fluid Swipe Tracking API). -#ifdef __LP64__ - (void)maybeTrackScrollEventAsSwipe:(NSEvent *)anEvent scrollOverflow:(double)overflow { @@ -3321,13 +3362,6 @@ NSEvent* gLastDragMouseDownEvent = nil; return; } - // If a swipe is currently being tracked kill it -- it's been interrupted by - // another gesture or legacy scroll wheel event. - if (mSwipeAnimationCancelled && (*mSwipeAnimationCancelled == NO)) { - *mSwipeAnimationCancelled = YES; - mSwipeAnimationCancelled = nil; - } - // Only initiate tracking if the user has tried to scroll past the edge of // the current page (as indicated by 'overflow' being non-zero). Gecko only // sets nsMouseScrollEvent.scrollOverflow when it's processing @@ -3360,6 +3394,27 @@ NSEvent* gLastDragMouseDownEvent = nil; return; } + // If a swipe is currently being tracked kill it -- it's been interrupted by + // another gesture or legacy scroll wheel event. + [self cancelSwipeIfRunning]; + + // We're ready to start the animation. Tell Gecko about it, and at the same + // time ask it if it really wants to start an animation for this event. + // This event also reports back the directions that we can swipe in. + PRUint32 allowedDirections = 0; + bool shouldStartSwipe = [self sendSwipeEvent:anEvent + withKind:NS_SIMPLE_GESTURE_SWIPE_START + allowedDirections:&allowedDirections + direction:0 + delta:0.0]; + + if (!shouldStartSwipe) { + return; + } + + double min = (allowedDirections & nsIDOMSimpleGestureEvent::DIRECTION_RIGHT) ? -1 : 0; + double max = (allowedDirections & nsIDOMSimpleGestureEvent::DIRECTION_LEFT) ? 1 : 0; + __block BOOL animationCancelled = NO; __block BOOL geckoSwipeEventSent = NO; // At this point, anEvent is the first scroll wheel event in a two-finger @@ -3377,9 +3432,9 @@ NSEvent* gLastDragMouseDownEvent = nil; // the anEvent object because it's retained by the block, see bug 682445. // The block will release it when the block goes away at the end of the // animation, or when the animation is canceled. - [anEvent trackSwipeEventWithOptions:0 - dampenAmountThresholdMin:-1 - max:1 + [anEvent trackSwipeEventWithOptions:NSEventSwipeTrackingLockDirection + dampenAmountThresholdMin:min + max:max usingHandler:^(CGFloat gestureAmount, NSEventPhase phase, BOOL isComplete, BOOL *stop) { // Since this tracking handler can be called asynchronously, mGeckoChild // might have become NULL here (our child widget might have been @@ -3388,48 +3443,56 @@ NSEvent* gLastDragMouseDownEvent = nil; *stop = YES; return; } - // gestureAmount is documented to be '-1', '0' or '1' when isComplete - // is TRUE, but the docs don't say anything about its value at other - // times. However, tests show that, when phase == NSEventPhaseEnded, - // gestureAmount is negative when it will be '-1' at isComplete, and - // positive when it will be '1'. And phase is never equal to - // NSEventPhaseEnded when gestureAmount will be '0' at isComplete. - // Not waiting until isComplete is TRUE substantially reduces the - // time it takes to change pages after a swipe, and helps resolve - // bug 678891. + + PRUint32 allowedDirectionsCopy = allowedDirections; + + // Update animation overlay to match gestureAmount. + [self sendSwipeEvent:anEvent + withKind:NS_SIMPLE_GESTURE_SWIPE_UPDATE + allowedDirections:&allowedDirectionsCopy + direction:0 + delta:gestureAmount]; + if (phase == NSEventPhaseEnded && !geckoSwipeEventSent) { - if (gestureAmount) { - nsSimpleGestureEvent geckoEvent(true, NS_SIMPLE_GESTURE_SWIPE, mGeckoChild, 0, 0.0); - [self convertCocoaMouseEvent:anEvent toGeckoEvent:&geckoEvent]; - if (gestureAmount > 0) { - geckoEvent.direction |= nsIDOMSimpleGestureEvent::DIRECTION_LEFT; - } else { - geckoEvent.direction |= nsIDOMSimpleGestureEvent::DIRECTION_RIGHT; - } - // If DispatchWindowEvent() does something to trigger a modal dialog - // (which spins the event loop), the OS gets confused and makes - // several re-entrant calls to this handler, all of which have - // 'phase' set to NSEventPhaseEnded. Unless we do something about - // it, this results in an equal number of re-entrant calls to - // DispatchWindowEvent(), and to our modal-event handling code. - // Probably because of bug 478703, this really messes things up, - // and requires a force quit to get out of. We avoid this by - // avoiding re-entrant calls to DispatchWindowEvent(). See bug - // 770626. - geckoSwipeEventSent = YES; - mGeckoChild->DispatchWindowEvent(geckoEvent); - } - mSwipeAnimationCancelled = nil; - } else if (phase == NSEventPhaseCancelled) { - mSwipeAnimationCancelled = nil; + // The result of the swipe is now known, so the main event can be sent. + // The animation might continue even after this event was sent, so + // don't tear down the animation overlay yet. + // gestureAmount is documented to be '-1', '0' or '1' when isComplete + // is TRUE, but the docs don't say anything about its value at other + // times. However, tests show that, when phase == NSEventPhaseEnded, + // gestureAmount is negative when it will be '-1' at isComplete, and + // positive when it will be '1'. And phase is never equal to + // NSEventPhaseEnded when gestureAmount will be '0' at isComplete. + PRUint32 direction = gestureAmount > 0 ? + (PRUint32)nsIDOMSimpleGestureEvent::DIRECTION_LEFT : + (PRUint32)nsIDOMSimpleGestureEvent::DIRECTION_RIGHT; + // If DispatchWindowEvent() does something to trigger a modal dialog + // (which spins the event loop), the OS gets confused and makes + // several re-entrant calls to this handler, all of which have + // 'phase' set to NSEventPhaseEnded. Unless we do something about + // it, this results in an equal number of re-entrant calls to + // DispatchWindowEvent(), and to our modal-event handling code. + // Probably because of bug 478703, this really messes things up, + // and requires a force quit to get out of. We avoid this by + // avoiding re-entrant calls to DispatchWindowEvent(). See bug + // 770626. + geckoSwipeEventSent = YES; + [self sendSwipeEvent:anEvent + withKind:NS_SIMPLE_GESTURE_SWIPE + allowedDirections:&allowedDirectionsCopy + direction:direction + delta:0.0]; + } + + if (isComplete) { + [self cancelSwipeIfRunning]; + [self sendSwipeEndEvent:anEvent allowedDirections:allowedDirections]; } }]; - // We keep a pointer to the __block variable (animationCanceled) so we - // can cancel our block handler at any time. Note: We must assign - // &animationCanceled after our block creation and copy -- its address - // isn't resolved until then! - mSwipeAnimationCancelled = &animationCancelled; + mCancelSwipeAnimation = [^{ + animationCancelled = YES; + } copy]; } #endif // #ifdef __LP64__ diff --git a/widget/cocoa/nsLookAndFeel.mm b/widget/cocoa/nsLookAndFeel.mm index 3ff2989bb86..9fde6a84061 100644 --- a/widget/cocoa/nsLookAndFeel.mm +++ b/widget/cocoa/nsLookAndFeel.mm @@ -414,6 +414,13 @@ nsLookAndFeel::GetIntImpl(IntID aID, int32_t &aResult) case eIntID_ScrollbarButtonAutoRepeatBehavior: aResult = 0; break; + case eIntID_SwipeAnimationEnabled: + aResult = 0; + if ([NSEvent respondsToSelector:@selector( + isSwipeTrackingFromScrollEventsEnabled)]) { + aResult = [NSEvent isSwipeTrackingFromScrollEventsEnabled] ? 1 : 0; + } + break; default: aResult = 0; res = NS_ERROR_FAILURE; diff --git a/widget/gtk2/nsLookAndFeel.cpp b/widget/gtk2/nsLookAndFeel.cpp index 04d12cbcccf..a4ae0238e73 100644 --- a/widget/gtk2/nsLookAndFeel.cpp +++ b/widget/gtk2/nsLookAndFeel.cpp @@ -566,6 +566,9 @@ nsLookAndFeel::GetIntImpl(IntID aID, int32_t &aResult) case eIntID_ScrollbarButtonAutoRepeatBehavior: aResult = 1; break; + case eIntID_SwipeAnimationEnabled: + aResult = 0; + break; default: aResult = 0; res = NS_ERROR_FAILURE; diff --git a/widget/nsGUIEvent.h b/widget/nsGUIEvent.h index 6b50008d473..871e7f4ccaf 100644 --- a/widget/nsGUIEvent.h +++ b/widget/nsGUIEvent.h @@ -337,16 +337,19 @@ enum nsEventStructType { // Simple gesture events #define NS_SIMPLE_GESTURE_EVENT_START 3500 -#define NS_SIMPLE_GESTURE_SWIPE (NS_SIMPLE_GESTURE_EVENT_START) -#define NS_SIMPLE_GESTURE_MAGNIFY_START (NS_SIMPLE_GESTURE_EVENT_START+1) -#define NS_SIMPLE_GESTURE_MAGNIFY_UPDATE (NS_SIMPLE_GESTURE_EVENT_START+2) -#define NS_SIMPLE_GESTURE_MAGNIFY (NS_SIMPLE_GESTURE_EVENT_START+3) -#define NS_SIMPLE_GESTURE_ROTATE_START (NS_SIMPLE_GESTURE_EVENT_START+4) -#define NS_SIMPLE_GESTURE_ROTATE_UPDATE (NS_SIMPLE_GESTURE_EVENT_START+5) -#define NS_SIMPLE_GESTURE_ROTATE (NS_SIMPLE_GESTURE_EVENT_START+6) -#define NS_SIMPLE_GESTURE_TAP (NS_SIMPLE_GESTURE_EVENT_START+7) -#define NS_SIMPLE_GESTURE_PRESSTAP (NS_SIMPLE_GESTURE_EVENT_START+8) -#define NS_SIMPLE_GESTURE_EDGEUI (NS_SIMPLE_GESTURE_EVENT_START+9) +#define NS_SIMPLE_GESTURE_SWIPE_START (NS_SIMPLE_GESTURE_EVENT_START) +#define NS_SIMPLE_GESTURE_SWIPE_UPDATE (NS_SIMPLE_GESTURE_EVENT_START+1) +#define NS_SIMPLE_GESTURE_SWIPE_END (NS_SIMPLE_GESTURE_EVENT_START+2) +#define NS_SIMPLE_GESTURE_SWIPE (NS_SIMPLE_GESTURE_EVENT_START+3) +#define NS_SIMPLE_GESTURE_MAGNIFY_START (NS_SIMPLE_GESTURE_EVENT_START+4) +#define NS_SIMPLE_GESTURE_MAGNIFY_UPDATE (NS_SIMPLE_GESTURE_EVENT_START+5) +#define NS_SIMPLE_GESTURE_MAGNIFY (NS_SIMPLE_GESTURE_EVENT_START+6) +#define NS_SIMPLE_GESTURE_ROTATE_START (NS_SIMPLE_GESTURE_EVENT_START+7) +#define NS_SIMPLE_GESTURE_ROTATE_UPDATE (NS_SIMPLE_GESTURE_EVENT_START+8) +#define NS_SIMPLE_GESTURE_ROTATE (NS_SIMPLE_GESTURE_EVENT_START+9) +#define NS_SIMPLE_GESTURE_TAP (NS_SIMPLE_GESTURE_EVENT_START+10) +#define NS_SIMPLE_GESTURE_PRESSTAP (NS_SIMPLE_GESTURE_EVENT_START+11) +#define NS_SIMPLE_GESTURE_EDGEUI (NS_SIMPLE_GESTURE_EVENT_START+12) // These are used to send native events to plugins. #define NS_PLUGIN_EVENT_START 3600 @@ -1701,20 +1704,22 @@ public: nsSimpleGestureEvent(bool isTrusted, uint32_t msg, nsIWidget* w, uint32_t directionArg, double deltaArg) : nsMouseEvent_base(isTrusted, msg, w, NS_SIMPLE_GESTURE_EVENT), - direction(directionArg), delta(deltaArg), clickCount(0) + allowedDirections(0), direction(directionArg), delta(deltaArg), + clickCount(0) { } nsSimpleGestureEvent(const nsSimpleGestureEvent& other) : nsMouseEvent_base(other.mFlags.mIsTrusted, other.message, other.widget, NS_SIMPLE_GESTURE_EVENT), - direction(other.direction), delta(other.delta), clickCount(0) + allowedDirections(other.allowedDirections), direction(other.direction), + delta(other.delta), clickCount(0) { } - - uint32_t direction; // See nsIDOMSimpleGestureEvent for values - double delta; // Delta for magnify and rotate events - uint32_t clickCount; // The number of taps for tap events + uint32_t allowedDirections; // See nsIDOMSimpleGestureEvent for values + uint32_t direction; // See nsIDOMSimpleGestureEvent for values + double delta; // Delta for magnify and rotate events + uint32_t clickCount; // The number of taps for tap events }; class nsTransitionEvent : public nsEvent diff --git a/widget/os2/nsLookAndFeel.cpp b/widget/os2/nsLookAndFeel.cpp index 384993dfa5a..c776b7f700b 100644 --- a/widget/os2/nsLookAndFeel.cpp +++ b/widget/os2/nsLookAndFeel.cpp @@ -329,6 +329,9 @@ nsLookAndFeel::GetIntImpl(IntID aID, int32_t &aResult) case eIntID_ScrollbarButtonAutoRepeatBehavior: aResult = 0; break; + case eIntID_SwipeAnimationEnabled: + aResult = 0; + break; default: aResult = 0; diff --git a/widget/windows/nsLookAndFeel.cpp b/widget/windows/nsLookAndFeel.cpp index e5a17534038..9b5579f2b29 100644 --- a/widget/windows/nsLookAndFeel.cpp +++ b/widget/windows/nsLookAndFeel.cpp @@ -455,6 +455,9 @@ nsLookAndFeel::GetIntImpl(IntID aID, int32_t &aResult) case eIntID_ScrollbarButtonAutoRepeatBehavior: aResult = 0; break; + case eIntID_SwipeAnimationEnabled: + aResult = 0; + break; default: aResult = 0; res = NS_ERROR_FAILURE;