]>
Commit | Line | Data |
---|---|---|
d3796c14 JM |
1 | /* |
2 | * noVNC: HTML5 VNC client | |
1d728ace | 3 | * Copyright (C) 2012 Joel Martin |
b2f1961a | 4 | * Copyright (C) 2013 Samuel Mannehed for Cendio AB |
1d728ace | 5 | * Licensed under MPL 2.0 or any later version (see LICENSE.txt) |
d3796c14 JM |
6 | */ |
7 | ||
d6e281ba | 8 | /*jslint browser: true, white: false */ |
d3796c14 JM |
9 | /*global window, Util */ |
10 | ||
6d6f0db0 SR |
11 | import * as Log from '../util/logging.js'; |
12 | import { isTouchDevice } from '../util/browsers.js' | |
13 | import { setCapture, releaseCapture, stopEvent, getPointerEvent } from '../util/events.js'; | |
14 | import { set_defaults, make_properties } from '../util/properties.js'; | |
15 | import * as KeyboardUtil from "./util.js"; | |
16 | ||
17 | // | |
18 | // Keyboard event handler | |
19 | // | |
20 | ||
21 | const Keyboard = function (defaults) { | |
22 | this._keyDownList = []; // List of depressed keys | |
23 | // (even if they are happy) | |
24 | ||
25 | set_defaults(this, defaults, { | |
26 | 'target': document, | |
27 | 'focused': true | |
28 | }); | |
29 | ||
30 | // create the keyboard handler | |
31 | this._handler = new KeyboardUtil.KeyEventDecoder(KeyboardUtil.ModifierSync(), | |
32 | KeyboardUtil.VerifyCharModifier( /* jshint newcap: false */ | |
33 | KeyboardUtil.TrackKeyState( | |
34 | KeyboardUtil.EscapeModifiers(this._handleRfbEvent.bind(this)) | |
35 | ) | |
36 | ) | |
37 | ); /* jshint newcap: true */ | |
38 | ||
39 | // keep these here so we can refer to them later | |
40 | this._eventHandlers = { | |
41 | 'keyup': this._handleKeyUp.bind(this), | |
42 | 'keydown': this._handleKeyDown.bind(this), | |
43 | 'keypress': this._handleKeyPress.bind(this), | |
44 | 'blur': this._allKeysUp.bind(this) | |
45 | }; | |
46 | }; | |
d6e281ba | 47 | |
6d6f0db0 SR |
48 | Keyboard.prototype = { |
49 | // private methods | |
d6e281ba | 50 | |
6d6f0db0 SR |
51 | _handleRfbEvent: function (e) { |
52 | if (this._onKeyPress) { | |
53 | Log.Debug("onKeyPress " + (e.type == 'keydown' ? "down" : "up") + | |
524d67f2 | 54 | ", keysym: " + e.keysym); |
6d6f0db0 SR |
55 | this._onKeyPress(e); |
56 | } | |
57 | }, | |
d6e281ba | 58 | |
6d6f0db0 SR |
59 | setQEMUVNCKeyboardHandler: function () { |
60 | this._handler = new KeyboardUtil.QEMUKeyEventDecoder(KeyboardUtil.ModifierSync(), | |
61 | KeyboardUtil.TrackQEMUKeyState( | |
62 | this._handleRfbEvent.bind(this) | |
d6e281ba | 63 | ) |
6d6f0db0 SR |
64 | ); |
65 | }, | |
66 | ||
67 | _handleKeyDown: function (e) { | |
68 | if (!this._focused) { return; } | |
69 | ||
70 | if (this._handler.keydown(e)) { | |
71 | // Suppress bubbling/default actions | |
72 | stopEvent(e); | |
73 | } else { | |
74 | // Allow the event to bubble and become a keyPress event which | |
75 | // will have the character code translated | |
76 | } | |
77 | }, | |
d6e281ba | 78 | |
6d6f0db0 SR |
79 | _handleKeyPress: function (e) { |
80 | if (!this._focused) { return; } | |
d6e281ba | 81 | |
6d6f0db0 SR |
82 | if (this._handler.keypress(e)) { |
83 | // Suppress bubbling/default actions | |
84 | stopEvent(e); | |
85 | } | |
86 | }, | |
d3796c14 | 87 | |
6d6f0db0 SR |
88 | _handleKeyUp: function (e) { |
89 | if (!this._focused) { return; } | |
c96f9003 | 90 | |
6d6f0db0 SR |
91 | if (this._handler.keyup(e)) { |
92 | // Suppress bubbling/default actions | |
93 | stopEvent(e); | |
94 | } | |
95 | }, | |
d3796c14 | 96 | |
6d6f0db0 SR |
97 | _allKeysUp: function () { |
98 | Log.Debug(">> Keyboard.allKeysUp"); | |
99 | this._handler.releaseAll(); | |
100 | Log.Debug("<< Keyboard.allKeysUp"); | |
101 | }, | |
d6e281ba | 102 | |
6d6f0db0 | 103 | // Public methods |
d6e281ba | 104 | |
6d6f0db0 SR |
105 | grab: function () { |
106 | //Log.Debug(">> Keyboard.grab"); | |
107 | var c = this._target; | |
d6e281ba | 108 | |
6d6f0db0 SR |
109 | c.addEventListener('keydown', this._eventHandlers.keydown); |
110 | c.addEventListener('keyup', this._eventHandlers.keyup); | |
111 | c.addEventListener('keypress', this._eventHandlers.keypress); | |
d6e281ba | 112 | |
6d6f0db0 SR |
113 | // Release (key up) if window loses focus |
114 | window.addEventListener('blur', this._eventHandlers.blur); | |
d6e281ba | 115 | |
6d6f0db0 SR |
116 | //Log.Debug("<< Keyboard.grab"); |
117 | }, | |
d6e281ba | 118 | |
6d6f0db0 SR |
119 | ungrab: function () { |
120 | //Log.Debug(">> Keyboard.ungrab"); | |
121 | var c = this._target; | |
d6e281ba | 122 | |
6d6f0db0 SR |
123 | c.removeEventListener('keydown', this._eventHandlers.keydown); |
124 | c.removeEventListener('keyup', this._eventHandlers.keyup); | |
125 | c.removeEventListener('keypress', this._eventHandlers.keypress); | |
126 | window.removeEventListener('blur', this._eventHandlers.blur); | |
d6e281ba | 127 | |
6d6f0db0 SR |
128 | // Release (key up) all keys that are in a down state |
129 | this._allKeysUp(); | |
d3796c14 | 130 | |
6d6f0db0 SR |
131 | //Log.Debug(">> Keyboard.ungrab"); |
132 | }, | |
d3796c14 | 133 | |
6d6f0db0 SR |
134 | sync: function (e) { |
135 | this._handler.syncModifiers(e); | |
136 | } | |
137 | }; | |
d6e281ba | 138 | |
6d6f0db0 SR |
139 | make_properties(Keyboard, [ |
140 | ['target', 'wo', 'dom'], // DOM element that captures keyboard input | |
141 | ['focused', 'rw', 'bool'], // Capture and send key events | |
d6e281ba | 142 | |
6d6f0db0 SR |
143 | ['onKeyPress', 'rw', 'func'] // Handler for key press/release |
144 | ]); | |
d6e281ba | 145 | |
6d6f0db0 SR |
146 | const Mouse = function (defaults) { |
147 | this._mouseCaptured = false; | |
d3796c14 | 148 | |
6d6f0db0 SR |
149 | this._doubleClickTimer = null; |
150 | this._lastTouchPos = null; | |
d6e281ba | 151 | |
6d6f0db0 SR |
152 | // Configuration attributes |
153 | set_defaults(this, defaults, { | |
154 | 'target': document, | |
155 | 'focused': true, | |
156 | 'touchButton': 1 | |
157 | }); | |
d6e281ba | 158 | |
6d6f0db0 SR |
159 | this._eventHandlers = { |
160 | 'mousedown': this._handleMouseDown.bind(this), | |
161 | 'mouseup': this._handleMouseUp.bind(this), | |
162 | 'mousemove': this._handleMouseMove.bind(this), | |
163 | 'mousewheel': this._handleMouseWheel.bind(this), | |
164 | 'mousedisable': this._handleMouseDisable.bind(this) | |
d6e281ba | 165 | }; |
6d6f0db0 | 166 | }; |
d6e281ba | 167 | |
6d6f0db0 SR |
168 | Mouse.prototype = { |
169 | // private methods | |
170 | _captureMouse: function () { | |
171 | // capturing the mouse ensures we get the mouseup event | |
172 | setCapture(this._target); | |
d6e281ba | 173 | |
6d6f0db0 SR |
174 | // some browsers give us mouseup events regardless, |
175 | // so if we never captured the mouse, we can disregard the event | |
176 | this._mouseCaptured = true; | |
177 | }, | |
d6e281ba | 178 | |
6d6f0db0 SR |
179 | _releaseMouse: function () { |
180 | releaseCapture(); | |
181 | this._mouseCaptured = false; | |
182 | }, | |
d6e281ba | 183 | |
6d6f0db0 SR |
184 | _resetDoubleClickTimer: function () { |
185 | this._doubleClickTimer = null; | |
186 | }, | |
b2f1961a | 187 | |
6d6f0db0 SR |
188 | _handleMouseButton: function (e, down) { |
189 | if (!this._focused) { return; } | |
cf19ad37 | 190 | |
6d6f0db0 SR |
191 | if (this._notify) { |
192 | this._notify(e); | |
193 | } | |
b2f1961a | 194 | |
6d6f0db0 SR |
195 | var pos = this._getMousePosition(e); |
196 | ||
197 | var bmask; | |
198 | if (e.touches || e.changedTouches) { | |
199 | // Touch device | |
200 | ||
201 | // When two touches occur within 500 ms of each other and are | |
202 | // close enough together a double click is triggered. | |
203 | if (down == 1) { | |
204 | if (this._doubleClickTimer === null) { | |
205 | this._lastTouchPos = pos; | |
206 | } else { | |
207 | clearTimeout(this._doubleClickTimer); | |
208 | ||
209 | // When the distance between the two touches is small enough | |
210 | // force the position of the latter touch to the position of | |
211 | // the first. | |
212 | ||
213 | var xs = this._lastTouchPos.x - pos.x; | |
214 | var ys = this._lastTouchPos.y - pos.y; | |
215 | var d = Math.sqrt((xs * xs) + (ys * ys)); | |
216 | ||
217 | // The goal is to trigger on a certain physical width, the | |
218 | // devicePixelRatio brings us a bit closer but is not optimal. | |
219 | var threshold = 20 * (window.devicePixelRatio || 1); | |
220 | if (d < threshold) { | |
221 | pos = this._lastTouchPos; | |
d6e281ba | 222 | } |
a4ec2f5c | 223 | } |
6d6f0db0 | 224 | this._doubleClickTimer = setTimeout(this._resetDoubleClickTimer.bind(this), 500); |
b2f1961a | 225 | } |
6d6f0db0 SR |
226 | bmask = this._touchButton; |
227 | // If bmask is set | |
228 | } else if (e.which) { | |
229 | /* everything except IE */ | |
230 | bmask = 1 << e.button; | |
231 | } else { | |
232 | /* IE including 9 */ | |
233 | bmask = (e.button & 0x1) + // Left | |
234 | (e.button & 0x2) * 2 + // Right | |
235 | (e.button & 0x4) / 2; // Middle | |
236 | } | |
d6e281ba | 237 | |
6d6f0db0 SR |
238 | if (this._onMouseButton) { |
239 | Log.Debug("onMouseButton " + (down ? "down" : "up") + | |
240 | ", x: " + pos.x + ", y: " + pos.y + ", bmask: " + bmask); | |
241 | this._onMouseButton(pos.x, pos.y, down, bmask); | |
242 | } | |
243 | stopEvent(e); | |
244 | }, | |
d6e281ba | 245 | |
6d6f0db0 SR |
246 | _handleMouseDown: function (e) { |
247 | this._captureMouse(); | |
248 | this._handleMouseButton(e, 1); | |
249 | }, | |
d6e281ba | 250 | |
6d6f0db0 SR |
251 | _handleMouseUp: function (e) { |
252 | if (!this._mouseCaptured) { return; } | |
d6e281ba | 253 | |
6d6f0db0 SR |
254 | this._handleMouseButton(e, 0); |
255 | this._releaseMouse(); | |
256 | }, | |
d6e281ba | 257 | |
6d6f0db0 SR |
258 | _handleMouseWheel: function (e) { |
259 | if (!this._focused) { return; } | |
d6e281ba | 260 | |
6d6f0db0 SR |
261 | if (this._notify) { |
262 | this._notify(e); | |
263 | } | |
d6e281ba | 264 | |
6d6f0db0 | 265 | var pos = this._getMousePosition(e); |
ebb9086a | 266 | |
6d6f0db0 SR |
267 | if (this._onMouseButton) { |
268 | if (e.deltaX < 0) { | |
269 | this._onMouseButton(pos.x, pos.y, 1, 1 << 5); | |
270 | this._onMouseButton(pos.x, pos.y, 0, 1 << 5); | |
271 | } else if (e.deltaX > 0) { | |
272 | this._onMouseButton(pos.x, pos.y, 1, 1 << 6); | |
273 | this._onMouseButton(pos.x, pos.y, 0, 1 << 6); | |
d6e281ba | 274 | } |
ebb9086a | 275 | |
6d6f0db0 SR |
276 | if (e.deltaY < 0) { |
277 | this._onMouseButton(pos.x, pos.y, 1, 1 << 3); | |
278 | this._onMouseButton(pos.x, pos.y, 0, 1 << 3); | |
279 | } else if (e.deltaY > 0) { | |
280 | this._onMouseButton(pos.x, pos.y, 1, 1 << 4); | |
281 | this._onMouseButton(pos.x, pos.y, 0, 1 << 4); | |
d6e281ba | 282 | } |
6d6f0db0 | 283 | } |
d6e281ba | 284 | |
6d6f0db0 SR |
285 | stopEvent(e); |
286 | }, | |
af1b2ae1 | 287 | |
6d6f0db0 SR |
288 | _handleMouseMove: function (e) { |
289 | if (! this._focused) { return; } | |
d6e281ba | 290 | |
6d6f0db0 SR |
291 | if (this._notify) { |
292 | this._notify(e); | |
293 | } | |
d6e281ba | 294 | |
6d6f0db0 SR |
295 | var pos = this._getMousePosition(e); |
296 | if (this._onMouseMove) { | |
297 | this._onMouseMove(pos.x, pos.y); | |
298 | } | |
299 | stopEvent(e); | |
300 | }, | |
301 | ||
302 | _handleMouseDisable: function (e) { | |
303 | if (!this._focused) { return; } | |
304 | ||
305 | /* | |
306 | * Stop propagation if inside canvas area | |
307 | * Note: This is only needed for the 'click' event as it fails | |
308 | * to fire properly for the target element so we have | |
309 | * to listen on the document element instead. | |
310 | */ | |
311 | if (e.target == this._target) { | |
312 | stopEvent(e); | |
313 | } | |
314 | }, | |
315 | ||
316 | // Return coordinates relative to target | |
317 | _getMousePosition: function(e) { | |
318 | e = getPointerEvent(e); | |
319 | var bounds = this._target.getBoundingClientRect(); | |
320 | var x, y; | |
321 | // Clip to target bounds | |
322 | if (e.clientX < bounds.left) { | |
323 | x = 0; | |
324 | } else if (e.clientX >= bounds.right) { | |
325 | x = bounds.width - 1; | |
326 | } else { | |
327 | x = e.clientX - bounds.left; | |
328 | } | |
329 | if (e.clientY < bounds.top) { | |
330 | y = 0; | |
331 | } else if (e.clientY >= bounds.bottom) { | |
332 | y = bounds.height - 1; | |
333 | } else { | |
334 | y = e.clientY - bounds.top; | |
335 | } | |
336 | return {x:x, y:y}; | |
337 | }, | |
338 | ||
339 | // Public methods | |
340 | grab: function () { | |
341 | var c = this._target; | |
342 | ||
343 | if (isTouchDevice) { | |
344 | c.addEventListener('touchstart', this._eventHandlers.mousedown); | |
345 | window.addEventListener('touchend', this._eventHandlers.mouseup); | |
346 | c.addEventListener('touchend', this._eventHandlers.mouseup); | |
347 | c.addEventListener('touchmove', this._eventHandlers.mousemove); | |
348 | } | |
349 | c.addEventListener('mousedown', this._eventHandlers.mousedown); | |
350 | window.addEventListener('mouseup', this._eventHandlers.mouseup); | |
351 | c.addEventListener('mouseup', this._eventHandlers.mouseup); | |
352 | c.addEventListener('mousemove', this._eventHandlers.mousemove); | |
353 | c.addEventListener('wheel', this._eventHandlers.mousewheel); | |
354 | ||
355 | /* Prevent middle-click pasting (see above for why we bind to document) */ | |
356 | document.addEventListener('click', this._eventHandlers.mousedisable); | |
357 | ||
358 | /* preventDefault() on mousedown doesn't stop this event for some | |
359 | reason so we have to explicitly block it */ | |
360 | c.addEventListener('contextmenu', this._eventHandlers.mousedisable); | |
361 | }, | |
362 | ||
363 | ungrab: function () { | |
364 | var c = this._target; | |
365 | ||
366 | if (isTouchDevice) { | |
367 | c.removeEventListener('touchstart', this._eventHandlers.mousedown); | |
368 | window.removeEventListener('touchend', this._eventHandlers.mouseup); | |
369 | c.removeEventListener('touchend', this._eventHandlers.mouseup); | |
370 | c.removeEventListener('touchmove', this._eventHandlers.mousemove); | |
371 | } | |
372 | c.removeEventListener('mousedown', this._eventHandlers.mousedown); | |
373 | window.removeEventListener('mouseup', this._eventHandlers.mouseup); | |
374 | c.removeEventListener('mouseup', this._eventHandlers.mouseup); | |
375 | c.removeEventListener('mousemove', this._eventHandlers.mousemove); | |
376 | c.removeEventListener('wheel', this._eventHandlers.mousewheel); | |
d6e281ba | 377 | |
6d6f0db0 | 378 | document.removeEventListener('click', this._eventHandlers.mousedisable); |
d6e281ba | 379 | |
6d6f0db0 SR |
380 | c.removeEventListener('contextmenu', this._eventHandlers.mousedisable); |
381 | } | |
382 | }; | |
383 | ||
384 | make_properties(Mouse, [ | |
385 | ['target', 'ro', 'dom'], // DOM element that captures mouse input | |
386 | ['notify', 'ro', 'func'], // Function to call to notify whenever a mouse event is received | |
387 | ['focused', 'rw', 'bool'], // Capture and send mouse clicks/movement | |
d6e281ba | 388 | |
6d6f0db0 SR |
389 | ['onMouseButton', 'rw', 'func'], // Handler for mouse button click/release |
390 | ['onMouseMove', 'rw', 'func'], // Handler for mouse movement | |
391 | ['touchButton', 'rw', 'int'] // Button mask (1, 2, 4) for touch devices (0 means ignore clicks) | |
392 | ]); | |
d6e281ba | 393 | |
6d6f0db0 | 394 | export { Keyboard, Mouse }; |