]>
git.proxmox.com Git - mirror_novnc.git/blob - core/input/util.js
52b31283f9a0d728fbcfa3e5e264a5e7fef0cb89
2 * import KeyTable from "./keysym";
3 * import keysyms from "./keysymdef";
11 function substituteCodepoint(cp
) {
12 // Any Unicode code points which do not have corresponding keysym entries
13 // can be swapped out for another code point by adding them to this table
15 // {S,s} with comma below -> {S,s} with cedilla
18 // {T,t} with comma below -> {T,t} with cedilla
23 var sub
= substitutions
[cp
];
24 return sub
? sub
: cp
;
28 return navigator
&& !!(/mac/i).exec(navigator
.platform
);
30 function isWindows() {
31 return navigator
&& !!(/win/i).exec(navigator
.platform
);
34 return navigator
&& !!(/linux/i).exec(navigator
.platform
);
37 // Return true if a modifier which is not the specified char modifier (and is not shift) is down
38 function hasShortcutModifier(charModifier
, currentModifiers
) {
40 for (var key
in currentModifiers
) {
41 if (parseInt(key
) !== KeyTable
.XK_Shift_L
) {
42 mods
[key
] = currentModifiers
[key
];
47 for (var k
in currentModifiers
) {
52 if (hasCharModifier(charModifier
, mods
)) {
53 return sum
> charModifier
.length
;
60 // Return true if the specified char modifier is currently down
61 function hasCharModifier(charModifier
, currentModifiers
) {
62 if (charModifier
.length
=== 0) { return false; }
64 for (var i
= 0; i
< charModifier
.length
; ++i
) {
65 if (!currentModifiers
[charModifier
[i
]]) {
72 // Helper object tracking modifier key state
73 // and generates fake key events to compensate if it gets out of sync
74 function ModifierSync(charModifier
) {
77 // on Mac, Option (AKA Alt) is used as a char modifier
78 charModifier
= [KeyTable
.XK_Alt_L
];
80 else if (isWindows()) {
81 // on Windows, Ctrl+Alt is used as a char modifier
82 charModifier
= [KeyTable
.XK_Alt_L
, KeyTable
.XK_Control_L
];
85 // on Linux, ISO Level 3 Shift (AltGr) is used as a char modifier
86 charModifier
= [KeyTable
.XK_ISO_Level3_Shift
];
94 state
[KeyTable
.XK_Control_L
] = false;
95 state
[KeyTable
.XK_Alt_L
] = false;
96 state
[KeyTable
.XK_ISO_Level3_Shift
] = false;
97 state
[KeyTable
.XK_Shift_L
] = false;
98 state
[KeyTable
.XK_Meta_L
] = false;
100 function sync(evt
, keysym
) {
102 function syncKey(keysym
) {
103 return {keysym
: keysyms
.lookup(keysym
), type
: state
[keysym
] ? 'keydown' : 'keyup'};
106 if (evt
.ctrlKey
!== undefined &&
107 evt
.ctrlKey
!== state
[KeyTable
.XK_Control_L
] && keysym
!== KeyTable
.XK_Control_L
) {
108 state
[KeyTable
.XK_Control_L
] = evt
.ctrlKey
;
109 result
.push(syncKey(KeyTable
.XK_Control_L
));
111 if (evt
.altKey
!== undefined &&
112 evt
.altKey
!== state
[KeyTable
.XK_Alt_L
] && keysym
!== KeyTable
.XK_Alt_L
) {
113 state
[KeyTable
.XK_Alt_L
] = evt
.altKey
;
114 result
.push(syncKey(KeyTable
.XK_Alt_L
));
116 if (evt
.altGraphKey
!== undefined &&
117 evt
.altGraphKey
!== state
[KeyTable
.XK_ISO_Level3_Shift
] && keysym
!== KeyTable
.XK_ISO_Level3_Shift
) {
118 state
[KeyTable
.XK_ISO_Level3_Shift
] = evt
.altGraphKey
;
119 result
.push(syncKey(KeyTable
.XK_ISO_Level3_Shift
));
121 if (evt
.shiftKey
!== undefined &&
122 evt
.shiftKey
!== state
[KeyTable
.XK_Shift_L
] && keysym
!== KeyTable
.XK_Shift_L
) {
123 state
[KeyTable
.XK_Shift_L
] = evt
.shiftKey
;
124 result
.push(syncKey(KeyTable
.XK_Shift_L
));
126 if (evt
.metaKey
!== undefined &&
127 evt
.metaKey
!== state
[KeyTable
.XK_Meta_L
] && keysym
!== KeyTable
.XK_Meta_L
) {
128 state
[KeyTable
.XK_Meta_L
] = evt
.metaKey
;
129 result
.push(syncKey(KeyTable
.XK_Meta_L
));
133 function syncKeyEvent(evt
, down
) {
134 var obj
= getKeysym(evt
);
135 var keysym
= obj
? obj
.keysym
: null;
137 // first, apply the event itself, if relevant
138 if (keysym
!== null && state
[keysym
] !== undefined) {
139 state
[keysym
] = down
;
141 return sync(evt
, keysym
);
145 // sync on the appropriate keyboard event
146 keydown: function(evt
) { return syncKeyEvent(evt
, true);},
147 keyup: function(evt
) { return syncKeyEvent(evt
, false);},
148 // Call this with a non-keyboard event (such as mouse events) to use its modifier state to synchronize anyway
149 syncAny: function(evt
) { return sync(evt
);},
151 // is a shortcut modifier down?
152 hasShortcutModifier: function() { return hasShortcutModifier(charModifier
, state
); },
153 // if a char modifier is down, return the keys it consists of, otherwise return null
154 activeCharModifier: function() { return hasCharModifier(charModifier
, state
) ? charModifier
: null; }
158 // Get a key ID from a keyboard event
159 // May be a string or an integer depending on the available properties
160 function getKey(evt
){
161 if ('keyCode' in evt
&& 'key' in evt
) {
162 return evt
.key
+ ':' + evt
.keyCode
;
164 else if ('keyCode' in evt
) {
172 // Get the most reliable keysym value we can get from a key event
173 // if char/charCode is available, prefer those, otherwise fall back to key/keyCode/which
174 function getKeysym(evt
){
176 if (evt
.char && evt
.char.length
=== 1) {
177 codepoint
= evt
.char.charCodeAt();
179 else if (evt
.charCode
) {
180 codepoint
= evt
.charCode
;
182 else if (evt
.keyCode
&& evt
.type
=== 'keypress') {
183 // IE10 stores the char code as keyCode, and has no other useful properties
184 codepoint
= evt
.keyCode
;
187 return keysyms
.fromUnicode(substituteCodepoint(codepoint
));
189 // we could check evt.key here.
190 // Legal values are defined in http://www.w3.org/TR/DOM-Level-3-Events/#key-values-list,
191 // so we "just" need to map them to keysym, but AFAIK this is only available in IE10, which also provides evt.key
192 // so we don't *need* it yet
194 return keysyms
.lookup(keysymFromKeyCode(evt
.keyCode
, evt
.shiftKey
));
197 return keysyms
.lookup(keysymFromKeyCode(evt
.which
, evt
.shiftKey
));
202 // Given a keycode, try to predict which keysym it might be.
203 // If the keycode is unknown, null is returned.
204 function keysymFromKeyCode(keycode
, shiftPressed
) {
205 if (typeof(keycode
) !== 'number') {
208 // won't be accurate for azerty
209 if (keycode
>= 0x30 && keycode
<= 0x39) {
210 return keycode
; // digit
212 if (keycode
>= 0x41 && keycode
<= 0x5a) {
213 // remap to lowercase unless shift is down
214 return shiftPressed
? keycode
: keycode
+ 32; // A-Z
216 if (keycode
>= 0x60 && keycode
<= 0x69) {
217 return KeyTable
.XK_KP_0
+ (keycode
- 0x60); // numpad 0-9
221 case 0x20: return KeyTable
.XK_space
;
222 case 0x6a: return KeyTable
.XK_KP_Multiply
;
223 case 0x6b: return KeyTable
.XK_KP_Add
;
224 case 0x6c: return KeyTable
.XK_KP_Separator
;
225 case 0x6d: return KeyTable
.XK_KP_Subtract
;
226 case 0x6e: return KeyTable
.XK_KP_Decimal
;
227 case 0x6f: return KeyTable
.XK_KP_Divide
;
228 case 0xbb: return KeyTable
.XK_plus
;
229 case 0xbc: return KeyTable
.XK_comma
;
230 case 0xbd: return KeyTable
.XK_minus
;
231 case 0xbe: return KeyTable
.XK_period
;
234 return nonCharacterKey({keyCode
: keycode
});
237 // if the key is a known non-character key (any key which doesn't generate character data)
238 // return its keysym value. Otherwise return null
239 function nonCharacterKey(evt
) {
240 // evt.key not implemented yet
241 if (!evt
.keyCode
) { return null; }
242 var keycode
= evt
.keyCode
;
244 if (keycode
>= 0x70 && keycode
<= 0x87) {
245 return KeyTable
.XK_F1
+ keycode
- 0x70; // F1-F24
249 case 8 : return KeyTable
.XK_BackSpace
;
250 case 13 : return KeyTable
.XK_Return
;
252 case 9 : return KeyTable
.XK_Tab
;
254 case 27 : return KeyTable
.XK_Escape
;
255 case 46 : return KeyTable
.XK_Delete
;
257 case 36 : return KeyTable
.XK_Home
;
258 case 35 : return KeyTable
.XK_End
;
259 case 33 : return KeyTable
.XK_Page_Up
;
260 case 34 : return KeyTable
.XK_Page_Down
;
261 case 45 : return KeyTable
.XK_Insert
;
263 case 37 : return KeyTable
.XK_Left
;
264 case 38 : return KeyTable
.XK_Up
;
265 case 39 : return KeyTable
.XK_Right
;
266 case 40 : return KeyTable
.XK_Down
;
268 case 16 : return KeyTable
.XK_Shift_L
;
269 case 17 : return KeyTable
.XK_Control_L
;
270 case 18 : return KeyTable
.XK_Alt_L
; // also: Option-key on Mac
272 case 224 : return KeyTable
.XK_Meta_L
;
273 case 225 : return KeyTable
.XK_ISO_Level3_Shift
; // AltGr
274 case 91 : return KeyTable
.XK_Super_L
; // also: Windows-key
275 case 92 : return KeyTable
.XK_Super_R
; // also: Windows-key
276 case 93 : return KeyTable
.XK_Menu
; // also: Windows-Menu, Command on Mac
277 default: return null;
281 KeyboardUtil
.hasShortcutModifier
= hasShortcutModifier
;
282 KeyboardUtil
.hasCharModifier
= hasCharModifier
;
283 KeyboardUtil
.ModifierSync
= ModifierSync
;
284 KeyboardUtil
.getKey
= getKey
;
285 KeyboardUtil
.getKeysym
= getKeysym
;
286 KeyboardUtil
.keysymFromKeyCode
= keysymFromKeyCode
;
287 KeyboardUtil
.nonCharacterKey
= nonCharacterKey
;
288 KeyboardUtil
.substituteCodepoint
= substituteCodepoint
;
291 KeyboardUtil
.QEMUKeyEventDecoder = function(modifierState
, next
) {
294 function sendAll(evts
) {
295 for (var i
= 0; i
< evts
.length
; ++i
) {
300 var numPadCodes
= ["Numpad0", "Numpad1", "Numpad2",
301 "Numpad3", "Numpad4", "Numpad5", "Numpad6",
302 "Numpad7", "Numpad8", "Numpad9", "NumpadDecimal"];
304 var numLockOnKeySyms
= {
305 "Numpad0": 0xffb0, "Numpad1": 0xffb1, "Numpad2": 0xffb2,
306 "Numpad3": 0xffb3, "Numpad4": 0xffb4, "Numpad5": 0xffb5,
307 "Numpad6": 0xffb6, "Numpad7": 0xffb7, "Numpad8": 0xffb8,
308 "Numpad9": 0xffb9, "NumpadDecimal": 0xffac
311 var numLockOnKeyCodes
= [96, 97, 98, 99, 100, 101, 102,
312 103, 104, 105, 108, 110];
314 function isNumPadMultiKey(evt
) {
315 return (numPadCodes
.indexOf(evt
.code
) !== -1);
318 function getNumPadKeySym(evt
) {
319 if (numLockOnKeyCodes
.indexOf(evt
.keyCode
) !== -1) {
320 return numLockOnKeySyms
[evt
.code
];
325 function process(evt
, type
) {
326 var result
= {type
: type
};
327 result
.code
= evt
.code
;
330 if (isNumPadMultiKey(evt
)) {
331 result
.keysym
= getNumPadKeySym(evt
);
334 var hasModifier
= modifierState
.hasShortcutModifier() || !!modifierState
.activeCharModifier();
335 var isShift
= evt
.keyCode
=== 0x10 || evt
.key
=== 'Shift';
337 var suppress
= !isShift
&& (type
!== 'keydown' || modifierState
.hasShortcutModifier() || !!KeyboardUtil
.nonCharacterKey(evt
));
343 keydown: function(evt
) {
344 sendAll(modifierState
.keydown(evt
));
345 return process(evt
, 'keydown');
347 keypress: function(evt
) {
350 keyup: function(evt
) {
351 sendAll(modifierState
.keyup(evt
));
352 return process(evt
, 'keyup');
354 syncModifiers: function(evt
) {
355 sendAll(modifierState
.syncAny(evt
));
357 releaseAll: function() { next({type
: 'releaseall'}); }
361 KeyboardUtil
.TrackQEMUKeyState = function(next
) {
365 return function (evt
) {
366 var last
= state
.length
!== 0 ? state
[state
.length
-1] : null;
371 if (!last
|| last
.code
!== evt
.code
) {
372 last
= {code
: evt
.code
};
374 if (state
.length
> 0 && state
[state
.length
-1].code
== 'ControlLeft') {
375 if (evt
.code
!== 'AltRight') {
376 next({code
: 'ControlLeft', type
: 'keydown', keysym
: 0});
383 if (evt
.code
!== 'ControlLeft') {
389 if (state
.length
=== 0) {
393 // do we have a matching key tracked as being down?
394 for (var i
= 0; i
!== state
.length
; ++i
) {
395 if (state
[i
].code
=== evt
.code
) {
400 // if we couldn't find a match (it happens), assume it was the last key pressed
402 if (evt
.code
=== 'ControlLeft') {
405 idx
= state
.length
- 1;
408 state
.splice(idx
, 1);
412 /* jshint shadow: true */
413 for (var i
= 0; i
< state
.length
; ++i
) {
414 next({code
: state
[i
].code
, keysym
: 0, type
: 'keyup'});
416 /* jshint shadow: false */
422 // Takes a DOM keyboard event and:
423 // - determines which keysym it represents
424 // - determines a keyId identifying the key that was pressed (corresponding to the key/keyCode properties on the DOM event)
425 // - synthesizes events to synchronize modifier key state between which modifiers are actually down, and which we thought were down
426 // - marks each event with an 'escape' property if a modifier was down which should be "escaped"
427 // - generates a "stall" event in cases where it might be necessary to wait and see if a keypress event follows a keydown
428 // This information is collected into an object which is passed to the next() function. (one call per event)
429 KeyboardUtil
.KeyEventDecoder = function(modifierState
, next
) {
431 function sendAll(evts
) {
432 for (var i
= 0; i
< evts
.length
; ++i
) {
436 function process(evt
, type
) {
437 var result
= {type
: type
};
438 var keyId
= KeyboardUtil
.getKey(evt
);
440 result
.keyId
= keyId
;
443 var keysym
= KeyboardUtil
.getKeysym(evt
);
445 var hasModifier
= modifierState
.hasShortcutModifier() || !!modifierState
.activeCharModifier();
446 // Is this a case where we have to decide on the keysym right away, rather than waiting for the keypress?
447 // "special" keys like enter, tab or backspace don't send keypress events,
448 // and some browsers don't send keypresses at all if a modifier is down
449 if (keysym
&& (type
!== 'keydown' || KeyboardUtil
.nonCharacterKey(evt
) || hasModifier
)) {
450 result
.keysym
= keysym
;
453 var isShift
= evt
.keyCode
=== 0x10 || evt
.key
=== 'Shift';
455 // Should we prevent the browser from handling the event?
456 // Doing so on a keydown (in most browsers) prevents keypress from being generated
457 // so only do that if we have to.
458 var suppress
= !isShift
&& (type
!== 'keydown' || modifierState
.hasShortcutModifier() || !!KeyboardUtil
.nonCharacterKey(evt
));
460 // If a char modifier is down on a keydown, we need to insert a stall,
461 // so VerifyCharModifier knows to wait and see if a keypress is comnig
462 var stall
= type
=== 'keydown' && modifierState
.activeCharModifier() && !KeyboardUtil
.nonCharacterKey(evt
);
464 // if a char modifier is pressed, get the keys it consists of (on Windows, AltGr is equivalent to Ctrl+Alt)
465 var active
= modifierState
.activeCharModifier();
467 // If we have a char modifier down, and we're able to determine a keysym reliably
468 // then (a) we know to treat the modifier as a char modifier,
469 // and (b) we'll have to "escape" the modifier to undo the modifier when sending the char.
470 if (active
&& keysym
) {
471 var isCharModifier
= false;
472 for (var i
= 0; i
< active
.length
; ++i
) {
473 if (active
[i
] === keysym
.keysym
) {
474 isCharModifier
= true;
477 if (type
=== 'keypress' && !isCharModifier
) {
478 result
.escape
= modifierState
.activeCharModifier();
483 // insert a fake "stall" event
484 next({type
: 'stall'});
492 keydown: function(evt
) {
493 sendAll(modifierState
.keydown(evt
));
494 return process(evt
, 'keydown');
496 keypress: function(evt
) {
497 return process(evt
, 'keypress');
499 keyup: function(evt
) {
500 sendAll(modifierState
.keyup(evt
));
501 return process(evt
, 'keyup');
503 syncModifiers: function(evt
) {
504 sendAll(modifierState
.syncAny(evt
));
506 releaseAll: function() { next({type
: 'releaseall'}); }
510 // Combines keydown and keypress events where necessary to handle char modifiers.
511 // On some OS'es, a char modifier is sometimes used as a shortcut modifier.
512 // 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
513 // 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.
514 // The only way we can distinguish these cases is to wait and see if a keypress event arrives
515 // When we receive a "stall" event, wait a few ms before processing the next keydown. If a keypress has also arrived, merge the two
516 KeyboardUtil
.VerifyCharModifier = function(next
) {
525 var delayProcess = function () {
531 while (queue
.length
!== 0) {
533 queue
= queue
.splice(1);
536 // insert a delay before processing available events.
537 /* jshint loopfunc: true */
538 timer
= setTimeout(delayProcess
, 5);
539 /* jshint loopfunc: false */
542 // is the next element a keypress? Then we should merge the two
543 if (queue
.length
!== 0 && queue
[0].type
=== 'keypress') {
544 // Firefox sends keypress even when no char is generated.
545 // so, if keypress keysym is the same as we'd have guessed from keydown,
546 // the modifier didn't have any effect, and should not be escaped
547 if (queue
[0].escape
&& (!cur
.keysym
|| cur
.keysym
.keysym
!== queue
[0].keysym
.keysym
)) {
548 cur
.escape
= queue
[0].escape
;
550 cur
.keysym
= queue
[0].keysym
;
551 queue
= queue
.splice(1);
556 // swallow stall events, and pass all others to the next stage
557 if (cur
.type
!== 'stall') {
562 return function(evt
) {
568 // Keeps track of which keys we (and the server) believe are down
569 // When a keyup is received, match it against this list, to determine the corresponding keysym(s)
570 // in some cases, a single key may produce multiple keysyms, so the corresponding keyup event must release all of these chars
571 // key repeat events should be merged into a single entry.
572 // Because we can't always identify which entry a keydown or keyup event corresponds to, we sometimes have to guess
573 KeyboardUtil
.TrackKeyState = function(next
) {
577 return function (evt
) {
578 var last
= state
.length
!== 0 ? state
[state
.length
-1] : null;
582 // insert a new entry if last seen key was different.
583 if (!last
|| !evt
.keyId
|| last
.keyId
!== evt
.keyId
) {
584 last
= {keyId
: evt
.keyId
, keysyms
: {}};
588 // make sure last event contains this keysym (a single "logical" keyevent
589 // can cause multiple key events to be sent to the VNC server)
590 last
.keysyms
[evt
.keysym
.keysym
] = evt
.keysym
;
591 last
.ignoreKeyPress
= true;
597 last
= {keyId
: evt
.keyId
, keysyms
: {}};
601 console
.log('keypress with no keysym:', evt
);
604 // If we didn't expect a keypress, and already sent a keydown to the VNC server
605 // based on the keydown, make sure to skip this event.
606 if (evt
.keysym
&& !last
.ignoreKeyPress
) {
607 last
.keysyms
[evt
.keysym
.keysym
] = evt
.keysym
;
608 evt
.type
= 'keydown';
613 if (state
.length
=== 0) {
617 // do we have a matching key tracked as being down?
618 for (var i
= 0; i
!== state
.length
; ++i
) {
619 if (state
[i
].keyId
=== evt
.keyId
) {
624 // if we couldn't find a match (it happens), assume it was the last key pressed
626 idx
= state
.length
- 1;
629 var item
= state
.splice(idx
, 1)[0];
630 // for each keysym tracked by this key entry, clone the current event and override the keysym
631 var clone
= (function(){
633 return function (obj
) { Clone
.prototype=obj
; return new Clone(); };
635 for (var key
in item
.keysyms
) {
636 var out
= clone(evt
);
637 out
.keysym
= item
.keysyms
[key
];
642 /* jshint shadow: true */
643 for (var i
= 0; i
< state
.length
; ++i
) {
644 for (var key
in state
[i
].keysyms
) {
645 var keysym
= state
[i
].keysyms
[key
];
646 next({keyId
: 0, keysym
: keysym
, type
: 'keyup'});
649 /* jshint shadow: false */
655 // Handles "escaping" of modifiers: if a char modifier is used to produce a keysym (such as AltGr-2 to generate an @),
656 // then the modifier must be "undone" before sending the @, and "redone" afterwards.
657 KeyboardUtil
.EscapeModifiers = function(next
) {
659 return function(evt
) {
660 if (evt
.type
!== 'keydown' || evt
.escape
=== undefined) {
665 for (var i
= 0; i
< evt
.escape
.length
; ++i
) {
666 next({type
: 'keyup', keyId
: 0, keysym
: keysyms
.lookup(evt
.escape
[i
])});
668 // send the character event
671 /* jshint shadow: true */
672 for (var i
= 0; i
< evt
.escape
.length
; ++i
) {
673 next({type
: 'keydown', keyId
: 0, keysym
: keysyms
.lookup(evt
.escape
[i
])});
675 /* jshint shadow: false */
679 /* [module] export default KeyboardUtil; */