From c509e6d9c875aed87d09580bb7d5d27c943623c4 Mon Sep 17 00:00:00 2001 From: Samuel Mannehed Date: Sun, 17 Sep 2017 16:29:59 +0200 Subject: [PATCH 1/4] Add tests for mouse module --- tests/test.mouse.js | 226 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 226 insertions(+) create mode 100644 tests/test.mouse.js diff --git a/tests/test.mouse.js b/tests/test.mouse.js new file mode 100644 index 00000000..d75340e4 --- /dev/null +++ b/tests/test.mouse.js @@ -0,0 +1,226 @@ +var assert = chai.assert; +var expect = chai.expect; + +import Mouse from '../core/input/mouse.js'; +import * as eventUtils from '../core/util/events.js'; + +/* jshint newcap: false, expr: true */ +describe('Mouse Event Handling', function() { + "use strict"; + + sinon.stub(eventUtils, 'setCapture'); + // This function is only used on target (the canvas) + // and for these tests we can assume that the canvas is 100x100 + // located at coordinates 10x10 + sinon.stub(Element.prototype, 'getBoundingClientRect').returns( + {left: 10, right: 110, top: 10, bottom: 110, width: 100, height: 100}); + var target = document.createElement('canvas'); + + // The real constructors might not work everywhere we + // want to run these tests + var mouseevent, touchevent; + mouseevent = touchevent = function(typeArg, MouseEventInit) { + var e = { type: typeArg }; + for (var key in MouseEventInit) { + e[key] = MouseEventInit[key]; + } + e.stopPropagation = sinon.spy(); + e.preventDefault = sinon.spy(); + return e; + }; + + describe('Decode Mouse Events', function() { + it('should decode mousedown events', function(done) { + var mouse = new Mouse({ + onMouseButton: function(x, y, down, bmask) { + expect(bmask).to.be.equal(0x01); + expect(down).to.be.equal(1); + done(); + }, + target: target + }); + mouse._handleMouseDown(mouseevent('mousedown', { button: '0x01' })); + }); + it('should decode mouseup events', function(done) { + var calls = 0; + var mouse = new Mouse({ + onMouseButton: function(x, y, down, bmask) { + expect(bmask).to.be.equal(0x01); + if (calls++ === 1) { + expect(down).to.not.be.equal(1); + done(); + } + }, + target: target + }); + mouse._handleMouseDown(mouseevent('mousedown', { button: '0x01' })); + mouse._handleMouseUp(mouseevent('mouseup', { button: '0x01' })); + }); + it('should decode mousemove events', function(done) { + var mouse = new Mouse({ + onMouseMove: function(x, y) { + // Note that target relative coordinates are sent + expect(x).to.be.equal(40); + expect(y).to.be.equal(10); + done(); + }, + target: target + }); + mouse._handleMouseMove(mouseevent('mousemove', + { clientX: 50, clientY: 20 })); + }); + it('should decode mousewheel events', function(done) { + var calls = 0; + var mouse = new Mouse({ + onMouseButton: function(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(); + } + }, + target: target + }); + mouse._handleMouseWheel(mouseevent('mousewheel', + { deltaX: 50, deltaY: 0, + deltaMode: 0})); + }); + }); + + describe('Double-click for Touch', function() { + + beforeEach(function () { this.clock = sinon.useFakeTimers(); }); + afterEach(function () { this.clock.restore(); }); + + it('should use same pos for 2nd tap if close enough', function(done) { + var calls = 0; + var mouse = new Mouse({ + onMouseButton: function(x, y, down, bmask) { + calls++; + if (calls === 1) { + expect(down).to.be.equal(1); + expect(x).to.be.equal(68); + expect(y).to.be.equal(36); + } else if (calls === 3) { + expect(down).to.be.equal(1); + expect(x).to.be.equal(68); + expect(y).to.be.equal(36); + done(); + } + }, + target: target + }); + // touch events are sent in an array of events + // with one item for each touch point + mouse._handleMouseDown(touchevent( + 'touchstart', { touches: [{ clientX: 78, clientY: 46 }]})); + this.clock.tick(10); + mouse._handleMouseUp(touchevent( + 'touchend', { touches: [{ clientX: 79, clientY: 45 }]})); + this.clock.tick(200); + mouse._handleMouseDown(touchevent( + 'touchstart', { touches: [{ clientX: 67, clientY: 35 }]})); + this.clock.tick(10); + mouse._handleMouseUp(touchevent( + 'touchend', { touches: [{ clientX: 66, clientY: 36 }]})); + }); + + it('should not modify 2nd tap pos if far apart', function(done) { + var calls = 0; + var mouse = new Mouse({ + onMouseButton: function(x, y, down, bmask) { + calls++; + if (calls === 1) { + expect(down).to.be.equal(1); + expect(x).to.be.equal(68); + expect(y).to.be.equal(36); + } else if (calls === 3) { + expect(down).to.be.equal(1); + expect(x).to.not.be.equal(68); + expect(y).to.not.be.equal(36); + done(); + } + }, + target: target + }); + mouse._handleMouseDown(touchevent( + 'touchstart', { touches: [{ clientX: 78, clientY: 46 }]})); + this.clock.tick(10); + mouse._handleMouseUp(touchevent( + 'touchend', { touches: [{ clientX: 79, clientY: 45 }]})); + this.clock.tick(200); + mouse._handleMouseDown(touchevent( + 'touchstart', { touches: [{ clientX: 57, clientY: 35 }]})); + this.clock.tick(10); + mouse._handleMouseUp(touchevent( + 'touchend', { touches: [{ clientX: 56, clientY: 36 }]})); + }); + + it('should not modify 2nd tap pos if not soon enough', function(done) { + var calls = 0; + var mouse = new Mouse({ + onMouseButton: function(x, y, down, bmask) { + calls++; + if (calls === 1) { + expect(down).to.be.equal(1); + expect(x).to.be.equal(68); + expect(y).to.be.equal(36); + } else if (calls === 3) { + expect(down).to.be.equal(1); + expect(x).to.not.be.equal(68); + expect(y).to.not.be.equal(36); + done(); + } + }, + target: target + }); + mouse._handleMouseDown(touchevent( + 'touchstart', { touches: [{ clientX: 78, clientY: 46 }]})); + this.clock.tick(10); + mouse._handleMouseUp(touchevent( + 'touchend', { touches: [{ clientX: 79, clientY: 45 }]})); + this.clock.tick(500); + mouse._handleMouseDown(touchevent( + 'touchstart', { touches: [{ clientX: 67, clientY: 35 }]})); + this.clock.tick(10); + mouse._handleMouseUp(touchevent( + 'touchend', { touches: [{ clientX: 66, clientY: 36 }]})); + }); + + it('should not modify 2nd tap pos if not touch', function(done) { + var calls = 0; + var mouse = new Mouse({ + onMouseButton: function(x, y, down, bmask) { + calls++; + if (calls === 1) { + expect(down).to.be.equal(1); + expect(x).to.be.equal(68); + expect(y).to.be.equal(36); + } else if (calls === 3) { + expect(down).to.be.equal(1); + expect(x).to.not.be.equal(68); + expect(y).to.not.be.equal(36); + done(); + } + }, + target: target + }); + mouse._handleMouseDown(mouseevent( + 'mousedown', { button: '0x01', clientX: 78, clientY: 46 })); + this.clock.tick(10); + mouse._handleMouseUp(mouseevent( + 'mouseup', { button: '0x01', clientX: 79, clientY: 45 })); + this.clock.tick(200); + mouse._handleMouseDown(mouseevent( + 'mousedown', { button: '0x01', clientX: 67, clientY: 35 })); + this.clock.tick(10); + mouse._handleMouseUp(mouseevent( + 'mouseup', { button: '0x01', clientX: 66, clientY: 36 })); + }); + + }); + +}); From c1e2785fb69701240f71969ee761780ede9e4a9f Mon Sep 17 00:00:00 2001 From: Samuel Mannehed Date: Tue, 12 Sep 2017 11:03:39 +0200 Subject: [PATCH 2/4] Split devices.js into keyboard.js and mouse.js --- core/input/{devices.js => keyboard.js} | 225 +----------------------- core/input/mouse.js | 232 +++++++++++++++++++++++++ core/rfb.js | 3 +- tests/input.html | 3 +- tests/test.keyboard.js | 2 +- tests/vnc_perf.html | 4 +- 6 files changed, 241 insertions(+), 228 deletions(-) rename core/input/{devices.js => keyboard.js} (59%) create mode 100644 core/input/mouse.js diff --git a/core/input/devices.js b/core/input/keyboard.js similarity index 59% rename from core/input/devices.js rename to core/input/keyboard.js index ae09c7f8..7aa6288a 100644 --- a/core/input/devices.js +++ b/core/input/keyboard.js @@ -9,8 +9,7 @@ /*global window, Util */ import * as Log from '../util/logging.js'; -import { isTouchDevice } from '../util/browsers.js'; -import { setCapture, stopEvent, getPointerEvent } from '../util/events.js'; +import { stopEvent } from '../util/events.js'; import { set_defaults, make_properties } from '../util/properties.js'; import * as KeyboardUtil from "./util.js"; import KeyTable from "./keysym.js"; @@ -19,7 +18,7 @@ import KeyTable from "./keysym.js"; // Keyboard event handler // -function Keyboard(defaults) { +export default function Keyboard(defaults) { this._keyDownList = {}; // List of depressed keys // (even if they are happy) this._pendingKey = null; // Key waiting for keypress @@ -353,223 +352,3 @@ make_properties(Keyboard, [ ['onKeyEvent', 'rw', 'func'] // Handler for key press/release ]); - -function Mouse(defaults) { - - this._doubleClickTimer = null; - this._lastTouchPos = null; - - // Configuration attributes - set_defaults(this, defaults, { - 'target': document, - 'focused': true, - 'touchButton': 1 - }); - - 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) - }; -}; - -Mouse.prototype = { - // private methods - - _resetDoubleClickTimer: function () { - this._doubleClickTimer = null; - }, - - _handleMouseButton: function (e, down) { - if (!this._focused) { return; } - - var pos = this._getMousePosition(e); - - var bmask; - if (e.touches || e.changedTouches) { - // Touch device - - // When two touches occur within 500 ms of each other and are - // close enough together a double click is triggered. - if (down == 1) { - if (this._doubleClickTimer === null) { - this._lastTouchPos = pos; - } else { - clearTimeout(this._doubleClickTimer); - - // When the distance between the two touches is small enough - // force the position of the latter touch to the position of - // the first. - - var xs = this._lastTouchPos.x - pos.x; - var ys = this._lastTouchPos.y - pos.y; - var d = Math.sqrt((xs * xs) + (ys * ys)); - - // The goal is to trigger on a certain physical width, the - // devicePixelRatio brings us a bit closer but is not optimal. - var threshold = 20 * (window.devicePixelRatio || 1); - if (d < threshold) { - pos = this._lastTouchPos; - } - } - this._doubleClickTimer = setTimeout(this._resetDoubleClickTimer.bind(this), 500); - } - bmask = this._touchButton; - // If bmask is set - } else if (e.which) { - /* everything except IE */ - bmask = 1 << e.button; - } else { - /* IE including 9 */ - bmask = (e.button & 0x1) + // Left - (e.button & 0x2) * 2 + // Right - (e.button & 0x4) / 2; // Middle - } - - if (this._onMouseButton) { - Log.Debug("onMouseButton " + (down ? "down" : "up") + - ", x: " + pos.x + ", y: " + pos.y + ", bmask: " + bmask); - this._onMouseButton(pos.x, pos.y, down, bmask); - } - stopEvent(e); - }, - - _handleMouseDown: function (e) { - // Touch events have implicit capture - if (e.type === "mousedown") { - setCapture(this._target); - } - - this._handleMouseButton(e, 1); - }, - - _handleMouseUp: function (e) { - this._handleMouseButton(e, 0); - }, - - _handleMouseWheel: function (e) { - if (!this._focused) { return; } - - var pos = this._getMousePosition(e); - - if (this._onMouseButton) { - 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) { - this._onMouseButton(pos.x, pos.y, 1, 1 << 3); - this._onMouseButton(pos.x, pos.y, 0, 1 << 3); - } else if (e.deltaY > 0) { - this._onMouseButton(pos.x, pos.y, 1, 1 << 4); - this._onMouseButton(pos.x, pos.y, 0, 1 << 4); - } - } - - stopEvent(e); - }, - - _handleMouseMove: function (e) { - if (! this._focused) { return; } - - var pos = this._getMousePosition(e); - if (this._onMouseMove) { - this._onMouseMove(pos.x, pos.y); - } - stopEvent(e); - }, - - _handleMouseDisable: function (e) { - if (!this._focused) { return; } - - /* - * Stop propagation if inside canvas area - * Note: This is only needed for the 'click' event as it fails - * to fire properly for the target element so we have - * to listen on the document element instead. - */ - if (e.target == this._target) { - stopEvent(e); - } - }, - - // Return coordinates relative to target - _getMousePosition: function(e) { - e = getPointerEvent(e); - var bounds = this._target.getBoundingClientRect(); - var x, y; - // Clip to target bounds - if (e.clientX < bounds.left) { - x = 0; - } else if (e.clientX >= bounds.right) { - x = bounds.width - 1; - } else { - x = e.clientX - bounds.left; - } - if (e.clientY < bounds.top) { - y = 0; - } else if (e.clientY >= bounds.bottom) { - y = bounds.height - 1; - } else { - y = e.clientY - bounds.top; - } - return {x:x, y:y}; - }, - - // Public methods - grab: function () { - var c = this._target; - - if (isTouchDevice) { - c.addEventListener('touchstart', this._eventHandlers.mousedown); - c.addEventListener('touchend', this._eventHandlers.mouseup); - c.addEventListener('touchmove', this._eventHandlers.mousemove); - } - c.addEventListener('mousedown', this._eventHandlers.mousedown); - c.addEventListener('mouseup', this._eventHandlers.mouseup); - c.addEventListener('mousemove', this._eventHandlers.mousemove); - c.addEventListener('wheel', this._eventHandlers.mousewheel); - - /* Prevent middle-click pasting (see above for why we bind to document) */ - document.addEventListener('click', this._eventHandlers.mousedisable); - - /* preventDefault() on mousedown doesn't stop this event for some - reason so we have to explicitly block it */ - c.addEventListener('contextmenu', this._eventHandlers.mousedisable); - }, - - ungrab: function () { - var c = this._target; - - if (isTouchDevice) { - c.removeEventListener('touchstart', this._eventHandlers.mousedown); - c.removeEventListener('touchend', this._eventHandlers.mouseup); - c.removeEventListener('touchmove', this._eventHandlers.mousemove); - } - c.removeEventListener('mousedown', this._eventHandlers.mousedown); - c.removeEventListener('mouseup', this._eventHandlers.mouseup); - c.removeEventListener('mousemove', this._eventHandlers.mousemove); - c.removeEventListener('wheel', this._eventHandlers.mousewheel); - - document.removeEventListener('click', this._eventHandlers.mousedisable); - - c.removeEventListener('contextmenu', this._eventHandlers.mousedisable); - } -}; - -make_properties(Mouse, [ - ['target', 'ro', 'dom'], // DOM element that captures mouse input - ['focused', 'rw', 'bool'], // Capture and send mouse clicks/movement - - ['onMouseButton', 'rw', 'func'], // Handler for mouse button click/release - ['onMouseMove', 'rw', 'func'], // Handler for mouse movement - ['touchButton', 'rw', 'int'] // Button mask (1, 2, 4) for touch devices (0 means ignore clicks) -]); - -export { Keyboard, Mouse }; diff --git a/core/input/mouse.js b/core/input/mouse.js new file mode 100644 index 00000000..f17f9b93 --- /dev/null +++ b/core/input/mouse.js @@ -0,0 +1,232 @@ +/* + * noVNC: HTML5 VNC client + * Copyright (C) 2012 Joel Martin + * Copyright (C) 2013 Samuel Mannehed for Cendio AB + * Licensed under MPL 2.0 or any later version (see LICENSE.txt) + */ + +/*jslint browser: true, white: false */ +/*global window, Util */ + +import * as Log from '../util/logging.js'; +import { isTouchDevice } from '../util/browsers.js'; +import { setCapture, stopEvent, getPointerEvent } from '../util/events.js'; +import { set_defaults, make_properties } from '../util/properties.js'; + +export default function Mouse(defaults) { + + this._doubleClickTimer = null; + this._lastTouchPos = null; + + // Configuration attributes + set_defaults(this, defaults, { + 'target': document, + 'focused': true, + 'touchButton': 1 + }); + + 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) + }; +}; + +Mouse.prototype = { + // private methods + + _resetDoubleClickTimer: function () { + this._doubleClickTimer = null; + }, + + _handleMouseButton: function (e, down) { + if (!this._focused) { return; } + + var pos = this._getMousePosition(e); + + var bmask; + if (e.touches || e.changedTouches) { + // Touch device + + // When two touches occur within 500 ms of each other and are + // close enough together a double click is triggered. + if (down == 1) { + if (this._doubleClickTimer === null) { + this._lastTouchPos = pos; + } else { + clearTimeout(this._doubleClickTimer); + + // When the distance between the two touches is small enough + // force the position of the latter touch to the position of + // the first. + + var xs = this._lastTouchPos.x - pos.x; + var ys = this._lastTouchPos.y - pos.y; + var d = Math.sqrt((xs * xs) + (ys * ys)); + + // The goal is to trigger on a certain physical width, the + // devicePixelRatio brings us a bit closer but is not optimal. + var threshold = 20 * (window.devicePixelRatio || 1); + if (d < threshold) { + pos = this._lastTouchPos; + } + } + this._doubleClickTimer = setTimeout(this._resetDoubleClickTimer.bind(this), 500); + } + bmask = this._touchButton; + // If bmask is set + } else if (e.which) { + /* everything except IE */ + bmask = 1 << e.button; + } else { + /* IE including 9 */ + bmask = (e.button & 0x1) + // Left + (e.button & 0x2) * 2 + // Right + (e.button & 0x4) / 2; // Middle + } + + if (this._onMouseButton) { + Log.Debug("onMouseButton " + (down ? "down" : "up") + + ", x: " + pos.x + ", y: " + pos.y + ", bmask: " + bmask); + this._onMouseButton(pos.x, pos.y, down, bmask); + } + stopEvent(e); + }, + + _handleMouseDown: function (e) { + // Touch events have implicit capture + if (e.type === "mousedown") { + setCapture(this._target); + } + + this._handleMouseButton(e, 1); + }, + + _handleMouseUp: function (e) { + this._handleMouseButton(e, 0); + }, + + _handleMouseWheel: function (e) { + if (!this._focused) { return; } + + var pos = this._getMousePosition(e); + + if (this._onMouseButton) { + 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) { + this._onMouseButton(pos.x, pos.y, 1, 1 << 3); + this._onMouseButton(pos.x, pos.y, 0, 1 << 3); + } else if (e.deltaY > 0) { + this._onMouseButton(pos.x, pos.y, 1, 1 << 4); + this._onMouseButton(pos.x, pos.y, 0, 1 << 4); + } + } + + stopEvent(e); + }, + + _handleMouseMove: function (e) { + if (! this._focused) { return; } + + var pos = this._getMousePosition(e); + if (this._onMouseMove) { + this._onMouseMove(pos.x, pos.y); + } + stopEvent(e); + }, + + _handleMouseDisable: function (e) { + if (!this._focused) { return; } + + /* + * Stop propagation if inside canvas area + * Note: This is only needed for the 'click' event as it fails + * to fire properly for the target element so we have + * to listen on the document element instead. + */ + if (e.target == this._target) { + stopEvent(e); + } + }, + + // Return coordinates relative to target + _getMousePosition: function(e) { + e = getPointerEvent(e); + var bounds = this._target.getBoundingClientRect(); + var x, y; + // Clip to target bounds + if (e.clientX < bounds.left) { + x = 0; + } else if (e.clientX >= bounds.right) { + x = bounds.width - 1; + } else { + x = e.clientX - bounds.left; + } + if (e.clientY < bounds.top) { + y = 0; + } else if (e.clientY >= bounds.bottom) { + y = bounds.height - 1; + } else { + y = e.clientY - bounds.top; + } + return {x:x, y:y}; + }, + + // Public methods + grab: function () { + var c = this._target; + + if (isTouchDevice) { + c.addEventListener('touchstart', this._eventHandlers.mousedown); + c.addEventListener('touchend', this._eventHandlers.mouseup); + c.addEventListener('touchmove', this._eventHandlers.mousemove); + } + c.addEventListener('mousedown', this._eventHandlers.mousedown); + c.addEventListener('mouseup', this._eventHandlers.mouseup); + c.addEventListener('mousemove', this._eventHandlers.mousemove); + c.addEventListener('wheel', this._eventHandlers.mousewheel); + + /* Prevent middle-click pasting (see above for why we bind to document) */ + document.addEventListener('click', this._eventHandlers.mousedisable); + + /* preventDefault() on mousedown doesn't stop this event for some + reason so we have to explicitly block it */ + c.addEventListener('contextmenu', this._eventHandlers.mousedisable); + }, + + ungrab: function () { + var c = this._target; + + if (isTouchDevice) { + c.removeEventListener('touchstart', this._eventHandlers.mousedown); + c.removeEventListener('touchend', this._eventHandlers.mouseup); + c.removeEventListener('touchmove', this._eventHandlers.mousemove); + } + c.removeEventListener('mousedown', this._eventHandlers.mousedown); + c.removeEventListener('mouseup', this._eventHandlers.mouseup); + c.removeEventListener('mousemove', this._eventHandlers.mousemove); + c.removeEventListener('wheel', this._eventHandlers.mousewheel); + + document.removeEventListener('click', this._eventHandlers.mousedisable); + + c.removeEventListener('contextmenu', this._eventHandlers.mousedisable); + } +}; + +make_properties(Mouse, [ + ['target', 'ro', 'dom'], // DOM element that captures mouse input + ['focused', 'rw', 'bool'], // Capture and send mouse clicks/movement + + ['onMouseButton', 'rw', 'func'], // Handler for mouse button click/release + ['onMouseMove', 'rw', 'func'], // Handler for mouse movement + ['touchButton', 'rw', 'int'] // Button mask (1, 2, 4) for touch devices (0 means ignore clicks) +]); diff --git a/core/rfb.js b/core/rfb.js index ece0f008..3dc7cbca 100644 --- a/core/rfb.js +++ b/core/rfb.js @@ -15,7 +15,8 @@ import _ from './util/localization.js'; import { decodeUTF8 } from './util/strings.js'; import { set_defaults, make_properties } from './util/properties.js'; import Display from "./display.js"; -import { Keyboard, Mouse } from "./input/devices.js"; +import Keyboard from "./input/keyboard.js"; +import Mouse from "./input/mouse.js"; import Websock from "./websock.js"; import Base64 from "./base64.js"; import DES from "./des.js"; diff --git a/tests/input.html b/tests/input.html index 4925a3a8..56261785 100644 --- a/tests/input.html +++ b/tests/input.html @@ -31,7 +31,8 @@ - + +