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