]> git.proxmox.com Git - mirror_novnc.git/blame - core/input/util.js
Use standard DOM identifiers for physical keys
[mirror_novnc.git] / core / input / util.js
CommitLineData
3ae0bb09
SR
1import KeyTable from "./keysym.js";
2import keysyms from "./keysymdef.js";
80cb8ffd 3import vkeys from "./vkeys.js";
3ae0bb09 4
6d6f0db0
SR
5function isMac() {
6 return navigator && !!(/mac/i).exec(navigator.platform);
7}
8function isWindows() {
9 return navigator && !!(/win/i).exec(navigator.platform);
10}
11function isLinux() {
12 return navigator && !!(/linux/i).exec(navigator.platform);
13}
14
15// Return true if a modifier which is not the specified char modifier (and is not shift) is down
16export function hasShortcutModifier(charModifier, currentModifiers) {
17 var mods = {};
18 for (var key in currentModifiers) {
19 if (parseInt(key) !== KeyTable.XK_Shift_L) {
20 mods[key] = currentModifiers[key];
21 }
31f169e8 22 }
466a09f0 23
6d6f0db0
SR
24 var sum = 0;
25 for (var k in currentModifiers) {
26 if (mods[k]) {
27 ++sum;
28 }
4ef7566b 29 }
6d6f0db0
SR
30 if (hasCharModifier(charModifier, mods)) {
31 return sum > charModifier.length;
4ef7566b 32 }
6d6f0db0
SR
33 else {
34 return sum > 0;
4ef7566b 35 }
6d6f0db0 36}
4ef7566b 37
6d6f0db0
SR
38// Return true if the specified char modifier is currently down
39export function hasCharModifier(charModifier, currentModifiers) {
40 if (charModifier.length === 0) { return false; }
41
42 for (var i = 0; i < charModifier.length; ++i) {
43 if (!currentModifiers[charModifier[i]]) {
44 return false;
4ef7566b 45 }
6d6f0db0
SR
46 }
47 return true;
48}
4ef7566b 49
6d6f0db0
SR
50// Helper object tracking modifier key state
51// and generates fake key events to compensate if it gets out of sync
52export function ModifierSync(charModifier) {
53 if (!charModifier) {
54 if (isMac()) {
55 // on Mac, Option (AKA Alt) is used as a char modifier
56 charModifier = [KeyTable.XK_Alt_L];
57 }
58 else if (isWindows()) {
59 // on Windows, Ctrl+Alt is used as a char modifier
60 charModifier = [KeyTable.XK_Alt_L, KeyTable.XK_Control_L];
4ef7566b 61 }
6d6f0db0
SR
62 else if (isLinux()) {
63 // on Linux, ISO Level 3 Shift (AltGr) is used as a char modifier
64 charModifier = [KeyTable.XK_ISO_Level3_Shift];
4ef7566b 65 }
66 else {
6d6f0db0 67 charModifier = [];
4ef7566b 68 }
69 }
70
6d6f0db0
SR
71 var state = {};
72 state[KeyTable.XK_Control_L] = false;
73 state[KeyTable.XK_Alt_L] = false;
74 state[KeyTable.XK_ISO_Level3_Shift] = false;
75 state[KeyTable.XK_Shift_L] = false;
76 state[KeyTable.XK_Meta_L] = false;
77
78 function sync(evt, keysym) {
79 var result = [];
80 function syncKey(keysym) {
524d67f2 81 return {keysym: keysym, type: state[keysym] ? 'keydown' : 'keyup'};
6d6f0db0
SR
82 }
83
84 if (evt.ctrlKey !== undefined &&
85 evt.ctrlKey !== state[KeyTable.XK_Control_L] && keysym !== KeyTable.XK_Control_L) {
86 state[KeyTable.XK_Control_L] = evt.ctrlKey;
87 result.push(syncKey(KeyTable.XK_Control_L));
88 }
89 if (evt.altKey !== undefined &&
90 evt.altKey !== state[KeyTable.XK_Alt_L] && keysym !== KeyTable.XK_Alt_L) {
91 state[KeyTable.XK_Alt_L] = evt.altKey;
92 result.push(syncKey(KeyTable.XK_Alt_L));
93 }
94 if (evt.altGraphKey !== undefined &&
95 evt.altGraphKey !== state[KeyTable.XK_ISO_Level3_Shift] && keysym !== KeyTable.XK_ISO_Level3_Shift) {
96 state[KeyTable.XK_ISO_Level3_Shift] = evt.altGraphKey;
97 result.push(syncKey(KeyTable.XK_ISO_Level3_Shift));
98 }
99 if (evt.shiftKey !== undefined &&
100 evt.shiftKey !== state[KeyTable.XK_Shift_L] && keysym !== KeyTable.XK_Shift_L) {
101 state[KeyTable.XK_Shift_L] = evt.shiftKey;
102 result.push(syncKey(KeyTable.XK_Shift_L));
103 }
104 if (evt.metaKey !== undefined &&
105 evt.metaKey !== state[KeyTable.XK_Meta_L] && keysym !== KeyTable.XK_Meta_L) {
106 state[KeyTable.XK_Meta_L] = evt.metaKey;
107 result.push(syncKey(KeyTable.XK_Meta_L));
108 }
109 return result;
4ef7566b 110 }
6d6f0db0 111 function syncKeyEvent(evt, down) {
524d67f2 112 var keysym = getKeysym(evt);
4ef7566b 113
6d6f0db0
SR
114 // first, apply the event itself, if relevant
115 if (keysym !== null && state[keysym] !== undefined) {
116 state[keysym] = down;
4ef7566b 117 }
6d6f0db0
SR
118 return sync(evt, keysym);
119 }
4ef7566b 120
6d6f0db0
SR
121 return {
122 // sync on the appropriate keyboard event
123 keydown: function(evt) { return syncKeyEvent(evt, true);},
124 keyup: function(evt) { return syncKeyEvent(evt, false);},
125 // Call this with a non-keyboard event (such as mouse events) to use its modifier state to synchronize anyway
126 syncAny: function(evt) { return sync(evt);},
127
128 // is a shortcut modifier down?
129 hasShortcutModifier: function() { return hasShortcutModifier(charModifier, state); },
130 // if a char modifier is down, return the keys it consists of, otherwise return null
131 activeCharModifier: function() { return hasCharModifier(charModifier, state) ? charModifier : null; }
132 };
133}
4ef7566b 134
80cb8ffd
PO
135// Get 'KeyboardEvent.code', handling legacy browsers
136export function getKeycode(evt){
137 // Are we getting proper key identifiers?
138 // (unfortunately Firefox and Chrome are crappy here and gives
139 // us an empty string on some platforms, rather than leaving it
140 // undefined)
141 if (evt.code) {
142 // Mozilla isn't fully in sync with the spec yet
143 switch (evt.code) {
144 case 'OSLeft': return 'MetaLeft';
145 case 'OSRight': return 'MetaRight';
146 }
147
148 return evt.code;
6d6f0db0 149 }
80cb8ffd
PO
150
151 // The de-facto standard is to use Windows Virtual-Key codes
152 // in the 'keyCode' field for non-printable characters. However
153 // Webkit sets it to the same as charCode in 'keypress' events.
154 if ((evt.type !== 'keypress') && (evt.keyCode in vkeys)) {
155 var code = vkeys[evt.keyCode];
156
157 // macOS has messed up this code for some reason
158 if (isMac() && (code === 'ContextMenu')) {
159 code = 'MetaRight';
160 }
161
162 // The keyCode doesn't distinguish between left and right
163 // for the standard modifiers
164 if (evt.location === 2) {
165 switch (code) {
166 case 'ShiftLeft': return 'ShiftRight';
167 case 'ControlLeft': return 'ControlRight';
168 case 'AltLeft': return 'AltRight';
169 }
170 }
171
172 // Nor a bunch of the numpad keys
173 if (evt.location === 3) {
174 switch (code) {
175 case 'Delete': return 'NumpadDecimal';
176 case 'Insert': return 'Numpad0';
177 case 'End': return 'Numpad1';
178 case 'ArrowDown': return 'Numpad2';
179 case 'PageDown': return 'Numpad3';
180 case 'ArrowLeft': return 'Numpad4';
181 case 'ArrowRight': return 'Numpad6';
182 case 'Home': return 'Numpad7';
183 case 'ArrowUp': return 'Numpad8';
184 case 'PageUp': return 'Numpad9';
185 case 'Enter': return 'NumpadEnter';
186 }
187 }
188
189 return code;
6d6f0db0 190 }
80cb8ffd
PO
191
192 return 'Unidentified';
6d6f0db0
SR
193}
194
195// Get the most reliable keysym value we can get from a key event
196// if char/charCode is available, prefer those, otherwise fall back to key/keyCode/which
197export function getKeysym(evt){
198 var codepoint;
199 if (evt.char && evt.char.length === 1) {
200 codepoint = evt.char.charCodeAt();
201 }
202 else if (evt.charCode) {
203 codepoint = evt.charCode;
4ef7566b 204 }
6d6f0db0
SR
205 else if (evt.keyCode && evt.type === 'keypress') {
206 // IE10 stores the char code as keyCode, and has no other useful properties
207 codepoint = evt.keyCode;
208 }
209 if (codepoint) {
0a865e15 210 return keysyms.lookup(codepoint);
6d6f0db0
SR
211 }
212 // we could check evt.key here.
213 // Legal values are defined in http://www.w3.org/TR/DOM-Level-3-Events/#key-values-list,
214 // so we "just" need to map them to keysym, but AFAIK this is only available in IE10, which also provides evt.key
215 // so we don't *need* it yet
216 if (evt.keyCode) {
524d67f2 217 return keysymFromKeyCode(evt.keyCode, evt.shiftKey);
6d6f0db0
SR
218 }
219 if (evt.which) {
524d67f2 220 return keysymFromKeyCode(evt.which, evt.shiftKey);
6d6f0db0
SR
221 }
222 return null;
223}
4ef7566b 224
6d6f0db0
SR
225// Given a keycode, try to predict which keysym it might be.
226// If the keycode is unknown, null is returned.
a5c8a755 227function keysymFromKeyCode(keycode, shiftPressed) {
6d6f0db0 228 if (typeof(keycode) !== 'number') {
4ef7566b 229 return null;
230 }
6d6f0db0
SR
231 // won't be accurate for azerty
232 if (keycode >= 0x30 && keycode <= 0x39) {
233 return keycode; // digit
234 }
235 if (keycode >= 0x41 && keycode <= 0x5a) {
236 // remap to lowercase unless shift is down
237 return shiftPressed ? keycode : keycode + 32; // A-Z
238 }
239 if (keycode >= 0x60 && keycode <= 0x69) {
240 return KeyTable.XK_KP_0 + (keycode - 0x60); // numpad 0-9
241 }
4ef7566b 242
6d6f0db0
SR
243 switch(keycode) {
244 case 0x20: return KeyTable.XK_space;
245 case 0x6a: return KeyTable.XK_KP_Multiply;
246 case 0x6b: return KeyTable.XK_KP_Add;
247 case 0x6c: return KeyTable.XK_KP_Separator;
248 case 0x6d: return KeyTable.XK_KP_Subtract;
249 case 0x6e: return KeyTable.XK_KP_Decimal;
250 case 0x6f: return KeyTable.XK_KP_Divide;
251 case 0xbb: return KeyTable.XK_plus;
252 case 0xbc: return KeyTable.XK_comma;
253 case 0xbd: return KeyTable.XK_minus;
254 case 0xbe: return KeyTable.XK_period;
4ef7566b 255 }
256
6d6f0db0
SR
257 return nonCharacterKey({keyCode: keycode});
258}
4ef7566b 259
6d6f0db0
SR
260// if the key is a known non-character key (any key which doesn't generate character data)
261// return its keysym value. Otherwise return null
a5c8a755 262function nonCharacterKey(evt) {
6d6f0db0
SR
263 // evt.key not implemented yet
264 if (!evt.keyCode) { return null; }
265 var keycode = evt.keyCode;
4ef7566b 266
6d6f0db0
SR
267 if (keycode >= 0x70 && keycode <= 0x87) {
268 return KeyTable.XK_F1 + keycode - 0x70; // F1-F24
269 }
270 switch (keycode) {
282834ca 271
6d6f0db0
SR
272 case 8 : return KeyTable.XK_BackSpace;
273 case 13 : return KeyTable.XK_Return;
4ef7566b 274
6d6f0db0 275 case 9 : return KeyTable.XK_Tab;
4ef7566b 276
6d6f0db0
SR
277 case 27 : return KeyTable.XK_Escape;
278 case 46 : return KeyTable.XK_Delete;
4ef7566b 279
6d6f0db0
SR
280 case 36 : return KeyTable.XK_Home;
281 case 35 : return KeyTable.XK_End;
282 case 33 : return KeyTable.XK_Page_Up;
283 case 34 : return KeyTable.XK_Page_Down;
284 case 45 : return KeyTable.XK_Insert;
4ef7566b 285
6d6f0db0
SR
286 case 37 : return KeyTable.XK_Left;
287 case 38 : return KeyTable.XK_Up;
288 case 39 : return KeyTable.XK_Right;
289 case 40 : return KeyTable.XK_Down;
4ef7566b 290
6d6f0db0
SR
291 case 16 : return KeyTable.XK_Shift_L;
292 case 17 : return KeyTable.XK_Control_L;
293 case 18 : return KeyTable.XK_Alt_L; // also: Option-key on Mac
294
295 case 224 : return KeyTable.XK_Meta_L;
296 case 225 : return KeyTable.XK_ISO_Level3_Shift; // AltGr
297 case 91 : return KeyTable.XK_Super_L; // also: Windows-key
298 case 92 : return KeyTable.XK_Super_R; // also: Windows-key
299 case 93 : return KeyTable.XK_Menu; // also: Windows-Menu, Command on Mac
300 default: return null;
4ef7566b 301 }
6d6f0db0 302}
ae510306 303
6d6f0db0 304export function QEMUKeyEventDecoder (modifierState, next) {
49637e43
DHB
305 "use strict";
306
307 function sendAll(evts) {
308 for (var i = 0; i < evts.length; ++i) {
309 next(evts[i]);
310 }
311 }
312
313 var numPadCodes = ["Numpad0", "Numpad1", "Numpad2",
314 "Numpad3", "Numpad4", "Numpad5", "Numpad6",
315 "Numpad7", "Numpad8", "Numpad9", "NumpadDecimal"];
316
317 var numLockOnKeySyms = {
318 "Numpad0": 0xffb0, "Numpad1": 0xffb1, "Numpad2": 0xffb2,
319 "Numpad3": 0xffb3, "Numpad4": 0xffb4, "Numpad5": 0xffb5,
320 "Numpad6": 0xffb6, "Numpad7": 0xffb7, "Numpad8": 0xffb8,
321 "Numpad9": 0xffb9, "NumpadDecimal": 0xffac
322 };
323
324 var numLockOnKeyCodes = [96, 97, 98, 99, 100, 101, 102,
325 103, 104, 105, 108, 110];
326
327 function isNumPadMultiKey(evt) {
328 return (numPadCodes.indexOf(evt.code) !== -1);
329 }
330
331 function getNumPadKeySym(evt) {
332 if (numLockOnKeyCodes.indexOf(evt.keyCode) !== -1) {
333 return numLockOnKeySyms[evt.code];
334 }
335 return 0;
336 }
337
338 function process(evt, type) {
339 var result = {type: type};
80cb8ffd 340 result.code = getKeycode(evt);
49637e43
DHB
341 result.keysym = 0;
342
343 if (isNumPadMultiKey(evt)) {
344 result.keysym = getNumPadKeySym(evt);
345 }
346
347 var hasModifier = modifierState.hasShortcutModifier() || !!modifierState.activeCharModifier();
80cb8ffd 348 var isShift = result.code === 'ShiftLeft' || result.code === 'ShiftRight';
49637e43 349
6d6f0db0 350 var suppress = !isShift && (type !== 'keydown' || modifierState.hasShortcutModifier() || !!nonCharacterKey(evt));
49637e43
DHB
351
352 next(result);
353 return suppress;
354 }
355 return {
356 keydown: function(evt) {
357 sendAll(modifierState.keydown(evt));
358 return process(evt, 'keydown');
359 },
360 keypress: function(evt) {
361 return true;
362 },
363 keyup: function(evt) {
364 sendAll(modifierState.keyup(evt));
365 return process(evt, 'keyup');
366 },
367 syncModifiers: function(evt) {
368 sendAll(modifierState.syncAny(evt));
369 },
370 releaseAll: function() { next({type: 'releaseall'}); }
371 };
ae510306 372};
49637e43 373
6d6f0db0 374export function TrackQEMUKeyState (next) {
49637e43
DHB
375 "use strict";
376 var state = [];
377
378 return function (evt) {
379 var last = state.length !== 0 ? state[state.length-1] : null;
380
381 switch (evt.type) {
382 case 'keydown':
383
384 if (!last || last.code !== evt.code) {
385 last = {code: evt.code};
386
387 if (state.length > 0 && state[state.length-1].code == 'ControlLeft') {
388 if (evt.code !== 'AltRight') {
389 next({code: 'ControlLeft', type: 'keydown', keysym: 0});
390 } else {
391 state.pop();
392 }
393 }
394 state.push(last);
395 }
396 if (evt.code !== 'ControlLeft') {
397 next(evt);
398 }
399 break;
400
401 case 'keyup':
402 if (state.length === 0) {
403 return;
404 }
405 var idx = null;
406 // do we have a matching key tracked as being down?
407 for (var i = 0; i !== state.length; ++i) {
408 if (state[i].code === evt.code) {
409 idx = i;
410 break;
411 }
412 }
413 // if we couldn't find a match (it happens), assume it was the last key pressed
414 if (idx === null) {
415 if (evt.code === 'ControlLeft') {
416 return;
417 }
418 idx = state.length - 1;
419 }
420
421 state.splice(idx, 1);
422 next(evt);
423 break;
424 case 'releaseall':
425 /* jshint shadow: true */
426 for (var i = 0; i < state.length; ++i) {
427 next({code: state[i].code, keysym: 0, type: 'keyup'});
428 }
429 /* jshint shadow: false */
430 state = [];
431 }
432 };
ae510306 433};
49637e43 434
4ef7566b 435// Takes a DOM keyboard event and:
436// - determines which keysym it represents
80cb8ffd 437// - determines a code identifying the key that was pressed (corresponding to the code/keyCode properties on the DOM event)
4ef7566b 438// - synthesizes events to synchronize modifier key state between which modifiers are actually down, and which we thought were down
439// - marks each event with an 'escape' property if a modifier was down which should be "escaped"
440// - generates a "stall" event in cases where it might be necessary to wait and see if a keypress event follows a keydown
441// This information is collected into an object which is passed to the next() function. (one call per event)
6d6f0db0 442export function KeyEventDecoder (modifierState, next) {
4ef7566b 443 "use strict";
444 function sendAll(evts) {
445 for (var i = 0; i < evts.length; ++i) {
446 next(evts[i]);
447 }
448 }
449 function process(evt, type) {
450 var result = {type: type};
80cb8ffd
PO
451 var code = getKeycode(evt);
452 if (code === 'Unidentified') {
453 // Unstable, but we don't have anything else to go on
454 // (don't use it for 'keypress' events thought since
455 // WebKit sets it to the same as charCode)
456 if (evt.keyCode && (evt.type !== 'keypress')) {
457 code = 'Platform' + evt.keyCode;
458 }
4ef7566b 459 }
80cb8ffd 460 result.code = code;
4ef7566b 461
6d6f0db0 462 var keysym = getKeysym(evt);
4ef7566b 463
464 var hasModifier = modifierState.hasShortcutModifier() || !!modifierState.activeCharModifier();
465 // Is this a case where we have to decide on the keysym right away, rather than waiting for the keypress?
466 // "special" keys like enter, tab or backspace don't send keypress events,
467 // and some browsers don't send keypresses at all if a modifier is down
6d6f0db0 468 if (keysym && (type !== 'keydown' || nonCharacterKey(evt) || hasModifier)) {
4ef7566b 469 result.keysym = keysym;
470 }
471
80cb8ffd 472 var isShift = code === 'ShiftLeft' || code === 'ShiftRight';
4ef7566b 473
474 // Should we prevent the browser from handling the event?
475 // Doing so on a keydown (in most browsers) prevents keypress from being generated
476 // so only do that if we have to.
6d6f0db0 477 var suppress = !isShift && (type !== 'keydown' || modifierState.hasShortcutModifier() || !!nonCharacterKey(evt));
4ef7566b 478
479 // If a char modifier is down on a keydown, we need to insert a stall,
480 // so VerifyCharModifier knows to wait and see if a keypress is comnig
6d6f0db0 481 var stall = type === 'keydown' && modifierState.activeCharModifier() && !nonCharacterKey(evt);
4ef7566b 482
483 // if a char modifier is pressed, get the keys it consists of (on Windows, AltGr is equivalent to Ctrl+Alt)
484 var active = modifierState.activeCharModifier();
485
486 // If we have a char modifier down, and we're able to determine a keysym reliably
487 // then (a) we know to treat the modifier as a char modifier,
488 // and (b) we'll have to "escape" the modifier to undo the modifier when sending the char.
489 if (active && keysym) {
490 var isCharModifier = false;
491 for (var i = 0; i < active.length; ++i) {
524d67f2 492 if (active[i] === keysym) {
4ef7566b 493 isCharModifier = true;
494 }
495 }
496 if (type === 'keypress' && !isCharModifier) {
497 result.escape = modifierState.activeCharModifier();
498 }
499 }
500
501 if (stall) {
502 // insert a fake "stall" event
503 next({type: 'stall'});
504 }
505 next(result);
506
507 return suppress;
508 }
509
510 return {
511 keydown: function(evt) {
512 sendAll(modifierState.keydown(evt));
513 return process(evt, 'keydown');
514 },
515 keypress: function(evt) {
516 return process(evt, 'keypress');
517 },
518 keyup: function(evt) {
519 sendAll(modifierState.keyup(evt));
520 return process(evt, 'keyup');
521 },
522 syncModifiers: function(evt) {
523 sendAll(modifierState.syncAny(evt));
524 },
525 releaseAll: function() { next({type: 'releaseall'}); }
526 };
ae510306 527};
4ef7566b 528
529// Combines keydown and keypress events where necessary to handle char modifiers.
530// On some OS'es, a char modifier is sometimes used as a shortcut modifier.
531// For example, on Windows, AltGr is synonymous with Ctrl-Alt. On a Danish keyboard layout, AltGr-2 yields a @, but Ctrl-Alt-D does nothing
532// so when used with the '2' key, Ctrl-Alt counts as a char modifier (and should be escaped), but when used with 'D', it does not.
533// The only way we can distinguish these cases is to wait and see if a keypress event arrives
534// When we receive a "stall" event, wait a few ms before processing the next keydown. If a keypress has also arrived, merge the two
6d6f0db0 535export function VerifyCharModifier (next) {
4ef7566b 536 "use strict";
537 var queue = [];
538 var timer = null;
539 function process() {
540 if (timer) {
541 return;
542 }
31f169e8
SR
543
544 var delayProcess = function () {
545 clearTimeout(timer);
546 timer = null;
547 process();
548 };
549
4ef7566b 550 while (queue.length !== 0) {
551 var cur = queue[0];
552 queue = queue.splice(1);
553 switch (cur.type) {
554 case 'stall':
555 // insert a delay before processing available events.
31f169e8
SR
556 /* jshint loopfunc: true */
557 timer = setTimeout(delayProcess, 5);
558 /* jshint loopfunc: false */
4ef7566b 559 return;
560 case 'keydown':
561 // is the next element a keypress? Then we should merge the two
562 if (queue.length !== 0 && queue[0].type === 'keypress') {
563 // Firefox sends keypress even when no char is generated.
564 // so, if keypress keysym is the same as we'd have guessed from keydown,
565 // the modifier didn't have any effect, and should not be escaped
524d67f2 566 if (queue[0].escape && (!cur.keysym || cur.keysym !== queue[0].keysym)) {
4ef7566b 567 cur.escape = queue[0].escape;
568 }
569 cur.keysym = queue[0].keysym;
570 queue = queue.splice(1);
571 }
572 break;
573 }
574
575 // swallow stall events, and pass all others to the next stage
576 if (cur.type !== 'stall') {
577 next(cur);
578 }
579 }
580 }
581 return function(evt) {
582 queue.push(evt);
583 process();
584 };
ae510306 585};
4ef7566b 586
587// Keeps track of which keys we (and the server) believe are down
588// When a keyup is received, match it against this list, to determine the corresponding keysym(s)
589// in some cases, a single key may produce multiple keysyms, so the corresponding keyup event must release all of these chars
590// key repeat events should be merged into a single entry.
591// Because we can't always identify which entry a keydown or keyup event corresponds to, we sometimes have to guess
6d6f0db0 592export function TrackKeyState (next) {
4ef7566b 593 "use strict";
594 var state = [];
595
596 return function (evt) {
597 var last = state.length !== 0 ? state[state.length-1] : null;
598
599 switch (evt.type) {
600 case 'keydown':
601 // insert a new entry if last seen key was different.
80cb8ffd
PO
602 if (!last || evt.code === 'Unidentified' || last.code !== evt.code) {
603 last = {code: evt.code, keysyms: {}};
4ef7566b 604 state.push(last);
605 }
606 if (evt.keysym) {
607 // make sure last event contains this keysym (a single "logical" keyevent
608 // can cause multiple key events to be sent to the VNC server)
524d67f2 609 last.keysyms[evt.keysym] = evt.keysym;
4ef7566b 610 last.ignoreKeyPress = true;
611 next(evt);
612 }
613 break;
614 case 'keypress':
615 if (!last) {
80cb8ffd 616 last = {code: evt.code, keysyms: {}};
4ef7566b 617 state.push(last);
618 }
619 if (!evt.keysym) {
620 console.log('keypress with no keysym:', evt);
621 }
622
623 // If we didn't expect a keypress, and already sent a keydown to the VNC server
624 // based on the keydown, make sure to skip this event.
625 if (evt.keysym && !last.ignoreKeyPress) {
524d67f2 626 last.keysyms[evt.keysym] = evt.keysym;
4ef7566b 627 evt.type = 'keydown';
628 next(evt);
629 }
630 break;
631 case 'keyup':
632 if (state.length === 0) {
633 return;
634 }
635 var idx = null;
636 // do we have a matching key tracked as being down?
637 for (var i = 0; i !== state.length; ++i) {
80cb8ffd 638 if (state[i].code === evt.code) {
4ef7566b 639 idx = i;
640 break;
641 }
642 }
643 // if we couldn't find a match (it happens), assume it was the last key pressed
644 if (idx === null) {
645 idx = state.length - 1;
646 }
647
648 var item = state.splice(idx, 1)[0];
649 // for each keysym tracked by this key entry, clone the current event and override the keysym
31f169e8
SR
650 var clone = (function(){
651 function Clone(){}
652 return function (obj) { Clone.prototype=obj; return new Clone(); };
653 }());
4ef7566b 654 for (var key in item.keysyms) {
4ef7566b 655 var out = clone(evt);
656 out.keysym = item.keysyms[key];
657 next(out);
658 }
659 break;
660 case 'releaseall':
31f169e8 661 /* jshint shadow: true */
4ef7566b 662 for (var i = 0; i < state.length; ++i) {
663 for (var key in state[i].keysyms) {
664 var keysym = state[i].keysyms[key];
80cb8ffd 665 next({code: 'Unidentified', keysym: keysym, type: 'keyup'});
4ef7566b 666 }
667 }
31f169e8 668 /* jshint shadow: false */
4ef7566b 669 state = [];
670 }
671 };
ae510306 672};
4ef7566b 673
674// Handles "escaping" of modifiers: if a char modifier is used to produce a keysym (such as AltGr-2 to generate an @),
675// then the modifier must be "undone" before sending the @, and "redone" afterwards.
6d6f0db0 676export function EscapeModifiers (next) {
4ef7566b 677 "use strict";
678 return function(evt) {
679 if (evt.type !== 'keydown' || evt.escape === undefined) {
680 next(evt);
681 return;
682 }
683 // undo modifiers
684 for (var i = 0; i < evt.escape.length; ++i) {
80cb8ffd 685 next({type: 'keyup', code: 'Unidentified', keysym: evt.escape[i]});
4ef7566b 686 }
687 // send the character event
688 next(evt);
689 // redo modifiers
31f169e8 690 /* jshint shadow: true */
4ef7566b 691 for (var i = 0; i < evt.escape.length; ++i) {
80cb8ffd 692 next({type: 'keydown', code: 'Unidentified', keysym: evt.escape[i]});
4ef7566b 693 }
31f169e8 694 /* jshint shadow: false */
4ef7566b 695 };
ae510306 696};