diff -r 000000000000 -r 6474c204b198 browser/metro/base/content/input.js --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/browser/metro/base/content/input.js Wed Dec 31 06:09:35 2014 +0100 @@ -0,0 +1,1059 @@ +// -*- Mode: js2; tab-width: 2; indent-tabs-mode: nil; js2-basic-offset: 2; js2-skip-preprocessor-directives: t; js2-strict-trailing-comma-warning: nil -*- +/* 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/. */ + +Components.utils.import("resource://gre/modules/Geometry.jsm"); + +/* + * Drag scrolling related constants + */ + +// maximum drag distance in inches while axis locking can still be reverted +const kAxisLockRevertThreshold = 0.8; + +// Same as NS_EVENT_STATE_ACTIVE from mozilla/EventStates.h +const kStateActive = 0x00000001; + +// After a drag begins, kinetic panning is stopped if the drag doesn't become +// a pan in 300 milliseconds. +const kStopKineticPanOnDragTimeout = 300; + +// Min/max velocity of kinetic panning. This is in pixels/millisecond. +const kMinVelocity = 0.4; +const kMaxVelocity = 6; + +/* + * prefs + */ + +// Display rects around selection ranges. Useful in debugging +// selection problems. +const kDebugSelectionDisplayPref = "metro.debug.selection.displayRanges"; +// Dump range rect data to the console. Very useful, but also slows +// things down a lot. +const kDebugSelectionDumpPref = "metro.debug.selection.dumpRanges"; +// Dump message manager event traffic for selection. +const kDebugSelectionDumpEvents = "metro.debug.selection.dumpEvents"; +const kAsyncPanZoomEnabled = "layers.async-pan-zoom.enabled" + +/** + * TouchModule + * + * Handles all touch-related input such as dragging and tapping. + * + * The Fennec chrome DOM tree has elements that are augmented dynamically with + * custom JS properties that tell the TouchModule they have custom support for + * either dragging or clicking. These JS properties are JS objects that expose + * an interface supporting dragging or clicking (though currently we only look + * to drag scrollable elements). + * + * A custom dragger is a JS property that lives on a scrollable DOM element, + * accessible as myElement.customDragger. The customDragger must support the + * following interface: (The `scroller' argument is given for convenience, and + * is the object reference to the element's scrollbox object). + * + * dragStart(cX, cY, target, scroller) + * Signals the beginning of a drag. Coordinates are passed as + * client coordinates. target is copied from the event. + * + * dragStop(dx, dy, scroller) + * Signals the end of a drag. The dx, dy parameters may be non-zero to + * indicate one last drag movement. + * + * dragMove(dx, dy, scroller, isKinetic) + * Signals an input attempt to drag by dx, dy. + * + * There is a default dragger in case a scrollable element is dragged --- see + * the defaultDragger prototype property. + */ + +var TouchModule = { + _debugEvents: false, + _isCancelled: false, + _isCancellable: false, + + init: function init() { + this._dragData = new DragData(); + + this._dragger = null; + + this._targetScrollbox = null; + this._targetScrollInterface = null; + + this._kinetic = new KineticController(this._dragBy.bind(this), + this._kineticStop.bind(this)); + + // capture phase events + window.addEventListener("CancelTouchSequence", this, true); + window.addEventListener("keydown", this, true); + window.addEventListener("MozMouseHittest", this, true); + + // bubble phase + window.addEventListener("contextmenu", this, false); + window.addEventListener("touchstart", this, false); + window.addEventListener("touchmove", this, false); + window.addEventListener("touchend", this, false); + + Services.obs.addObserver(this, "Gesture:SingleTap", false); + Services.obs.addObserver(this, "Gesture:DoubleTap", false); + }, + + /* + * Events + */ + + handleEvent: function handleEvent(aEvent) { + switch (aEvent.type) { + case "contextmenu": + this._onContextMenu(aEvent); + break; + + case "CancelTouchSequence": + this.cancelPending(); + break; + + default: { + if (this._debugEvents) { + if (aEvent.type != "touchmove") + Util.dumpLn("TouchModule:", aEvent.type, aEvent.target); + } + + switch (aEvent.type) { + case "touchstart": + this._onTouchStart(aEvent); + break; + case "touchmove": + this._onTouchMove(aEvent); + break; + case "touchend": + this._onTouchEnd(aEvent); + break; + case "keydown": + this._handleKeyDown(aEvent); + break; + case "MozMouseHittest": + // Used by widget to hit test chrome vs content. Make sure the XUl scrollbars + // are counted as "chrome". Since the XUL scrollbars have sub-elements we walk + // the parent chain to ensure we catch all of those as well. + let onScrollbar = false; + for (let node = aEvent.originalTarget; node instanceof XULElement; node = node.parentNode) { + if (node.tagName == 'scrollbar') { + onScrollbar = true; + break; + } + } + if (onScrollbar || aEvent.target.ownerDocument == document) { + aEvent.preventDefault(); + } + aEvent.stopPropagation(); + break; + } + } + } + }, + + _handleKeyDown: function _handleKeyDown(aEvent) { + const TABKEY = 9; + if (aEvent.keyCode == TABKEY && !InputSourceHelper.isPrecise) { + if (Util.isEditable(aEvent.target) && + aEvent.target.selectionStart != aEvent.target.selectionEnd) { + SelectionHelperUI.closeEditSession(false); + } + setTimeout(function() { + let element = Browser.selectedBrowser.contentDocument.activeElement; + // We only want to attach monocles if we have an input, text area, + // there is selection, and the target element changed. + // Sometimes the target element won't change even though selection is + // cleared because of focus outside the browser. + if (Util.isEditable(element) && + !SelectionHelperUI.isActive && + element.selectionStart != element.selectionEnd && + // not e10s friendly + aEvent.target != element) { + let rect = element.getBoundingClientRect(); + SelectionHelperUI.attachEditSession(Browser.selectedBrowser, + rect.left + rect.width / 2, + rect.top + rect.height / 2); + } + }, 50); + } + }, + + observe: function BrowserUI_observe(aSubject, aTopic, aData) { + switch (aTopic) { + case "Gesture:SingleTap": + case "Gesture:DoubleTap": + Browser.selectedBrowser.messageManager.sendAsyncMessage(aTopic, JSON.parse(aData)); + break; + } + }, + + + sample: function sample(aTimeStamp) { + this._waitingForPaint = false; + }, + + /** + * This gets invoked by the input handler if another module grabs. We should + * reset our state or something here. This is probably doing the wrong thing + * in its current form. + */ + cancelPending: function cancelPending() { + this._doDragStop(); + + // Kinetic panning may have already been active or drag stop above may have + // made kinetic panning active. + this._kinetic.end(); + + this._targetScrollbox = null; + this._targetScrollInterface = null; + }, + + _onContextMenu: function _onContextMenu(aEvent) { + // bug 598965 - chrome UI should stop to be pannable once the + // context menu has appeared. + if (ContextMenuUI.popupState) { + this.cancelPending(); + } + }, + + /** Begin possible pan and send tap down event. */ + _onTouchStart: function _onTouchStart(aEvent) { + if (aEvent.touches.length > 1) + return; + + this._isCancelled = false; + this._isCancellable = true; + + if (aEvent.defaultPrevented) { + this._isCancelled = true; + return; + } + + let dragData = this._dragData; + if (dragData.dragging) { + // Somehow a mouse up was missed. + this._doDragStop(); + } + dragData.reset(); + this.dX = 0; + this.dY = 0; + + // walk up the DOM tree in search of nearest scrollable ancestor. nulls are + // returned if none found. + let [targetScrollbox, targetScrollInterface, dragger] + = ScrollUtils.getScrollboxFromElement(aEvent.originalTarget); + + // stop kinetic panning if targetScrollbox has changed + if (this._kinetic.isActive() && this._dragger != dragger) + this._kinetic.end(); + + this._targetScrollbox = targetScrollInterface ? targetScrollInterface.element : targetScrollbox; + this._targetScrollInterface = targetScrollInterface; + + if (!this._targetScrollbox) { + return; + } + + // Don't allow kinetic panning if APZC is enabled and the pan element is the deck + let deck = document.getElementById("browsers"); + if (Services.prefs.getBoolPref(kAsyncPanZoomEnabled) && + this._targetScrollbox == deck) { + return; + } + + // XXX shouldn't dragger always be valid here? + if (dragger) { + let draggable = dragger.isDraggable(targetScrollbox, targetScrollInterface); + dragData.locked = !draggable.x || !draggable.y; + if (draggable.x || draggable.y) { + this._dragger = dragger; + if (dragger.freeDrag) + dragData.alwaysFreeDrag = dragger.freeDrag(); + this._doDragStart(aEvent, draggable); + } + } + }, + + /** Send tap up event and any necessary full taps. */ + _onTouchEnd: function _onTouchEnd(aEvent) { + if (aEvent.touches.length > 0 || this._isCancelled || !this._targetScrollbox) + return; + + // onMouseMove will not record the delta change if we are waiting for a + // paint. Since this is the last input for this drag, we override the flag. + this._waitingForPaint = false; + this._onTouchMove(aEvent); + + let dragData = this._dragData; + this._doDragStop(); + }, + + /** + * If we're in a drag, do what we have to do to drag on. + */ + _onTouchMove: function _onTouchMove(aEvent) { + if (aEvent.touches.length > 1) + return; + + if (this._isCancellable) { + // only the first touchmove is cancellable. + this._isCancellable = false; + if (aEvent.defaultPrevented) { + this._isCancelled = true; + } + } + + if (this._isCancelled) + return; + + let touch = aEvent.changedTouches[0]; + if (!this._targetScrollbox) { + return; + } + + let dragData = this._dragData; + + if (dragData.dragging) { + let oldIsPan = dragData.isPan(); + dragData.setDragPosition(touch.screenX, touch.screenY); + dragData.setMousePosition(touch); + + // Kinetic panning is sensitive to time. It is more stable if it receives + // the mousemove events as they come. For dragging though, we only want + // to call _dragBy if we aren't waiting for a paint (so we don't spam the + // main browser loop with a bunch of redundant paints). + // + // Here, we feed kinetic panning drag differences for mouse events as + // come; for dragging, we build up a drag buffer in this.dX/this.dY and + // release it when we are ready to paint. + // + let [sX, sY] = dragData.panPosition(); + this.dX += dragData.prevPanX - sX; + this.dY += dragData.prevPanY - sY; + + if (dragData.isPan()) { + // Only pan when mouse event isn't part of a click. Prevent jittering on tap. + this._kinetic.addData(sX - dragData.prevPanX, sY - dragData.prevPanY); + + // dragBy will reset dX and dY values to 0 + this._dragBy(this.dX, this.dY); + + // Let everyone know when mousemove begins a pan + if (!oldIsPan && dragData.isPan()) { + let event = document.createEvent("Events"); + event.initEvent("PanBegin", true, false); + this._targetScrollbox.dispatchEvent(event); + + Browser.selectedBrowser.messageManager.sendAsyncMessage("Browser:PanBegin", {}); + } + } + } + }, + + /** + * Inform our dragger of a dragStart. + */ + _doDragStart: function _doDragStart(aEvent, aDraggable) { + let touch = aEvent.changedTouches[0]; + let dragData = this._dragData; + dragData.setDragStart(touch.screenX, touch.screenY, aDraggable); + this._kinetic.addData(0, 0); + this._dragStartTime = Date.now(); + if (!this._kinetic.isActive()) { + this._dragger.dragStart(touch.clientX, touch.clientY, touch.target, this._targetScrollInterface); + } + }, + + /** Finish a drag. */ + _doDragStop: function _doDragStop() { + let dragData = this._dragData; + if (!dragData.dragging) + return; + + dragData.endDrag(); + + // Note: it is possible for kinetic scrolling to be active from a + // mousedown/mouseup event previous to this one. In this case, we + // want the kinetic panner to tell our drag interface to stop. + + if (dragData.isPan()) { + if (Date.now() - this._dragStartTime > kStopKineticPanOnDragTimeout) + this._kinetic._velocity.set(0, 0); + + // Start kinetic pan if we aren't using async pan zoom or the scroll + // element is not browsers. + let deck = document.getElementById("browsers"); + if (!Services.prefs.getBoolPref(kAsyncPanZoomEnabled) || + this._targetScrollbox != deck) { + this._kinetic.start(); + } + } else { + this._kinetic.end(); + if (this._dragger) + this._dragger.dragStop(0, 0, this._targetScrollInterface); + this._dragger = null; + } + }, + + /** + * Used by _onTouchMove() above and by KineticController's timer to do the + * actual dragMove signalling to the dragger. We'd put this in _onTouchMove() + * but then KineticController would be adding to its own data as it signals + * the dragger of dragMove()s. + */ + _dragBy: function _dragBy(dX, dY, aIsKinetic) { + let dragged = true; + let dragData = this._dragData; + if (!this._waitingForPaint || aIsKinetic) { + let dragData = this._dragData; + dragged = this._dragger.dragMove(dX, dY, this._targetScrollInterface, aIsKinetic, + dragData._mouseX, dragData._mouseY); + if (dragged && !this._waitingForPaint) { + this._waitingForPaint = true; + mozRequestAnimationFrame(this); + } + this.dX = 0; + this.dY = 0; + } + if (!dragData.isPan()) + this._kinetic.pause(); + + return dragged; + }, + + /** Callback for kinetic scroller. */ + _kineticStop: function _kineticStop() { + // Kinetic panning could finish while user is panning, so don't finish + // the pan just yet. + let dragData = this._dragData; + if (!dragData.dragging) { + if (this._dragger) + this._dragger.dragStop(0, 0, this._targetScrollInterface); + this._dragger = null; + + let event = document.createEvent("Events"); + event.initEvent("PanFinished", true, false); + this._targetScrollbox.dispatchEvent(event); + } + }, + + toString: function toString() { + return '[TouchModule] {' + + '\n\tdragData=' + this._dragData + ', ' + + 'dragger=' + this._dragger + ', ' + + '\n\ttargetScroller=' + this._targetScrollInterface + '}'; + }, +}; + +var ScrollUtils = { + // threshold in pixels for sensing a tap as opposed to a pan + get tapRadius() { + let dpi = Util.displayDPI; + delete this.tapRadius; + return this.tapRadius = Services.prefs.getIntPref("ui.dragThresholdX") / 240 * dpi; + }, + + /** + * Walk up (parentward) the DOM tree from elem in search of a scrollable element. + * Return the element and its scroll interface if one is found, two nulls otherwise. + * + * This function will cache the pointer to the scroll interface on the element itself, + * so it is safe to call it many times without incurring the same XPConnect overhead + * as in the initial call. + */ + getScrollboxFromElement: function getScrollboxFromElement(elem) { + let scrollbox = null; + let qinterface = null; + + // if element is content or the startui page, get the browser scroll interface + if (elem.ownerDocument == Browser.selectedBrowser.contentDocument) { + elem = Browser.selectedBrowser; + } + for (; elem; elem = elem.parentNode) { + try { + if (elem.anonScrollBox) { + scrollbox = elem.anonScrollBox; + qinterface = scrollbox.boxObject.QueryInterface(Ci.nsIScrollBoxObject); + } else if (elem.scrollBoxObject) { + scrollbox = elem; + qinterface = elem.scrollBoxObject; + break; + } else if (elem.customDragger) { + scrollbox = elem; + break; + } else if (elem.boxObject) { + let qi = (elem._cachedSBO) ? elem._cachedSBO + : elem.boxObject.QueryInterface(Ci.nsIScrollBoxObject); + if (qi) { + scrollbox = elem; + scrollbox._cachedSBO = qinterface = qi; + break; + } + } + } catch (e) { /* we aren't here to deal with your exceptions, we'll just keep + traversing until we find something more well-behaved, as we + prefer default behaviour to whiny scrollers. */ } + } + return [scrollbox, qinterface, (scrollbox ? (scrollbox.customDragger || this._defaultDragger) : null)]; + }, + + /** Determine if the distance moved can be considered a pan */ + isPan: function isPan(aPoint, aPoint2) { + return (Math.abs(aPoint.x - aPoint2.x) > this.tapRadius || + Math.abs(aPoint.y - aPoint2.y) > this.tapRadius); + }, + + /** + * The default dragger object used by TouchModule when dragging a scrollable + * element that provides no customDragger. Simply performs the expected + * regular scrollBy calls on the scroller. + */ + _defaultDragger: { + isDraggable: function isDraggable(target, scroller) { + let sX = {}, sY = {}, + pX = {}, pY = {}; + scroller.getPosition(pX, pY); + scroller.getScrolledSize(sX, sY); + let rect = target.getBoundingClientRect(); + return { x: (sX.value > rect.width || pX.value != 0), + y: (sY.value > rect.height || pY.value != 0) }; + }, + + dragStart: function dragStart(cx, cy, target, scroller) { + scroller.element.addEventListener("PanBegin", this._showScrollbars, false); + }, + + dragStop: function dragStop(dx, dy, scroller) { + scroller.element.removeEventListener("PanBegin", this._showScrollbars, false); + return this.dragMove(dx, dy, scroller); + }, + + dragMove: function dragMove(dx, dy, scroller) { + if (scroller.getPosition) { + try { + let oldX = {}, oldY = {}; + scroller.getPosition(oldX, oldY); + + scroller.scrollBy(dx, dy); + + let newX = {}, newY = {}; + scroller.getPosition(newX, newY); + + return (newX.value != oldX.value) || (newY.value != oldY.value); + + } catch (e) { /* we have no time for whiny scrollers! */ } + } + + return false; + }, + + _showScrollbars: function _showScrollbars(aEvent) { + let scrollbox = aEvent.target; + scrollbox.setAttribute("panning", "true"); + + let hideScrollbars = function() { + scrollbox.removeEventListener("PanFinished", hideScrollbars, false); + scrollbox.removeEventListener("CancelTouchSequence", hideScrollbars, false); + scrollbox.removeAttribute("panning"); + } + + // Wait for panning to be completely finished before removing scrollbars + scrollbox.addEventListener("PanFinished", hideScrollbars, false); + scrollbox.addEventListener("CancelTouchSequence", hideScrollbars, false); + } + } +}; + +/** + * DragData handles processing drags on the screen, handling both + * locking of movement on one axis, and click detection. + */ +function DragData() { + this._domUtils = Cc["@mozilla.org/inspector/dom-utils;1"].getService(Ci.inIDOMUtils); + this._lockRevertThreshold = Util.displayDPI * kAxisLockRevertThreshold; + this.reset(); +}; + +DragData.prototype = { + reset: function reset() { + this.dragging = false; + this.sX = null; + this.sY = null; + this.locked = false; + this.stayLocked = false; + this.alwaysFreeDrag = false; + this.lockedX = null; + this.lockedY = null; + this._originX = null; + this._originY = null; + this.prevPanX = null; + this.prevPanY = null; + this._isPan = false; + }, + + /** Depending on drag data, locks sX,sY to X-axis or Y-axis of start position. */ + _lockAxis: function _lockAxis(sX, sY) { + if (this.locked) { + if (this.lockedX !== null) + sX = this.lockedX; + else if (this.lockedY !== null) + sY = this.lockedY; + return [sX, sY]; + } + else { + return [this._originX, this._originY]; + } + }, + + setMousePosition: function setMousePosition(aEvent) { + this._mouseX = aEvent.clientX; + this._mouseY = aEvent.clientY; + }, + + setDragPosition: function setDragPosition(sX, sY) { + // Check if drag is now a pan. + if (!this._isPan) { + this._isPan = ScrollUtils.isPan(new Point(this._originX, this._originY), new Point(sX, sY)); + if (this._isPan) { + this._resetActive(); + } + } + + // If now a pan, mark previous position where panning was. + if (this._isPan) { + let absX = Math.abs(this._originX - sX); + let absY = Math.abs(this._originY - sY); + + if (absX > this._lockRevertThreshold || absY > this._lockRevertThreshold) + this.stayLocked = true; + + // After the first lock, see if locking decision should be reverted. + if (!this.stayLocked) { + if (this.lockedX && absX > 3 * absY) + this.lockedX = null; + else if (this.lockedY && absY > 3 * absX) + this.lockedY = null; + } + + if (!this.locked) { + // look at difference from origin coord to lock movement, but only + // do it if initial movement is sufficient to detect intent + + // divide possibilty space into eight parts. Diagonals will allow + // free movement, while moving towards a cardinal will lock that + // axis. We pick a direction if you move more than twice as far + // on one axis than another, which should be an angle of about 30 + // degrees from the axis + + if (absX > 2.5 * absY) + this.lockedY = sY; + else if (absY > absX) + this.lockedX = sX; + + this.locked = true; + } + } + + // Never lock if the dragger requests it + if (this.alwaysFreeDrag) { + this.lockedY = null; + this.lockedX = null; + } + + // After pan lock, figure out previous panning position. Base it on last drag + // position so there isn't a jump in panning. + let [prevX, prevY] = this._lockAxis(this.sX, this.sY); + this.prevPanX = prevX; + this.prevPanY = prevY; + + this.sX = sX; + this.sY = sY; + }, + + setDragStart: function setDragStart(screenX, screenY, aDraggable) { + this.sX = this._originX = screenX; + this.sY = this._originY = screenY; + this.dragging = true; + + // If the target area is pannable only in one direction lock it early + // on the right axis + this.lockedX = !aDraggable.x ? screenX : null; + this.lockedY = !aDraggable.y ? screenY : null; + this.stayLocked = this.lockedX || this.lockedY; + this.locked = this.stayLocked; + }, + + endDrag: function endDrag() { + this._resetActive(); + this.dragging = false; + }, + + /** Returns true if drag should pan scrollboxes.*/ + isPan: function isPan() { + return this._isPan; + }, + + /** Return true if drag should be parsed as a click. */ + isClick: function isClick() { + return !this._isPan; + }, + + /** + * Returns the screen position for a pan. This factors in axis locking. + * @return Array of screen X and Y coordinates + */ + panPosition: function panPosition() { + return this._lockAxis(this.sX, this.sY); + }, + + /** dismiss the active state of the pan element */ + _resetActive: function _resetActive() { + let target = document.documentElement; + // If the target is active, toggle (turn off) the active flag. Otherwise do nothing. + if (this._domUtils.getContentState(target) & kStateActive) + this._domUtils.setContentState(target, kStateActive); + }, + + toString: function toString() { + return '[DragData] { sX,sY=' + this.sX + ',' + this.sY + ', dragging=' + this.dragging + ' }'; + } +}; + + +/** + * KineticController - a class to take drag position data and use it + * to do kinetic panning of a scrollable object. + * + * aPanBy is a function that will be called with the dx and dy + * generated by the kinetic algorithm. It should return true if the + * object was panned, false if there was no movement. + * + * There are two complicated things done here. One is calculating the + * initial velocity of the movement based on user input. Two is + * calculating the distance to move every frame. + */ +function KineticController(aPanBy, aEndCallback) { + this._panBy = aPanBy; + this._beforeEnd = aEndCallback; + + // These are used to calculate the position of the scroll panes during kinetic panning. Think of + // these points as vectors that are added together and multiplied by scalars. + this._position = new Point(0, 0); + this._velocity = new Point(0, 0); + this._acceleration = new Point(0, 0); + this._time = 0; + this._timeStart = 0; + + // How often do we change the position of the scroll pane? Too often and panning may jerk near + // the end. Too little and panning will be choppy. In milliseconds. + this._updateInterval = Services.prefs.getIntPref("browser.ui.kinetic.updateInterval"); + // Constants that affect the "friction" of the scroll pane. + this._exponentialC = Services.prefs.getIntPref("browser.ui.kinetic.exponentialC"); + this._polynomialC = Services.prefs.getIntPref("browser.ui.kinetic.polynomialC") / 1000000; + // Number of milliseconds that can contain a swipe. Movements earlier than this are disregarded. + this._swipeLength = Services.prefs.getIntPref("browser.ui.kinetic.swipeLength"); + + this._reset(); +} + +KineticController.prototype = { + _reset: function _reset() { + this._active = false; + this._paused = false; + this.momentumBuffer = []; + this._velocity.set(0, 0); + }, + + isActive: function isActive() { + return this._active; + }, + + _startTimer: function _startTimer() { + let self = this; + + let lastp = this._position; // track last position vector because pan takes deltas + let v0 = this._velocity; // initial velocity + let a = this._acceleration; // acceleration + let c = this._exponentialC; + let p = new Point(0, 0); + let dx, dy, t, realt; + + function calcP(v0, a, t) { + // Important traits for this function: + // p(t=0) is 0 + // p'(t=0) is v0 + // + // We use exponential to get a smoother stop, but by itself exponential + // is too smooth at the end. Adding a polynomial with the appropriate + // weight helps to balance + return v0 * Math.exp(-t / c) * -c + a * t * t + v0 * c; + } + + this._calcV = function(v0, a, t) { + return v0 * Math.exp(-t / c) + 2 * a * t; + } + + let callback = { + sample: function kineticHandleEvent(timeStamp) { + // Someone called end() on us between timer intervals + // or we are paused. + if (!self.isActive() || self._paused) + return; + + // To make animation end fast enough but to keep smoothness, average the ideal + // time frame (smooth animation) with the actual time lapse (end fast enough). + // Animation will never take longer than 2 times the ideal length of time. + realt = timeStamp - self._initialTime; + self._time += self._updateInterval; + t = (self._time + realt) / 2; + + // Calculate new position. + p.x = calcP(v0.x, a.x, t); + p.y = calcP(v0.y, a.y, t); + dx = Math.round(p.x - lastp.x); + dy = Math.round(p.y - lastp.y); + + // Test to see if movement is finished for each component. + if (dx * a.x > 0) { + dx = 0; + lastp.x = 0; + v0.x = 0; + a.x = 0; + } + // Symmetric to above case. + if (dy * a.y > 0) { + dy = 0; + lastp.y = 0; + v0.y = 0; + a.y = 0; + } + + if (v0.x == 0 && v0.y == 0) { + self.end(); + } else { + let panStop = false; + if (dx != 0 || dy != 0) { + try { panStop = !self._panBy(-dx, -dy, true); } catch (e) {} + lastp.add(dx, dy); + } + + if (panStop) + self.end(); + else + mozRequestAnimationFrame(this); + } + } + }; + + this._active = true; + this._paused = false; + mozRequestAnimationFrame(callback); + }, + + start: function start() { + function sign(x) { + return x ? ((x > 0) ? 1 : -1) : 0; + } + + function clampFromZero(x, closerToZero, furtherFromZero) { + if (x >= 0) + return Math.max(closerToZero, Math.min(furtherFromZero, x)); + return Math.min(-closerToZero, Math.max(-furtherFromZero, x)); + } + + let mb = this.momentumBuffer; + let mblen = this.momentumBuffer.length; + + let lastTime = mb[mblen - 1].t; + let distanceX = 0; + let distanceY = 0; + let swipeLength = this._swipeLength; + + // determine speed based on recorded input + let me; + for (let i = 0; i < mblen; i++) { + me = mb[i]; + if (lastTime - me.t < swipeLength) { + distanceX += me.dx; + distanceY += me.dy; + } + } + + let currentVelocityX = 0; + let currentVelocityY = 0; + + if (this.isActive()) { + // If active, then we expect this._calcV to be defined. + let currentTime = Date.now() - this._initialTime; + currentVelocityX = Util.clamp(this._calcV(this._velocity.x, this._acceleration.x, currentTime), -kMaxVelocity, kMaxVelocity); + currentVelocityY = Util.clamp(this._calcV(this._velocity.y, this._acceleration.y, currentTime), -kMaxVelocity, kMaxVelocity); + } + + if (currentVelocityX * this._velocity.x <= 0) + currentVelocityX = 0; + if (currentVelocityY * this._velocity.y <= 0) + currentVelocityY = 0; + + let swipeTime = Math.min(swipeLength, lastTime - mb[0].t); + this._velocity.x = clampFromZero((distanceX / swipeTime) + currentVelocityX, Math.abs(currentVelocityX), kMaxVelocity); + this._velocity.y = clampFromZero((distanceY / swipeTime) + currentVelocityY, Math.abs(currentVelocityY), kMaxVelocity); + + if (Math.abs(this._velocity.x) < kMinVelocity) + this._velocity.x = 0; + if (Math.abs(this._velocity.y) < kMinVelocity) + this._velocity.y = 0; + + // Set acceleration vector to opposite signs of velocity + this._acceleration.set(this._velocity.clone().map(sign).scale(-this._polynomialC)); + + this._position.set(0, 0); + this._initialTime = mozAnimationStartTime; + this._time = 0; + this.momentumBuffer = []; + + if (!this.isActive() || this._paused) + this._startTimer(); + + return true; + }, + + pause: function pause() { + this._paused = true; + }, + + end: function end() { + if (this.isActive()) { + if (this._beforeEnd) + this._beforeEnd(); + this._reset(); + } + }, + + addData: function addData(dx, dy) { + let mbLength = this.momentumBuffer.length; + let now = Date.now(); + + if (this.isActive()) { + // Stop active movement when dragging in other direction. + if (dx * this._velocity.x < 0 || dy * this._velocity.y < 0) + this.end(); + } + + this.momentumBuffer.push({'t': now, 'dx' : dx, 'dy' : dy}); + } +}; + + +/* + * Simple gestures support + */ + +var GestureModule = { + _debugEvents: false, + + init: function init() { + window.addEventListener("MozSwipeGesture", this, true); + }, + + /* + * Events + * + * 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 nsIDOMEvent information structure + */ + + handleEvent: function handleEvent(aEvent) { + try { + aEvent.stopPropagation(); + aEvent.preventDefault(); + if (this._debugEvents) Util.dumpLn("GestureModule:", aEvent.type); + switch (aEvent.type) { + case "MozSwipeGesture": + if (this._onSwipe(aEvent)) { + let event = document.createEvent("Events"); + event.initEvent("CancelTouchSequence", true, true); + aEvent.target.dispatchEvent(event); + } + break; + } + } catch (e) { + Util.dumpLn("Error while handling gesture event", aEvent.type, + "\nPlease report error at:", e); + Cu.reportError(e); + } + }, + + _onSwipe: function _onSwipe(aEvent) { + switch (aEvent.direction) { + case Ci.nsIDOMSimpleGestureEvent.DIRECTION_LEFT: + return this._tryCommand("cmd_forward"); + case Ci.nsIDOMSimpleGestureEvent.DIRECTION_RIGHT: + return this._tryCommand("cmd_back"); + } + return false; + }, + + _tryCommand: function _tryCommand(aId) { + if (document.getElementById(aId).getAttribute("disabled") == "true") + return false; + CommandUpdater.doCommand(aId); + return true; + }, +}; + +/** + * Helper to track when the user is using a precise pointing device (pen/mouse) + * versus an imprecise one (touch). + */ +var InputSourceHelper = { + isPrecise: false, + + init: function ish_init() { + Services.obs.addObserver(this, "metro_precise_input", false); + Services.obs.addObserver(this, "metro_imprecise_input", false); + }, + + _precise: function () { + if (!this.isPrecise) { + this.isPrecise = true; + this._fire("MozPrecisePointer"); + } + }, + + _imprecise: function () { + if (this.isPrecise) { + this.isPrecise = false; + this._fire("MozImprecisePointer"); + } + }, + + observe: function BrowserUI_observe(aSubject, aTopic, aData) { + switch (aTopic) { + case "metro_precise_input": + this._precise(); + break; + case "metro_imprecise_input": + this._imprecise(); + break; + } + }, + + fireUpdate: function fireUpdate() { + if (this.isPrecise) { + this._fire("MozPrecisePointer"); + } else { + this._fire("MozImprecisePointer"); + } + }, + + _fire: function (name) { + let event = document.createEvent("Events"); + event.initEvent(name, true, true); + window.dispatchEvent(event); + } +};