]>
git.proxmox.com Git - mirror_novnc.git/blob - core/input/util.js
9124c196a68eb60d7f13ed34dd0d7d7864361e36
1 import KeyTable
from "./keysym.js";
2 import keysyms
from "./keysymdef.js";
5 return navigator
&& !!(/mac/i).exec(navigator
.platform
);
8 return navigator
&& !!(/win/i).exec(navigator
.platform
);
11 return navigator
&& !!(/linux/i).exec(navigator
.platform
);
14 // Return true if a modifier which is not the specified char modifier (and is not shift) is down
15 export function hasShortcutModifier(charModifier
, currentModifiers
) {
17 for (var key
in currentModifiers
) {
18 if (parseInt(key
) !== KeyTable
.XK_Shift_L
) {
19 mods
[key
] = currentModifiers
[key
];
24 for (var k
in currentModifiers
) {
29 if (hasCharModifier(charModifier
, mods
)) {
30 return sum
> charModifier
.length
;
37 // Return true if the specified char modifier is currently down
38 export function hasCharModifier(charModifier
, currentModifiers
) {
39 if (charModifier
.length
=== 0) { return false; }
41 for (var i
= 0; i
< charModifier
.length
; ++i
) {
42 if (!currentModifiers
[charModifier
[i
]]) {
49 // Helper object tracking modifier key state
50 // and generates fake key events to compensate if it gets out of sync
51 export function ModifierSync(charModifier
) {
54 // on Mac, Option (AKA Alt) is used as a char modifier
55 charModifier
= [KeyTable
.XK_Alt_L
];
57 else if (isWindows()) {
58 // on Windows, Ctrl+Alt is used as a char modifier
59 charModifier
= [KeyTable
.XK_Alt_L
, KeyTable
.XK_Control_L
];
62 // on Linux, ISO Level 3 Shift (AltGr) is used as a char modifier
63 charModifier
= [KeyTable
.XK_ISO_Level3_Shift
];
71 state
[KeyTable
.XK_Control_L
] = false;
72 state
[KeyTable
.XK_Alt_L
] = false;
73 state
[KeyTable
.XK_ISO_Level3_Shift
] = false;
74 state
[KeyTable
.XK_Shift_L
] = false;
75 state
[KeyTable
.XK_Meta_L
] = false;
77 function sync(evt
, keysym
) {
79 function syncKey(keysym
) {
80 return {keysym
: keysym
, type
: state
[keysym
] ? 'keydown' : 'keyup'};
83 if (evt
.ctrlKey
!== undefined &&
84 evt
.ctrlKey
!== state
[KeyTable
.XK_Control_L
] && keysym
!== KeyTable
.XK_Control_L
) {
85 state
[KeyTable
.XK_Control_L
] = evt
.ctrlKey
;
86 result
.push(syncKey(KeyTable
.XK_Control_L
));
88 if (evt
.altKey
!== undefined &&
89 evt
.altKey
!== state
[KeyTable
.XK_Alt_L
] && keysym
!== KeyTable
.XK_Alt_L
) {
90 state
[KeyTable
.XK_Alt_L
] = evt
.altKey
;
91 result
.push(syncKey(KeyTable
.XK_Alt_L
));
93 if (evt
.altGraphKey
!== undefined &&
94 evt
.altGraphKey
!== state
[KeyTable
.XK_ISO_Level3_Shift
] && keysym
!== KeyTable
.XK_ISO_Level3_Shift
) {
95 state
[KeyTable
.XK_ISO_Level3_Shift
] = evt
.altGraphKey
;
96 result
.push(syncKey(KeyTable
.XK_ISO_Level3_Shift
));
98 if (evt
.shiftKey
!== undefined &&
99 evt
.shiftKey
!== state
[KeyTable
.XK_Shift_L
] && keysym
!== KeyTable
.XK_Shift_L
) {
100 state
[KeyTable
.XK_Shift_L
] = evt
.shiftKey
;
101 result
.push(syncKey(KeyTable
.XK_Shift_L
));
103 if (evt
.metaKey
!== undefined &&
104 evt
.metaKey
!== state
[KeyTable
.XK_Meta_L
] && keysym
!== KeyTable
.XK_Meta_L
) {
105 state
[KeyTable
.XK_Meta_L
] = evt
.metaKey
;
106 result
.push(syncKey(KeyTable
.XK_Meta_L
));
110 function syncKeyEvent(evt
, down
) {
111 var keysym
= getKeysym(evt
);
113 // first, apply the event itself, if relevant
114 if (keysym
!== null && state
[keysym
] !== undefined) {
115 state
[keysym
] = down
;
117 return sync(evt
, keysym
);
121 // sync on the appropriate keyboard event
122 keydown: function(evt
) { return syncKeyEvent(evt
, true);},
123 keyup: function(evt
) { return syncKeyEvent(evt
, false);},
124 // Call this with a non-keyboard event (such as mouse events) to use its modifier state to synchronize anyway
125 syncAny: function(evt
) { return sync(evt
);},
127 // is a shortcut modifier down?
128 hasShortcutModifier: function() { return hasShortcutModifier(charModifier
, state
); },
129 // if a char modifier is down, return the keys it consists of, otherwise return null
130 activeCharModifier: function() { return hasCharModifier(charModifier
, state
) ? charModifier
: null; }
134 // Get a key ID from a keyboard event
135 // May be a string or an integer depending on the available properties
136 export function getKey(evt
){
137 if ('keyCode' in evt
&& 'key' in evt
) {
138 return evt
.key
+ ':' + evt
.keyCode
;
140 else if ('keyCode' in evt
) {
148 // Get the most reliable keysym value we can get from a key event
149 // if char/charCode is available, prefer those, otherwise fall back to key/keyCode/which
150 export function getKeysym(evt
){
152 if (evt
.char && evt
.char.length
=== 1) {
153 codepoint
= evt
.char.charCodeAt();
155 else if (evt
.charCode
) {
156 codepoint
= evt
.charCode
;
158 else if (evt
.keyCode
&& evt
.type
=== 'keypress') {
159 // IE10 stores the char code as keyCode, and has no other useful properties
160 codepoint
= evt
.keyCode
;
163 return keysyms
.lookup(codepoint
);
165 // we could check evt.key here.
166 // Legal values are defined in http://www.w3.org/TR/DOM-Level-3-Events/#key-values-list,
167 // so we "just" need to map them to keysym, but AFAIK this is only available in IE10, which also provides evt.key
168 // so we don't *need* it yet
170 return keysymFromKeyCode(evt
.keyCode
, evt
.shiftKey
);
173 return keysymFromKeyCode(evt
.which
, evt
.shiftKey
);
178 // Given a keycode, try to predict which keysym it might be.
179 // If the keycode is unknown, null is returned.
180 function keysymFromKeyCode(keycode
, shiftPressed
) {
181 if (typeof(keycode
) !== 'number') {
184 // won't be accurate for azerty
185 if (keycode
>= 0x30 && keycode
<= 0x39) {
186 return keycode
; // digit
188 if (keycode
>= 0x41 && keycode
<= 0x5a) {
189 // remap to lowercase unless shift is down
190 return shiftPressed
? keycode
: keycode
+ 32; // A-Z
192 if (keycode
>= 0x60 && keycode
<= 0x69) {
193 return KeyTable
.XK_KP_0
+ (keycode
- 0x60); // numpad 0-9
197 case 0x20: return KeyTable
.XK_space
;
198 case 0x6a: return KeyTable
.XK_KP_Multiply
;
199 case 0x6b: return KeyTable
.XK_KP_Add
;
200 case 0x6c: return KeyTable
.XK_KP_Separator
;
201 case 0x6d: return KeyTable
.XK_KP_Subtract
;
202 case 0x6e: return KeyTable
.XK_KP_Decimal
;
203 case 0x6f: return KeyTable
.XK_KP_Divide
;
204 case 0xbb: return KeyTable
.XK_plus
;
205 case 0xbc: return KeyTable
.XK_comma
;
206 case 0xbd: return KeyTable
.XK_minus
;
207 case 0xbe: return KeyTable
.XK_period
;
210 return nonCharacterKey({keyCode
: keycode
});
213 // if the key is a known non-character key (any key which doesn't generate character data)
214 // return its keysym value. Otherwise return null
215 function nonCharacterKey(evt
) {
216 // evt.key not implemented yet
217 if (!evt
.keyCode
) { return null; }
218 var keycode
= evt
.keyCode
;
220 if (keycode
>= 0x70 && keycode
<= 0x87) {
221 return KeyTable
.XK_F1
+ keycode
- 0x70; // F1-F24
225 case 8 : return KeyTable
.XK_BackSpace
;
226 case 13 : return KeyTable
.XK_Return
;
228 case 9 : return KeyTable
.XK_Tab
;
230 case 27 : return KeyTable
.XK_Escape
;
231 case 46 : return KeyTable
.XK_Delete
;
233 case 36 : return KeyTable
.XK_Home
;
234 case 35 : return KeyTable
.XK_End
;
235 case 33 : return KeyTable
.XK_Page_Up
;
236 case 34 : return KeyTable
.XK_Page_Down
;
237 case 45 : return KeyTable
.XK_Insert
;
239 case 37 : return KeyTable
.XK_Left
;
240 case 38 : return KeyTable
.XK_Up
;
241 case 39 : return KeyTable
.XK_Right
;
242 case 40 : return KeyTable
.XK_Down
;
244 case 16 : return KeyTable
.XK_Shift_L
;
245 case 17 : return KeyTable
.XK_Control_L
;
246 case 18 : return KeyTable
.XK_Alt_L
; // also: Option-key on Mac
248 case 224 : return KeyTable
.XK_Meta_L
;
249 case 225 : return KeyTable
.XK_ISO_Level3_Shift
; // AltGr
250 case 91 : return KeyTable
.XK_Super_L
; // also: Windows-key
251 case 92 : return KeyTable
.XK_Super_R
; // also: Windows-key
252 case 93 : return KeyTable
.XK_Menu
; // also: Windows-Menu, Command on Mac
253 default: return null;
257 export function QEMUKeyEventDecoder (modifierState
, next
) {
260 function sendAll(evts
) {
261 for (var i
= 0; i
< evts
.length
; ++i
) {
266 var numPadCodes
= ["Numpad0", "Numpad1", "Numpad2",
267 "Numpad3", "Numpad4", "Numpad5", "Numpad6",
268 "Numpad7", "Numpad8", "Numpad9", "NumpadDecimal"];
270 var numLockOnKeySyms
= {
271 "Numpad0": 0xffb0, "Numpad1": 0xffb1, "Numpad2": 0xffb2,
272 "Numpad3": 0xffb3, "Numpad4": 0xffb4, "Numpad5": 0xffb5,
273 "Numpad6": 0xffb6, "Numpad7": 0xffb7, "Numpad8": 0xffb8,
274 "Numpad9": 0xffb9, "NumpadDecimal": 0xffac
277 var numLockOnKeyCodes
= [96, 97, 98, 99, 100, 101, 102,
278 103, 104, 105, 108, 110];
280 function isNumPadMultiKey(evt
) {
281 return (numPadCodes
.indexOf(evt
.code
) !== -1);
284 function getNumPadKeySym(evt
) {
285 if (numLockOnKeyCodes
.indexOf(evt
.keyCode
) !== -1) {
286 return numLockOnKeySyms
[evt
.code
];
291 function process(evt
, type
) {
292 var result
= {type
: type
};
293 result
.code
= evt
.code
;
296 if (isNumPadMultiKey(evt
)) {
297 result
.keysym
= getNumPadKeySym(evt
);
300 var hasModifier
= modifierState
.hasShortcutModifier() || !!modifierState
.activeCharModifier();
301 var isShift
= evt
.keyCode
=== 0x10 || evt
.key
=== 'Shift';
303 var suppress
= !isShift
&& (type
!== 'keydown' || modifierState
.hasShortcutModifier() || !!nonCharacterKey(evt
));
309 keydown: function(evt
) {
310 sendAll(modifierState
.keydown(evt
));
311 return process(evt
, 'keydown');
313 keypress: function(evt
) {
316 keyup: function(evt
) {
317 sendAll(modifierState
.keyup(evt
));
318 return process(evt
, 'keyup');
320 syncModifiers: function(evt
) {
321 sendAll(modifierState
.syncAny(evt
));
323 releaseAll: function() { next({type
: 'releaseall'}); }
327 export function TrackQEMUKeyState (next
) {
331 return function (evt
) {
332 var last
= state
.length
!== 0 ? state
[state
.length
-1] : null;
337 if (!last
|| last
.code
!== evt
.code
) {
338 last
= {code
: evt
.code
};
340 if (state
.length
> 0 && state
[state
.length
-1].code
== 'ControlLeft') {
341 if (evt
.code
!== 'AltRight') {
342 next({code
: 'ControlLeft', type
: 'keydown', keysym
: 0});
349 if (evt
.code
!== 'ControlLeft') {
355 if (state
.length
=== 0) {
359 // do we have a matching key tracked as being down?
360 for (var i
= 0; i
!== state
.length
; ++i
) {
361 if (state
[i
].code
=== evt
.code
) {
366 // if we couldn't find a match (it happens), assume it was the last key pressed
368 if (evt
.code
=== 'ControlLeft') {
371 idx
= state
.length
- 1;
374 state
.splice(idx
, 1);
378 /* jshint shadow: true */
379 for (var i
= 0; i
< state
.length
; ++i
) {
380 next({code
: state
[i
].code
, keysym
: 0, type
: 'keyup'});
382 /* jshint shadow: false */
388 // Takes a DOM keyboard event and:
389 // - determines which keysym it represents
390 // - determines a keyId identifying the key that was pressed (corresponding to the key/keyCode properties on the DOM event)
391 // - synthesizes events to synchronize modifier key state between which modifiers are actually down, and which we thought were down
392 // - marks each event with an 'escape' property if a modifier was down which should be "escaped"
393 // - generates a "stall" event in cases where it might be necessary to wait and see if a keypress event follows a keydown
394 // This information is collected into an object which is passed to the next() function. (one call per event)
395 export function KeyEventDecoder (modifierState
, next
) {
397 function sendAll(evts
) {
398 for (var i
= 0; i
< evts
.length
; ++i
) {
402 function process(evt
, type
) {
403 var result
= {type
: type
};
404 var keyId
= getKey(evt
);
406 result
.keyId
= keyId
;
409 var keysym
= getKeysym(evt
);
411 var hasModifier
= modifierState
.hasShortcutModifier() || !!modifierState
.activeCharModifier();
412 // Is this a case where we have to decide on the keysym right away, rather than waiting for the keypress?
413 // "special" keys like enter, tab or backspace don't send keypress events,
414 // and some browsers don't send keypresses at all if a modifier is down
415 if (keysym
&& (type
!== 'keydown' || nonCharacterKey(evt
) || hasModifier
)) {
416 result
.keysym
= keysym
;
419 var isShift
= evt
.keyCode
=== 0x10 || evt
.key
=== 'Shift';
421 // Should we prevent the browser from handling the event?
422 // Doing so on a keydown (in most browsers) prevents keypress from being generated
423 // so only do that if we have to.
424 var suppress
= !isShift
&& (type
!== 'keydown' || modifierState
.hasShortcutModifier() || !!nonCharacterKey(evt
));
426 // If a char modifier is down on a keydown, we need to insert a stall,
427 // so VerifyCharModifier knows to wait and see if a keypress is comnig
428 var stall
= type
=== 'keydown' && modifierState
.activeCharModifier() && !nonCharacterKey(evt
);
430 // if a char modifier is pressed, get the keys it consists of (on Windows, AltGr is equivalent to Ctrl+Alt)
431 var active
= modifierState
.activeCharModifier();
433 // If we have a char modifier down, and we're able to determine a keysym reliably
434 // then (a) we know to treat the modifier as a char modifier,
435 // and (b) we'll have to "escape" the modifier to undo the modifier when sending the char.
436 if (active
&& keysym
) {
437 var isCharModifier
= false;
438 for (var i
= 0; i
< active
.length
; ++i
) {
439 if (active
[i
] === keysym
) {
440 isCharModifier
= true;
443 if (type
=== 'keypress' && !isCharModifier
) {
444 result
.escape
= modifierState
.activeCharModifier();
449 // insert a fake "stall" event
450 next({type
: 'stall'});
458 keydown: function(evt
) {
459 sendAll(modifierState
.keydown(evt
));
460 return process(evt
, 'keydown');
462 keypress: function(evt
) {
463 return process(evt
, 'keypress');
465 keyup: function(evt
) {
466 sendAll(modifierState
.keyup(evt
));
467 return process(evt
, 'keyup');
469 syncModifiers: function(evt
) {
470 sendAll(modifierState
.syncAny(evt
));
472 releaseAll: function() { next({type
: 'releaseall'}); }
476 // Combines keydown and keypress events where necessary to handle char modifiers.
477 // On some OS'es, a char modifier is sometimes used as a shortcut modifier.
478 // 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
479 // 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.
480 // The only way we can distinguish these cases is to wait and see if a keypress event arrives
481 // When we receive a "stall" event, wait a few ms before processing the next keydown. If a keypress has also arrived, merge the two
482 export function VerifyCharModifier (next
) {
491 var delayProcess = function () {
497 while (queue
.length
!== 0) {
499 queue
= queue
.splice(1);
502 // insert a delay before processing available events.
503 /* jshint loopfunc: true */
504 timer
= setTimeout(delayProcess
, 5);
505 /* jshint loopfunc: false */
508 // is the next element a keypress? Then we should merge the two
509 if (queue
.length
!== 0 && queue
[0].type
=== 'keypress') {
510 // Firefox sends keypress even when no char is generated.
511 // so, if keypress keysym is the same as we'd have guessed from keydown,
512 // the modifier didn't have any effect, and should not be escaped
513 if (queue
[0].escape
&& (!cur
.keysym
|| cur
.keysym
!== queue
[0].keysym
)) {
514 cur
.escape
= queue
[0].escape
;
516 cur
.keysym
= queue
[0].keysym
;
517 queue
= queue
.splice(1);
522 // swallow stall events, and pass all others to the next stage
523 if (cur
.type
!== 'stall') {
528 return function(evt
) {
534 // Keeps track of which keys we (and the server) believe are down
535 // When a keyup is received, match it against this list, to determine the corresponding keysym(s)
536 // in some cases, a single key may produce multiple keysyms, so the corresponding keyup event must release all of these chars
537 // key repeat events should be merged into a single entry.
538 // Because we can't always identify which entry a keydown or keyup event corresponds to, we sometimes have to guess
539 export function TrackKeyState (next
) {
543 return function (evt
) {
544 var last
= state
.length
!== 0 ? state
[state
.length
-1] : null;
548 // insert a new entry if last seen key was different.
549 if (!last
|| !evt
.keyId
|| last
.keyId
!== evt
.keyId
) {
550 last
= {keyId
: evt
.keyId
, keysyms
: {}};
554 // make sure last event contains this keysym (a single "logical" keyevent
555 // can cause multiple key events to be sent to the VNC server)
556 last
.keysyms
[evt
.keysym
] = evt
.keysym
;
557 last
.ignoreKeyPress
= true;
563 last
= {keyId
: evt
.keyId
, keysyms
: {}};
567 console
.log('keypress with no keysym:', evt
);
570 // If we didn't expect a keypress, and already sent a keydown to the VNC server
571 // based on the keydown, make sure to skip this event.
572 if (evt
.keysym
&& !last
.ignoreKeyPress
) {
573 last
.keysyms
[evt
.keysym
] = evt
.keysym
;
574 evt
.type
= 'keydown';
579 if (state
.length
=== 0) {
583 // do we have a matching key tracked as being down?
584 for (var i
= 0; i
!== state
.length
; ++i
) {
585 if (state
[i
].keyId
=== evt
.keyId
) {
590 // if we couldn't find a match (it happens), assume it was the last key pressed
592 idx
= state
.length
- 1;
595 var item
= state
.splice(idx
, 1)[0];
596 // for each keysym tracked by this key entry, clone the current event and override the keysym
597 var clone
= (function(){
599 return function (obj
) { Clone
.prototype=obj
; return new Clone(); };
601 for (var key
in item
.keysyms
) {
602 var out
= clone(evt
);
603 out
.keysym
= item
.keysyms
[key
];
608 /* jshint shadow: true */
609 for (var i
= 0; i
< state
.length
; ++i
) {
610 for (var key
in state
[i
].keysyms
) {
611 var keysym
= state
[i
].keysyms
[key
];
612 next({keyId
: 0, keysym
: keysym
, type
: 'keyup'});
615 /* jshint shadow: false */
621 // Handles "escaping" of modifiers: if a char modifier is used to produce a keysym (such as AltGr-2 to generate an @),
622 // then the modifier must be "undone" before sending the @, and "redone" afterwards.
623 export function EscapeModifiers (next
) {
625 return function(evt
) {
626 if (evt
.type
!== 'keydown' || evt
.escape
=== undefined) {
631 for (var i
= 0; i
< evt
.escape
.length
; ++i
) {
632 next({type
: 'keyup', keyId
: 0, keysym
: evt
.escape
[i
]});
634 // send the character event
637 /* jshint shadow: true */
638 for (var i
= 0; i
< evt
.escape
.length
; ++i
) {
639 next({type
: 'keydown', keyId
: 0, keysym
: evt
.escape
[i
]});
641 /* jshint shadow: false */