diff options
author | Pierre Ossman <ossman@cendio.se> | 2020-06-10 13:59:10 +0200 |
---|---|---|
committer | Samuel Mannehed <samuel@cendio.se> | 2020-06-12 09:18:46 +0200 |
commit | f84bc57bdaf6ed67804fee7d9caadd94c65d1bac (patch) | |
tree | 6ad770ea23d54e14770c881097d49f9aff55ff2e | |
parent | 4a87038080ee8d9ad8eb9f276fc6acde6ef87467 (diff) | |
download | novnc-f84bc57bdaf6ed67804fee7d9caadd94c65d1bac.tar.gz |
Move wheel event handling to RFB class
The Mouse class does very little now so it mostly just obfuscate things.
Move everything directly in to the RFB class instead.
-rw-r--r-- | core/input/mouse.js | 70 | ||||
-rw-r--r-- | core/rfb.js | 66 | ||||
-rw-r--r-- | tests/test.mouse.js | 82 | ||||
-rw-r--r-- | tests/test.rfb.js | 212 |
4 files changed, 221 insertions, 209 deletions
diff --git a/core/input/mouse.js b/core/input/mouse.js index 8c917d0..794adfe 100644 --- a/core/input/mouse.js +++ b/core/input/mouse.js @@ -7,22 +7,16 @@ import * as Log from '../util/logging.js'; import { setCapture, stopEvent, getPointerEvent } from '../util/events.js'; -const WHEEL_STEP = 10; // Delta threshold for a mouse wheel step -const WHEEL_LINE_HEIGHT = 19; - export default class Mouse { constructor(target) { this._target = target || document; this._pos = null; - this._accumulatedWheelDeltaX = 0; - this._accumulatedWheelDeltaY = 0; this._eventHandlers = { 'mousedown': this._handleMouseDown.bind(this), 'mouseup': this._handleMouseUp.bind(this), 'mousemove': this._handleMouseMove.bind(this), - 'mousewheel': this._handleMouseWheel.bind(this), 'mousedisable': this._handleMouseDisable.bind(this) }; @@ -61,68 +55,6 @@ export default class Mouse { 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() { - - 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() { - - 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; - } - - _handleMouseWheel(e) { - this._updateMousePosition(e); - - let dX = e.deltaX; - let dY = e.deltaY; - - // Pixel units unless it's non-zero. - // Note that if deltamode is line or page won't matter since we aren't - // 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(); - } - if (Math.abs(this._accumulatedWheelDeltaY) > WHEEL_STEP) { - this._generateWheelStepY(); - } - - stopEvent(e); - } - _handleMouseMove(e) { this._updateMousePosition(e); this.onmousemove(this._pos.x, this._pos.y); @@ -172,7 +104,6 @@ export default class Mouse { t.addEventListener('mousedown', this._eventHandlers.mousedown); t.addEventListener('mouseup', this._eventHandlers.mouseup); t.addEventListener('mousemove', this._eventHandlers.mousemove); - t.addEventListener('wheel', this._eventHandlers.mousewheel); // Prevent middle-click pasting (see above for why we bind to document) document.addEventListener('click', this._eventHandlers.mousedisable); @@ -188,7 +119,6 @@ export default class Mouse { t.removeEventListener('mousedown', this._eventHandlers.mousedown); t.removeEventListener('mouseup', this._eventHandlers.mouseup); t.removeEventListener('mousemove', this._eventHandlers.mousemove); - t.removeEventListener('wheel', this._eventHandlers.mousewheel); document.removeEventListener('click', this._eventHandlers.mousedisable); diff --git a/core/rfb.js b/core/rfb.js index 9db375f..4d1dcf1 100644 --- a/core/rfb.js +++ b/core/rfb.js @@ -41,6 +41,10 @@ const DEFAULT_BACKGROUND = 'rgb(40, 40, 40)'; // Minimum wait (ms) between two mouse moves const MOUSE_MOVE_DELAY = 17; +// Wheel thresholds +const WHEEL_STEP = 10; // Pixels needed for one step +const WHEEL_LINE_HEIGHT = 19; // Assumed pixels for one line step + // Gesture thresholds const GESTURE_ZOOMSENS = 75; const GESTURE_SCRLSENS = 50; @@ -152,6 +156,8 @@ export default class RFB extends EventTargetMixin { this._viewportDragging = false; this._viewportDragPos = {}; this._viewportHasMoved = false; + this._accumulatedWheelDeltaX = 0; + this._accumulatedWheelDeltaY = 0; // Gesture state this._gestureLastTapTime = null; @@ -163,6 +169,7 @@ export default class RFB extends EventTargetMixin { this._eventHandlers = { focusCanvas: this._focusCanvas.bind(this), windowResize: this._windowResize.bind(this), + handleWheel: this._handleWheel.bind(this), handleGesture: this._handleGesture.bind(this), }; @@ -532,6 +539,9 @@ export default class RFB extends EventTargetMixin { this._canvas.addEventListener("mousedown", this._eventHandlers.focusCanvas); this._canvas.addEventListener("touchstart", this._eventHandlers.focusCanvas); + // Wheel events + this._canvas.addEventListener("wheel", this._eventHandlers.handleWheel); + // Gesture events this._canvas.addEventListener("gesturestart", this._eventHandlers.handleGesture); this._canvas.addEventListener("gesturemove", this._eventHandlers.handleGesture); @@ -546,6 +556,7 @@ export default class RFB extends EventTargetMixin { this._canvas.removeEventListener("gesturestart", this._eventHandlers.handleGesture); this._canvas.removeEventListener("gesturemove", this._eventHandlers.handleGesture); this._canvas.removeEventListener("gestureend", this._eventHandlers.handleGesture); + this._canvas.removeEventListener("wheel", this._eventHandlers.handleWheel); this._canvas.removeEventListener("mousedown", this._eventHandlers.focusCanvas); this._canvas.removeEventListener("touchstart", this._eventHandlers.focusCanvas); window.removeEventListener('resize', this._eventHandlers.windowResize); @@ -939,6 +950,61 @@ export default class RFB extends EventTargetMixin { this._display.absY(y), mask); } + _handleWheel(ev) { + if (this._rfbConnectionState !== 'connected') { return; } + if (this._viewOnly) { return; } // View only, skip mouse events + + ev.stopPropagation(); + ev.preventDefault(); + + let pos = clientToElement(ev.clientX, ev.clientY, + this._canvas); + + let dX = ev.deltaX; + let dY = ev.deltaY; + + // Pixel units unless it's non-zero. + // Note that if deltamode is line or page won't matter since we aren't + // 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 (ev.deltaMode !== 0) { + dX *= WHEEL_LINE_HEIGHT; + dY *= WHEEL_LINE_HEIGHT; + } + + // 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. + this._accumulatedWheelDeltaX += dX; + this._accumulatedWheelDeltaY += dY; + + // Generate a mouse wheel step event when the accumulated delta + // for one of the axes is large enough. + if (Math.abs(this._accumulatedWheelDeltaX) > WHEEL_STEP) { + if (this._accumulatedWheelDeltaX < 0) { + this._handleMouseButton(pos.x, pos.y, true, 1 << 5); + this._handleMouseButton(pos.x, pos.y, false, 1 << 5); + } else if (this._accumulatedWheelDeltaX > 0) { + this._handleMouseButton(pos.x, pos.y, true, 1 << 6); + this._handleMouseButton(pos.x, pos.y, false, 1 << 6); + } + + this._accumulatedWheelDeltaX = 0; + } + if (Math.abs(this._accumulatedWheelDeltaY) > WHEEL_STEP) { + if (this._accumulatedWheelDeltaY < 0) { + this._handleMouseButton(pos.x, pos.y, true, 1 << 3); + this._handleMouseButton(pos.x, pos.y, false, 1 << 3); + } else if (this._accumulatedWheelDeltaY > 0) { + this._handleMouseButton(pos.x, pos.y, true, 1 << 4); + this._handleMouseButton(pos.x, pos.y, false, 1 << 4); + } + + this._accumulatedWheelDeltaY = 0; + } + } + _handleTapEvent(ev, bmask) { let pos = clientToElement(ev.detail.clientX, ev.detail.clientY, this._canvas); diff --git a/tests/test.mouse.js b/tests/test.mouse.js index 13bd0c6..25d5219 100644 --- a/tests/test.mouse.js +++ b/tests/test.mouse.js @@ -69,87 +69,5 @@ describe('Mouse Event Handling', function () { mouse._handleMouseMove(mouseevent('mousemove', { clientX: 50, clientY: 20 })); }); - it('should decode mousewheel events', function (done) { - let calls = 0; - const mouse = new Mouse(target); - mouse.onmousebutton = (x, y, down, bmask) => { - calls++; - expect(bmask).to.be.equal(1<<6); - if (calls === 1) { - expect(down).to.be.equal(1); - } else if (calls === 2) { - expect(down).to.not.be.equal(1); - done(); - } - }; - mouse._handleMouseWheel(mouseevent('mousewheel', - { deltaX: 50, deltaY: 0, - deltaMode: 0})); - }); - }); - - describe('Accumulate mouse wheel events with small delta', function () { - - it('should accumulate wheel events if small enough', function () { - const mouse = new Mouse(target); - mouse.onmousebutton = sinon.spy(); - - mouse._handleMouseWheel(mouseevent( - 'mousewheel', { clientX: 18, clientY: 40, - deltaX: 4, deltaY: 0, deltaMode: 0 })); - mouse._handleMouseWheel(mouseevent( - 'mousewheel', { clientX: 18, clientY: 40, - deltaX: 4, deltaY: 0, deltaMode: 0 })); - - // threshold is 10 - expect(mouse._accumulatedWheelDeltaX).to.be.equal(8); - - mouse._handleMouseWheel(mouseevent( - 'mousewheel', { clientX: 18, clientY: 40, - deltaX: 4, deltaY: 0, deltaMode: 0 })); - - expect(mouse.onmousebutton).to.have.callCount(2); // mouse down and up - - 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(mouse.onmousebutton).to.have.callCount(2); // still - }); - - it('should not accumulate large wheel events', function () { - const mouse = new Mouse(target); - mouse.onmousebutton = sinon.spy(); - - mouse._handleMouseWheel(mouseevent( - 'mousewheel', { clientX: 18, clientY: 40, - deltaX: 11, deltaY: 0, deltaMode: 0 })); - mouse._handleMouseWheel(mouseevent( - 'mousewheel', { clientX: 18, clientY: 40, - deltaX: 0, deltaY: 70, deltaMode: 0 })); - mouse._handleMouseWheel(mouseevent( - 'mousewheel', { clientX: 18, clientY: 40, - deltaX: 400, deltaY: 400, deltaMode: 0 })); - - expect(mouse.onmousebutton).to.have.callCount(8); // mouse down and up - }); - - it('should account for non-zero deltaMode', function () { - const mouse = new Mouse(target); - mouse.onmousebutton = sinon.spy(); - - mouse._handleMouseWheel(mouseevent( - 'mousewheel', { clientX: 18, clientY: 40, - deltaX: 0, deltaY: 2, deltaMode: 1 })); - - mouse._handleMouseWheel(mouseevent( - 'mousewheel', { clientX: 18, clientY: 40, - deltaX: 1, deltaY: 0, deltaMode: 2 })); - - expect(mouse.onmousebutton).to.have.callCount(4); // mouse down and up - }); }); }); diff --git a/tests/test.rfb.js b/tests/test.rfb.js index e46f40c..909b300 100644 --- a/tests/test.rfb.js +++ b/tests/test.rfb.js @@ -2731,20 +2731,44 @@ describe('Remote Frame Buffer Protocol Client', function () { describe('Asynchronous Events', function () { let client; + let pointerEvent; + let keyEvent; + let qemuKeyEvent; + beforeEach(function () { client = makeRFB(); + client._display.resize(100, 100); + + pointerEvent = sinon.spy(RFB.messages, 'pointerEvent'); + keyEvent = sinon.spy(RFB.messages, 'keyEvent'); + qemuKeyEvent = sinon.spy(RFB.messages, 'QEMUExtendedKeyEvent'); }); - describe('Mouse event handlers', function () { - beforeEach(function () { - this.clock = sinon.useFakeTimers(Date.now()); - sinon.spy(RFB.messages, 'pointerEvent'); - }); - afterEach(function () { - this.clock.restore(); - RFB.messages.pointerEvent.restore(); - }); + afterEach(function () { + pointerEvent.restore(); + keyEvent.restore(); + qemuKeyEvent.restore(); + }); + function elementToClient(x, y) { + let res = { x: 0, y: 0 }; + + let bounds = client._canvas.getBoundingClientRect(); + + /* + * If the canvas is on a fractional position we will calculate + * a fractional mouse position. But that gets truncated when we + * send the event, AND the same thing happens in RFB when it + * generates the PointerEvent message. To compensate for that + * fact we round the value upwards here. + */ + res.x = Math.ceil(bounds.left + x); + res.y = Math.ceil(bounds.top + y); + + return res; + } + + describe('Mouse Events', function () { it('should not send button messages in view-only mode', function () { client._viewOnly = true; client._handleMouseButton(0, 0, 1, 0x001); @@ -2878,7 +2902,128 @@ describe('Remote Frame Buffer Protocol Client', function () { }); }); - describe('Keyboard Event Handlers', function () { + describe('Wheel Events', function () { + function sendWheelEvent(x, y, dx, dy, mode=0) { + let pos = elementToClient(x, y); + let ev; + + try { + ev = new WheelEvent('wheel', + { 'screenX': pos.x + window.screenX, + 'screenY': pos.y + window.screenY, + 'clientX': pos.x, + 'clientY': pos.y, + 'deltaX': dx, + 'deltaY': dy, + 'deltaMode': mode }); + } catch (e) { + ev = document.createEvent('WheelEvent'); + ev.initWheelEvent('wheel', true, true, window, 0, + pos.x + window.screenX, + pos.y + window.screenY, + pos.x, pos.y, + 0, null, "", + dx, dy, 0, mode); + } + + client._canvas.dispatchEvent(ev); + } + + it('should handle wheel up event', function () { + sendWheelEvent(10, 10, 0, -50); + + expect(pointerEvent).to.have.been.calledTwice; + expect(pointerEvent.firstCall).to.have.been.calledWith(client._sock, + 10, 10, 1<<3); + expect(pointerEvent.secondCall).to.have.been.calledWith(client._sock, + 10, 10, 0); + }); + + it('should handle wheel down event', function () { + sendWheelEvent(10, 10, 0, 50); + + expect(pointerEvent).to.have.been.calledTwice; + expect(pointerEvent.firstCall).to.have.been.calledWith(client._sock, + 10, 10, 1<<4); + expect(pointerEvent.secondCall).to.have.been.calledWith(client._sock, + 10, 10, 0); + }); + + it('should handle wheel left event', function () { + sendWheelEvent(10, 10, -50, 0); + + expect(pointerEvent).to.have.been.calledTwice; + expect(pointerEvent.firstCall).to.have.been.calledWith(client._sock, + 10, 10, 1<<5); + expect(pointerEvent.secondCall).to.have.been.calledWith(client._sock, + 10, 10, 0); + }); + + it('should handle wheel right event', function () { + sendWheelEvent(10, 10, 50, 0); + + expect(pointerEvent).to.have.been.calledTwice; + expect(pointerEvent.firstCall).to.have.been.calledWith(client._sock, + 10, 10, 1<<6); + expect(pointerEvent.secondCall).to.have.been.calledWith(client._sock, + 10, 10, 0); + }); + + it('should ignore wheel when in view only', function () { + client._viewOnly = true; + + sendWheelEvent(10, 10, 50, 0); + + expect(pointerEvent).to.not.have.been.called; + }); + + it('should accumulate wheel events if small enough', function () { + sendWheelEvent(10, 10, 0, 4); + sendWheelEvent(10, 10, 0, 4); + + expect(pointerEvent).to.not.have.been.called; + + sendWheelEvent(10, 10, 0, 4); + + expect(pointerEvent).to.have.been.calledTwice; + expect(pointerEvent.firstCall).to.have.been.calledWith(client._sock, + 10, 10, 1<<4); + expect(pointerEvent.secondCall).to.have.been.calledWith(client._sock, + 10, 10, 0); + }); + + it('should not accumulate large wheel events', function () { + sendWheelEvent(10, 10, 0, 400); + + expect(pointerEvent).to.have.been.calledTwice; + expect(pointerEvent.firstCall).to.have.been.calledWith(client._sock, + 10, 10, 1<<4); + expect(pointerEvent.secondCall).to.have.been.calledWith(client._sock, + 10, 10, 0); + }); + + it('should handle line based wheel event', function () { + sendWheelEvent(10, 10, 0, 1, 1); + + expect(pointerEvent).to.have.been.calledTwice; + expect(pointerEvent.firstCall).to.have.been.calledWith(client._sock, + 10, 10, 1<<4); + expect(pointerEvent.secondCall).to.have.been.calledWith(client._sock, + 10, 10, 0); + }); + + it('should handle page based wheel event', function () { + sendWheelEvent(10, 10, 0, 1, 2); + + expect(pointerEvent).to.have.been.calledTwice; + expect(pointerEvent.firstCall).to.have.been.calledWith(client._sock, + 10, 10, 1<<4); + expect(pointerEvent.secondCall).to.have.been.calledWith(client._sock, + 10, 10, 0); + }); + }); + + describe('Keyboard Events', function () { it('should send a key message on a key press', function () { client._handleKeyEvent(0x41, 'KeyA', true); const keyMsg = {_sQ: new Uint8Array(8), _sQlen: 0, flush: () => {}}; @@ -2895,42 +3040,14 @@ describe('Remote Frame Buffer Protocol Client', function () { }); describe('Gesture event handlers', function () { - let pointerEvent; - beforeEach(function () { // Touch events and gestures are not supported on IE if (browser.isIE()) { this.skip(); return; } - - pointerEvent = sinon.spy(RFB.messages, 'pointerEvent'); - - client._display.resize(100, 100); }); - afterEach(function () { - pointerEvent.restore(); - }); - - function elementToClient(x, y) { - let res = { x: 0, y: 0 }; - - let bounds = client._canvas.getBoundingClientRect(); - - /* - * If the canvas is on a fractional position we will calculate - * a fractional mouse position. But that gets truncated when we - * send the event, AND the same thing happens in RFB when it - * generates the PointerEvent message. To compensate for that - * fact we round the value upwards here. - */ - res.x = Math.ceil(bounds.left + x); - res.y = Math.ceil(bounds.top + y); - - return res; - } - function gestureStart(gestureType, x, y, magnitudeX = 0, magnitudeY = 0) { let pos = elementToClient(x, y); @@ -3392,25 +3509,6 @@ describe('Remote Frame Buffer Protocol Client', function () { }); describe('Gesture pinch', function () { - let keyEvent; - let qemuKeyEvent; - - beforeEach(function () { - // Touch events and gestures are not supported on IE - if (browser.isIE()) { - this.skip(); - return; - } - - keyEvent = sinon.spy(RFB.messages, 'keyEvent'); - qemuKeyEvent = sinon.spy(RFB.messages, 'QEMUExtendedKeyEvent'); - }); - - afterEach(function () { - keyEvent.restore(); - qemuKeyEvent.restore(); - }); - it('should handle gesture pinch in events', function () { let keysym = KeyTable.XK_Control_L; let bmask = 0x10; // Button mask for scroll down |