Combine small mouse wheel events
The VNC protocol can't handle different deltas or speeds for a mouse wheel event. When using a device that sends a lot of small mouse wheel events, instead of fewer larger steps, the effect was that mouse wheel scrolling was way to sensitive. This patch looks at the delta of wheel events and doesn't send events until the combined delta has passed a threshold. Single events that doesn't pass the threshold get sent after a timeout in order to not loose any events. Fixes #577.
This commit is contained in:
parent
c1e2785fb6
commit
28b004fd70
|
@ -13,11 +13,21 @@ import { isTouchDevice } from '../util/browsers.js';
|
||||||
import { setCapture, stopEvent, getPointerEvent } from '../util/events.js';
|
import { setCapture, stopEvent, getPointerEvent } from '../util/events.js';
|
||||||
import { set_defaults, make_properties } from '../util/properties.js';
|
import { set_defaults, make_properties } from '../util/properties.js';
|
||||||
|
|
||||||
|
var WHEEL_STEP = 10; // Delta threshold for a mouse wheel step
|
||||||
|
var WHEEL_STEP_TIMEOUT = 50; // ms
|
||||||
|
var WHEEL_LINE_HEIGHT = 19;
|
||||||
|
|
||||||
export default function Mouse(defaults) {
|
export default function Mouse(defaults) {
|
||||||
|
|
||||||
this._doubleClickTimer = null;
|
this._doubleClickTimer = null;
|
||||||
this._lastTouchPos = null;
|
this._lastTouchPos = null;
|
||||||
|
|
||||||
|
this._pos = null;
|
||||||
|
this._wheelStepXTimer = null;
|
||||||
|
this._wheelStepYTimer = null;
|
||||||
|
this._accumulatedWheelDeltaX = 0;
|
||||||
|
this._accumulatedWheelDeltaY = 0;
|
||||||
|
|
||||||
// Configuration attributes
|
// Configuration attributes
|
||||||
set_defaults(this, defaults, {
|
set_defaults(this, defaults, {
|
||||||
'target': document,
|
'target': document,
|
||||||
|
@ -44,7 +54,8 @@ Mouse.prototype = {
|
||||||
_handleMouseButton: function (e, down) {
|
_handleMouseButton: function (e, down) {
|
||||||
if (!this._focused) { return; }
|
if (!this._focused) { return; }
|
||||||
|
|
||||||
var pos = this._getMousePosition(e);
|
this._updateMousePosition(e);
|
||||||
|
var pos = this._pos;
|
||||||
|
|
||||||
var bmask;
|
var bmask;
|
||||||
if (e.touches || e.changedTouches) {
|
if (e.touches || e.changedTouches) {
|
||||||
|
@ -108,27 +119,82 @@ Mouse.prototype = {
|
||||||
this._handleMouseButton(e, 0);
|
this._handleMouseButton(e, 0);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Mouse wheel events are sent in steps over VNC. This means that the VNC
|
||||||
|
// protocol can't handle a wheel event with specific distance or speed.
|
||||||
|
// Therefor, if we get a lot of small mouse wheel events we combine them.
|
||||||
|
_generateWheelStepX: function () {
|
||||||
|
|
||||||
|
if (this._accumulatedWheelDeltaX < 0) {
|
||||||
|
this._onMouseButton(this._pos.x, this._pos.y, 1, 1 << 5);
|
||||||
|
this._onMouseButton(this._pos.x, this._pos.y, 0, 1 << 5);
|
||||||
|
} else if (this._accumulatedWheelDeltaX > 0) {
|
||||||
|
this._onMouseButton(this._pos.x, this._pos.y, 1, 1 << 6);
|
||||||
|
this._onMouseButton(this._pos.x, this._pos.y, 0, 1 << 6);
|
||||||
|
}
|
||||||
|
|
||||||
|
this._accumulatedWheelDeltaX = 0;
|
||||||
|
},
|
||||||
|
|
||||||
|
_generateWheelStepY: function () {
|
||||||
|
|
||||||
|
if (this._accumulatedWheelDeltaY < 0) {
|
||||||
|
this._onMouseButton(this._pos.x, this._pos.y, 1, 1 << 3);
|
||||||
|
this._onMouseButton(this._pos.x, this._pos.y, 0, 1 << 3);
|
||||||
|
} else if (this._accumulatedWheelDeltaY > 0) {
|
||||||
|
this._onMouseButton(this._pos.x, this._pos.y, 1, 1 << 4);
|
||||||
|
this._onMouseButton(this._pos.x, this._pos.y, 0, 1 << 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
this._accumulatedWheelDeltaY = 0;
|
||||||
|
},
|
||||||
|
|
||||||
|
_resetWheelStepTimers: function () {
|
||||||
|
window.clearTimeout(this._wheelStepXTimer);
|
||||||
|
window.clearTimeout(this._wheelStepYTimer);
|
||||||
|
this._wheelStepXTimer = null;
|
||||||
|
this._wheelStepYTimer = null;
|
||||||
|
},
|
||||||
|
|
||||||
_handleMouseWheel: function (e) {
|
_handleMouseWheel: function (e) {
|
||||||
if (!this._focused) { return; }
|
if (!this._focused || !this._onMouseButton) { return; }
|
||||||
|
|
||||||
var pos = this._getMousePosition(e);
|
this._resetWheelStepTimers();
|
||||||
|
|
||||||
if (this._onMouseButton) {
|
this._updateMousePosition(e);
|
||||||
if (e.deltaX < 0) {
|
|
||||||
this._onMouseButton(pos.x, pos.y, 1, 1 << 5);
|
|
||||||
this._onMouseButton(pos.x, pos.y, 0, 1 << 5);
|
|
||||||
} else if (e.deltaX > 0) {
|
|
||||||
this._onMouseButton(pos.x, pos.y, 1, 1 << 6);
|
|
||||||
this._onMouseButton(pos.x, pos.y, 0, 1 << 6);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (e.deltaY < 0) {
|
var dX = e.deltaX;
|
||||||
this._onMouseButton(pos.x, pos.y, 1, 1 << 3);
|
var dY = e.deltaY;
|
||||||
this._onMouseButton(pos.x, pos.y, 0, 1 << 3);
|
|
||||||
} else if (e.deltaY > 0) {
|
// Pixel units unless it's non-zero.
|
||||||
this._onMouseButton(pos.x, pos.y, 1, 1 << 4);
|
// Note that if deltamode is line or page won't matter since we aren't
|
||||||
this._onMouseButton(pos.x, pos.y, 0, 1 << 4);
|
// sending the mouse wheel delta to the server anyway.
|
||||||
}
|
// The difference between pixel and line can be important however since
|
||||||
|
// we have a threshold that can be smaller than the line height.
|
||||||
|
if (e.deltaMode !== 0) {
|
||||||
|
dX *= WHEEL_LINE_HEIGHT;
|
||||||
|
dY *= WHEEL_LINE_HEIGHT;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._accumulatedWheelDeltaX += dX;
|
||||||
|
this._accumulatedWheelDeltaY += dY;
|
||||||
|
|
||||||
|
// Generate a mouse wheel step event when the accumulated delta
|
||||||
|
// for one of the axes is large enough.
|
||||||
|
// Small delta events that do not pass the threshold get sent
|
||||||
|
// after a timeout.
|
||||||
|
if (Math.abs(this._accumulatedWheelDeltaX) > WHEEL_STEP) {
|
||||||
|
this._generateWheelStepX();
|
||||||
|
} else {
|
||||||
|
this._wheelStepXTimer =
|
||||||
|
window.setTimeout(this._generateWheelStepX.bind(this),
|
||||||
|
WHEEL_STEP_TIMEOUT);
|
||||||
|
}
|
||||||
|
if (Math.abs(this._accumulatedWheelDeltaY) > WHEEL_STEP) {
|
||||||
|
this._generateWheelStepY();
|
||||||
|
} else {
|
||||||
|
this._wheelStepYTimer =
|
||||||
|
window.setTimeout(this._generateWheelStepY.bind(this),
|
||||||
|
WHEEL_STEP_TIMEOUT);
|
||||||
}
|
}
|
||||||
|
|
||||||
stopEvent(e);
|
stopEvent(e);
|
||||||
|
@ -137,9 +203,9 @@ Mouse.prototype = {
|
||||||
_handleMouseMove: function (e) {
|
_handleMouseMove: function (e) {
|
||||||
if (! this._focused) { return; }
|
if (! this._focused) { return; }
|
||||||
|
|
||||||
var pos = this._getMousePosition(e);
|
this._updateMousePosition(e);
|
||||||
if (this._onMouseMove) {
|
if (this._onMouseMove) {
|
||||||
this._onMouseMove(pos.x, pos.y);
|
this._onMouseMove(this._pos.x, this._pos.y);
|
||||||
}
|
}
|
||||||
stopEvent(e);
|
stopEvent(e);
|
||||||
},
|
},
|
||||||
|
@ -158,8 +224,8 @@ Mouse.prototype = {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
// Return coordinates relative to target
|
// Update coordinates relative to target
|
||||||
_getMousePosition: function(e) {
|
_updateMousePosition: function(e) {
|
||||||
e = getPointerEvent(e);
|
e = getPointerEvent(e);
|
||||||
var bounds = this._target.getBoundingClientRect();
|
var bounds = this._target.getBoundingClientRect();
|
||||||
var x, y;
|
var x, y;
|
||||||
|
@ -178,7 +244,7 @@ Mouse.prototype = {
|
||||||
} else {
|
} else {
|
||||||
y = e.clientY - bounds.top;
|
y = e.clientY - bounds.top;
|
||||||
}
|
}
|
||||||
return {x:x, y:y};
|
this._pos = {x:x, y:y};
|
||||||
},
|
},
|
||||||
|
|
||||||
// Public methods
|
// Public methods
|
||||||
|
@ -206,6 +272,8 @@ Mouse.prototype = {
|
||||||
ungrab: function () {
|
ungrab: function () {
|
||||||
var c = this._target;
|
var c = this._target;
|
||||||
|
|
||||||
|
this._resetWheelStepTimers();
|
||||||
|
|
||||||
if (isTouchDevice) {
|
if (isTouchDevice) {
|
||||||
c.removeEventListener('touchstart', this._eventHandlers.mousedown);
|
c.removeEventListener('touchstart', this._eventHandlers.mousedown);
|
||||||
c.removeEventListener('touchend', this._eventHandlers.mouseup);
|
c.removeEventListener('touchend', this._eventHandlers.mouseup);
|
||||||
|
|
|
@ -223,4 +223,91 @@ describe('Mouse Event Handling', function() {
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('Accumulate mouse wheel events with small delta', function() {
|
||||||
|
|
||||||
|
beforeEach(function () { this.clock = sinon.useFakeTimers(); });
|
||||||
|
afterEach(function () { this.clock.restore(); });
|
||||||
|
|
||||||
|
it('should accumulate wheel events if small enough', function () {
|
||||||
|
var callback = sinon.spy();
|
||||||
|
var mouse = new Mouse({ onMouseButton: callback, target: target });
|
||||||
|
|
||||||
|
mouse._handleMouseWheel(mouseevent(
|
||||||
|
'mousewheel', { clientX: 18, clientY: 40,
|
||||||
|
deltaX: 4, deltaY: 0, deltaMode: 0 }));
|
||||||
|
this.clock.tick(10);
|
||||||
|
mouse._handleMouseWheel(mouseevent(
|
||||||
|
'mousewheel', { clientX: 18, clientY: 40,
|
||||||
|
deltaX: 4, deltaY: 0, deltaMode: 0 }));
|
||||||
|
|
||||||
|
// threshold is 10
|
||||||
|
expect(mouse._accumulatedWheelDeltaX).to.be.equal(8);
|
||||||
|
|
||||||
|
this.clock.tick(10);
|
||||||
|
mouse._handleMouseWheel(mouseevent(
|
||||||
|
'mousewheel', { clientX: 18, clientY: 40,
|
||||||
|
deltaX: 4, deltaY: 0, deltaMode: 0 }));
|
||||||
|
|
||||||
|
expect(callback).to.have.callCount(2); // mouse down and up
|
||||||
|
|
||||||
|
this.clock.tick(10);
|
||||||
|
mouse._handleMouseWheel(mouseevent(
|
||||||
|
'mousewheel', { clientX: 18, clientY: 40,
|
||||||
|
deltaX: 4, deltaY: 9, deltaMode: 0 }));
|
||||||
|
|
||||||
|
expect(mouse._accumulatedWheelDeltaX).to.be.equal(4);
|
||||||
|
expect(mouse._accumulatedWheelDeltaY).to.be.equal(9);
|
||||||
|
|
||||||
|
expect(callback).to.have.callCount(2); // still
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not accumulate large wheel events', function () {
|
||||||
|
var callback = sinon.spy();
|
||||||
|
var mouse = new Mouse({ onMouseButton: callback, target: target });
|
||||||
|
|
||||||
|
mouse._handleMouseWheel(mouseevent(
|
||||||
|
'mousewheel', { clientX: 18, clientY: 40,
|
||||||
|
deltaX: 11, deltaY: 0, deltaMode: 0 }));
|
||||||
|
this.clock.tick(10);
|
||||||
|
mouse._handleMouseWheel(mouseevent(
|
||||||
|
'mousewheel', { clientX: 18, clientY: 40,
|
||||||
|
deltaX: 0, deltaY: 70, deltaMode: 0 }));
|
||||||
|
this.clock.tick(10);
|
||||||
|
mouse._handleMouseWheel(mouseevent(
|
||||||
|
'mousewheel', { clientX: 18, clientY: 40,
|
||||||
|
deltaX: 400, deltaY: 400, deltaMode: 0 }));
|
||||||
|
|
||||||
|
expect(callback).to.have.callCount(8); // mouse down and up
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should send even small wheel events after a timeout', function () {
|
||||||
|
var callback = sinon.spy();
|
||||||
|
var mouse = new Mouse({ onMouseButton: callback, target: target });
|
||||||
|
|
||||||
|
mouse._handleMouseWheel(mouseevent(
|
||||||
|
'mousewheel', { clientX: 18, clientY: 40,
|
||||||
|
deltaX: 1, deltaY: 0, deltaMode: 0 }));
|
||||||
|
this.clock.tick(51); // timeout on 50 ms
|
||||||
|
|
||||||
|
expect(callback).to.have.callCount(2); // mouse down and up
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should account for non-zero deltaMode', function () {
|
||||||
|
var callback = sinon.spy();
|
||||||
|
var mouse = new Mouse({ onMouseButton: callback, target: target });
|
||||||
|
|
||||||
|
mouse._handleMouseWheel(mouseevent(
|
||||||
|
'mousewheel', { clientX: 18, clientY: 40,
|
||||||
|
deltaX: 0, deltaY: 2, deltaMode: 1 }));
|
||||||
|
|
||||||
|
this.clock.tick(10);
|
||||||
|
|
||||||
|
mouse._handleMouseWheel(mouseevent(
|
||||||
|
'mousewheel', { clientX: 18, clientY: 40,
|
||||||
|
deltaX: 1, deltaY: 0, deltaMode: 2 }));
|
||||||
|
|
||||||
|
expect(callback).to.have.callCount(4); // mouse down and up
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in New Issue