--- a/mobile/chrome/content/InputHandler.js
+++ b/mobile/chrome/content/InputHandler.js
@@ -37,16 +37,37 @@
* use your version of this file under the terms of the MPL, indicate your
* decision by deleting the provisions above and replace them with the notice
* and other provisions required by the GPL or the LGPL. If you do not delete
* the provisions above, a recipient may use your version of this file under
* the terms of any one of the MPL, the GPL or the LGPL.
*
* ***** END LICENSE BLOCK ***** */
+// how many msecs elapse before two taps are not a double tap
+const kDoubleClickInterval = 400;
+
+// milliseconds between mouse down and drag direction determined
+const kMsUntilLock = 50;
+
+// threshold in pixels for sensing a tap as opposed to a pan
+const kTapRadius = 15;
+
+// how many milliseconds between each kinetic pan update
+const kKineticUpdateInterval = 25;
+
+// How much speed is removed every update
+const kDecelerationRate = .10;
+
+// How sensitive kinetic scroll is to mouse movement
+const kSpeedSensitivity = .8;
+
+// How relevant x earlier milliseconds is to determining the speed.
+const kTimeRelevance = .01;
+
/**
* InputHandler
*
* The input handler is an arbiter between the Fennec chrome window inputs and any
* registered input modules. It keeps an array of input module objects. Incoming
* input events are wrapped in an EventInfo object and passed down to the input modules
* in the order of the modules array. Every registed module thus gets called with
* an EventInfo for each event that the InputHandler is registered to listen for.
@@ -394,28 +415,28 @@ InputHandler.EventInfo.prototype = {
* cx1, cy1, and second click at client coordinates cx2, cy2.
*
* There is a default dragger in case a scrollable element is dragged --- see
* the defaultDragger prototype property. There is no default clicker.
*/
function MouseModule(owner, browserViewContainer) {
this._owner = owner;
this._browserViewContainer = browserViewContainer;
- this._dragData = new DragData(this, 15, 200);
+ this._dragData = new DragData(this, kTapRadius);
this._dragger = null;
this._clicker = null;
this._downUpEvents = [];
this._targetScrollInterface = null;
var self = this;
this._kinetic = new KineticController(
function _dragByBound(dx, dy) { return self._dragBy(dx, dy); },
- function _dragStopBound() { return self._doDragStop(0, 0, true); }
+ function _dragStopBound() { return self._doDragStop(0, 0, 0, true); }
);
}
MouseModule.prototype = {
handleEvent: function handleEvent(evInfo) {
if (evInfo.event.button !== 0) // avoid all but a clean left click
return;
@@ -511,18 +532,18 @@ MouseModule.prototype = {
*/
_onMouseUp: function _onMouseUp(evInfo) {
let dragData = this._dragData;
let [sX, sY] = dragData.lockAxis(evInfo.event.screenX, evInfo.event.screenY);
this._movedOutOfRadius = this._movedOutOfRadius || dragData.isPointOutsideRadius(sX, sY);
- if (dragData.dragging) // XXX same check as this._dragger but we
- this._doDragStop(sX, sY); // are using both, no good reason
+ if (dragData.dragging) // XXX same check as this._dragger but we
+ this._doDragStop(sX, sY, evInfo.event.timeStamp); // are using both, no good reason
if (this._clicker)
this._clicker.mouseUp(evInfo.event.clientX, evInfo.event.clientY);
if (this._targetIsContent(evInfo.event)) {
this._recordEvent(evInfo);
this._doClick(this._movedOutOfRadius);
}
@@ -537,17 +558,17 @@ MouseModule.prototype = {
*/
_onMouseMove: function _onMouseMove(evInfo) {
let dragData = this._dragData;
if (dragData.dragging) {
let [sX, sY] = dragData.lockAxis(evInfo.event.screenX, evInfo.event.screenY);
evInfo.event.stopPropagation();
evInfo.event.preventDefault();
- this._doDragMove(sX, sY);
+ this._doDragMove(sX, sY, evInfo.event.timeStamp);
}
this._movedOutOfRadius = this._movedOutOfRadius ||
dragData.isPointOutsideRadius(evInfo.event.screenX, evInfo.event.screenY);
},
/**
* Check if the event concern the browser content
@@ -567,53 +588,53 @@ MouseModule.prototype = {
/**
* Inform our dragger of a dragStart and update kinetic with new data.
*/
_doDragStart: function _doDragStart(event) {
let dragData = this._dragData;
dragData.setDragStart(event.screenX, event.screenY);
- this._kinetic.addData(event.screenX, event.screenY);
+ this._kinetic.addData(event.screenX, event.screenY, event.timeStamp);
this._dragger.dragStart(event.clientX, event.clientY, event.target, this._targetScrollInterface);
},
/**
* Finish a drag. The third parameter is a secret one used to distinguish
* between the supposed end of drag caused by a mouseup and the real end
* of drag which happens when KineticController::end() is called.
*/
- _doDragStop: function _doDragStop(sX, sY, kineticStop) {
+ _doDragStop: function _doDragStop(sX, sY, t, kineticStop) {
let dragData = this._dragData;
if (!kineticStop) { // we're not really done, since now it is
// kinetic's turn to scroll about
let dx = dragData.sX - sX;
let dy = dragData.sY - sY;
dragData.endDrag();
- this._kinetic.addData(sX, sY);
+ this._kinetic.addData(sX, sY, t);
this._kinetic.start();
} else { // now we're done, says our secret 3rd argument
this._dragger.dragStop(0, 0, this._targetScrollInterface);
dragData.reset();
}
},
/**
* Update kinetic with new data and drag.
*/
- _doDragMove: function _doDragMove(sX, sY) {
+ _doDragMove: function _doDragMove(sX, sY, t) {
let dragData = this._dragData;
let dX = dragData.sX - sX;
let dY = dragData.sY - sY;
- this._kinetic.addData(sX, sY);
+ this._kinetic.addData(sX, sY, t);
return this._dragBy(dX, dY);
},
/**
* Used by _doDragMove() above and by KineticController's timer to do the
* actual dragMove signalling to the dragger. We'd put this in _doDragMove()
* but then KineticController would be adding to its own data as it signals
* the dragger of dragMove()s.
@@ -646,24 +667,23 @@ MouseModule.prototype = {
/**
* Commit another click event to our click buffer. The `click buffer' is a
* timeout initiated by the first click. If the timeout is still alive when
* another click is committed, then the click buffer forms a double click, and
* the timeout is cancelled. Otherwise, the timeout issues a single click to
* the clicker.
*/
_commitAnotherClick: function _commitAnotherClick() {
- const doubleClickInterval = 400;
if (this._clickTimeout) { // we're waiting for a second click for double
window.clearTimeout(this._clickTimeout);
this._doDoubleClick();
} else {
this._clickTimeout = window.setTimeout(function _clickTimeout(self) { self._doSingleClick(); },
- doubleClickInterval, this);
+ kDoubleClickInterval, this);
}
},
/**
* Endpoint of _commitAnotherClick(). Finalize a single click and tell the clicker.
*/
_doSingleClick: function _doSingleClick() {
//dump('doing single click with ' + this._downUpEvents.length + '\n');
@@ -819,25 +839,22 @@ MouseModule.prototype = {
+ '\n\tclickTimeout=' + this._clickTimeout + '\n }';
}
};
/**
* DragData handles processing drags on the screen, handling both
* locking of movement on one axis, and click detection.
*/
-function DragData(owner, dragRadius, dragStartTimeoutLength) {
+function DragData(owner, dragRadius) {
this._owner = owner;
this._dragRadius = dragRadius;
this.reset();
};
-/* milliseconds between mouse down and drag direction determined */
-const kMsUntilLock = 50;
-
DragData.prototype = {
reset: function reset() {
this.dragging = false;
this.sX = null;
this.sY = null;
this.alreadyLocked = false;
this.lockedX = null;
this.lockedY = null;
@@ -907,17 +924,17 @@ DragData.prototype = {
return [sX, sY];
},
isPointOutsideRadius: function isPointOutsideRadius(sX, sY) {
if (this._originX === null)
return false;
return (Math.pow(sX - this._originX, 2) + Math.pow(sY - this._originY, 2)) >
- (2 * Math.pow(this._dragRadius, 2));
+ (Math.pow(this._dragRadius, 2));
},
toString: function toString() {
return '[DragData] { sX,sY=' + this.sX + ',' + this.sY + ', dragging=' + this.dragging + ' }';
}
};
@@ -931,26 +948,39 @@ DragData.prototype = {
*/
function KineticController(aPanBy, aEndCallback) {
this._panBy = aPanBy;
this._timer = null;
this._beforeEnd = aEndCallback;
try {
this._updateInterval = gPrefService.getIntPref("browser.ui.kinetic.updateInterval");
- // In preferences this value is an int. We divide so that it can be between 0 and 1;
- this._emaAlpha = gPrefService.getIntPref("browser.ui.kinetic.ema.alphaValue") / 10;
+ } catch(e) {
+ this._updateInterval = kKineticUpdateInterval;
+ }
+
+ try {
// In preferences this value is an int. We divide so that it can be a percent.
this._decelerationRate = gPrefService.getIntPref("browser.ui.kinetic.decelerationRate") / 100;
+ } catch (e) {
+ this._decelerationRate = kDecelerationRate;
+ };
+
+ try {
+ // In preferences this value is an int. We divide so that it can be a percent.
+ this._speedSensitivity = gPrefService.getIntPref("browser.ui.kinetic.speedsensitivity") / 100;
+ } catch(e) {
+ this._speedSensitivity = kSpeedSensitivity;
}
- catch (e) {
- this._updateInterval = 33;
- this._emaAlpha = .8;
- this._decelerationRate = .15;
- };
+
+ try {
+ this._timeRelevance = gPrefService.getIntPref("browser.ui.kinetic.timerelevance") / 100;
+ } catch(e) {
+ this._timeRelevance = kTimeRelevance;
+ }
this._reset();
}
KineticController.prototype = {
_reset: function _reset() {
if (this._timer != null) {
this._timer.cancel();
@@ -978,17 +1008,16 @@ KineticController.prototype = {
//dump(" speeds: " + self._speedX + " " + self._speedY + "\n");
if (self._speedX == 0 && self._speedY == 0) {
self.end();
return;
}
let dx = Math.round(self._speedX * self._updateInterval);
let dy = Math.round(self._speedY * self._updateInterval);
- //dump("dx, dy: " + dx + " " + dy + "\n");
let panned = false;
try { panned = self._panBy(-dx, -dy); } catch (e) {}
if (!panned) {
self.end();
return;
}
@@ -1020,58 +1049,66 @@ KineticController.prototype = {
let mblen = this.momentumBuffer.length;
// If we don't have at least 2 events do not do kinetic panning
if (mblen < 2) {
this.end();
return false;
}
- function ema(currentSpeed, lastSpeed, alpha) {
- return alpha * currentSpeed + (1 - alpha) * lastSpeed;
- };
+ let lastTime = mb[mblen - 1].t;
+ let weightedSpeedSumX = 0;
+ let weightedSpeedSumY = 0;
+ let weightSum = 0;
+ let prev = mb[0];
- // build arrays of each movement's speed in pixels/ms
- let prev = mb[0];
+ // determine speed based on recorded input, giving the most weight to inputs
+ // closest to the end
for (let i = 1; i < mblen; i++) {
let me = mb[i];
-
+ let weight = Math.exp((me.t - lastTime) * this._timeRelevance);
let timeDiff = me.t - prev.t;
-
- this._speedX = ema( ((me.sx - prev.sx) / timeDiff), this._speedX, this._emaAlpha);
- this._speedY = ema( ((me.sy - prev.sy) / timeDiff), this._speedY, this._emaAlpha);
-
+ weightSum += weight;
+ weightedSpeedSumX += weight * (me.sx - prev.sx) / timeDiff;
+ weightedSpeedSumY += weight * (me.sy - prev.sy) / timeDiff;
prev = me;
}
+ this._speedX = (weightedSpeedSumX / weightSum) * this._speedSensitivity;
+ this._speedY = (weightedSpeedSumY / weightSum) * this._speedSensitivity;
+
// fire off our kinetic timer which will do all the work
this._startTimer();
return true;
},
end: function end() {
if (this._beforeEnd)
this._beforeEnd();
this._reset();
},
- addData: function addData(sx, sy) {
+ addData: function addData(sx, sy, t) {
// if we're active, end that move before adding data
if (this.isActive())
this.end();
let mbLength = this.momentumBuffer.length;
+ let now = t || Date.now();
+
// avoid adding duplicates which would otherwise slow down the speed
- let now = Date.now();
-
if (mbLength > 0) {
let mbLast = this.momentumBuffer[mbLength - 1];
- if ((mbLast.sx == sx && mbLast.sy == sy) || mbLast.t == now)
+ if ((mbLast.sx == sx && mbLast.sy == sy) || mbLast.t == now) {
+ mbLast.sx = sx;
+ mbLast.sy = sy;
+ mbLast.t = t;
return;
+ }
}
// Util.dumpLn("adding t:", now, ", sx: ", sx, ", sy: ", sy);
this.momentumBuffer.push({'t': now, 'sx' : sx, 'sy' : sy});
}
};
/**