]> git.proxmox.com Git - mirror_novnc.git/blob - core/input/util.js
Remove modifier synchronisation
[mirror_novnc.git] / core / input / util.js
1 import KeyTable from "./keysym.js";
2 import keysyms from "./keysymdef.js";
3 import vkeys from "./vkeys.js";
4 import fixedkeys from "./fixedkeys.js";
5
6 function isMac() {
7 return navigator && !!(/mac/i).exec(navigator.platform);
8 }
9 function isWindows() {
10 return navigator && !!(/win/i).exec(navigator.platform);
11 }
12 function isLinux() {
13 return navigator && !!(/linux/i).exec(navigator.platform);
14 }
15
16 // Return true if the specified char modifier is currently down
17 export function hasCharModifier(charModifier, currentModifiers) {
18 if (charModifier.length === 0) { return false; }
19
20 for (var i = 0; i < charModifier.length; ++i) {
21 if (!currentModifiers[charModifier[i]]) {
22 return false;
23 }
24 }
25 return true;
26 }
27
28 // Helper object tracking modifier key state
29 // and generates fake key events to compensate if it gets out of sync
30 export function ModifierSync(charModifier) {
31 if (!charModifier) {
32 if (isMac()) {
33 // on Mac, Option (AKA Alt) is used as a char modifier
34 charModifier = [KeyTable.XK_Alt_L];
35 }
36 else if (isWindows()) {
37 // on Windows, Ctrl+Alt is used as a char modifier
38 charModifier = [KeyTable.XK_Alt_L, KeyTable.XK_Control_L];
39 }
40 else if (isLinux()) {
41 // on Linux, ISO Level 3 Shift (AltGr) is used as a char modifier
42 charModifier = [KeyTable.XK_ISO_Level3_Shift];
43 }
44 else {
45 charModifier = [];
46 }
47 }
48
49 var state = {};
50 state[KeyTable.XK_Control_L] = false;
51 state[KeyTable.XK_Alt_L] = false;
52 state[KeyTable.XK_ISO_Level3_Shift] = false;
53 state[KeyTable.XK_Shift_L] = false;
54 state[KeyTable.XK_Meta_L] = false;
55
56 function sync(evt, keysym) {
57 var result = [];
58 function syncKey(keysym) {
59 return {keysym: keysym, type: state[keysym] ? 'keydown' : 'keyup'};
60 }
61
62 if (evt.ctrlKey !== undefined &&
63 evt.ctrlKey !== state[KeyTable.XK_Control_L] && keysym !== KeyTable.XK_Control_L) {
64 state[KeyTable.XK_Control_L] = evt.ctrlKey;
65 result.push(syncKey(KeyTable.XK_Control_L));
66 }
67 if (evt.altKey !== undefined &&
68 evt.altKey !== state[KeyTable.XK_Alt_L] && keysym !== KeyTable.XK_Alt_L) {
69 state[KeyTable.XK_Alt_L] = evt.altKey;
70 result.push(syncKey(KeyTable.XK_Alt_L));
71 }
72 if (evt.altGraphKey !== undefined &&
73 evt.altGraphKey !== state[KeyTable.XK_ISO_Level3_Shift] && keysym !== KeyTable.XK_ISO_Level3_Shift) {
74 state[KeyTable.XK_ISO_Level3_Shift] = evt.altGraphKey;
75 result.push(syncKey(KeyTable.XK_ISO_Level3_Shift));
76 }
77 if (evt.shiftKey !== undefined &&
78 evt.shiftKey !== state[KeyTable.XK_Shift_L] && keysym !== KeyTable.XK_Shift_L) {
79 state[KeyTable.XK_Shift_L] = evt.shiftKey;
80 result.push(syncKey(KeyTable.XK_Shift_L));
81 }
82 if (evt.metaKey !== undefined &&
83 evt.metaKey !== state[KeyTable.XK_Meta_L] && keysym !== KeyTable.XK_Meta_L) {
84 state[KeyTable.XK_Meta_L] = evt.metaKey;
85 result.push(syncKey(KeyTable.XK_Meta_L));
86 }
87 return result;
88 }
89 function syncKeyEvent(evt, down) {
90 var keysym = getKeysym(evt);
91
92 // first, apply the event itself, if relevant
93 if (keysym !== null && state[keysym] !== undefined) {
94 state[keysym] = down;
95 }
96 return sync(evt, keysym);
97 }
98
99 return {
100 // sync on the appropriate keyboard event
101 keydown: function(evt) { return syncKeyEvent(evt, true);},
102 keyup: function(evt) { return syncKeyEvent(evt, false);},
103
104 // if a char modifier is down, return the keys it consists of, otherwise return null
105 activeCharModifier: function() { return hasCharModifier(charModifier, state) ? charModifier : null; }
106 };
107 }
108
109 // Get 'KeyboardEvent.code', handling legacy browsers
110 export function getKeycode(evt){
111 // Are we getting proper key identifiers?
112 // (unfortunately Firefox and Chrome are crappy here and gives
113 // us an empty string on some platforms, rather than leaving it
114 // undefined)
115 if (evt.code) {
116 // Mozilla isn't fully in sync with the spec yet
117 switch (evt.code) {
118 case 'OSLeft': return 'MetaLeft';
119 case 'OSRight': return 'MetaRight';
120 }
121
122 return evt.code;
123 }
124
125 // The de-facto standard is to use Windows Virtual-Key codes
126 // in the 'keyCode' field for non-printable characters. However
127 // Webkit sets it to the same as charCode in 'keypress' events.
128 if ((evt.type !== 'keypress') && (evt.keyCode in vkeys)) {
129 var code = vkeys[evt.keyCode];
130
131 // macOS has messed up this code for some reason
132 if (isMac() && (code === 'ContextMenu')) {
133 code = 'MetaRight';
134 }
135
136 // The keyCode doesn't distinguish between left and right
137 // for the standard modifiers
138 if (evt.location === 2) {
139 switch (code) {
140 case 'ShiftLeft': return 'ShiftRight';
141 case 'ControlLeft': return 'ControlRight';
142 case 'AltLeft': return 'AltRight';
143 }
144 }
145
146 // Nor a bunch of the numpad keys
147 if (evt.location === 3) {
148 switch (code) {
149 case 'Delete': return 'NumpadDecimal';
150 case 'Insert': return 'Numpad0';
151 case 'End': return 'Numpad1';
152 case 'ArrowDown': return 'Numpad2';
153 case 'PageDown': return 'Numpad3';
154 case 'ArrowLeft': return 'Numpad4';
155 case 'ArrowRight': return 'Numpad6';
156 case 'Home': return 'Numpad7';
157 case 'ArrowUp': return 'Numpad8';
158 case 'PageUp': return 'Numpad9';
159 case 'Enter': return 'NumpadEnter';
160 }
161 }
162
163 return code;
164 }
165
166 return 'Unidentified';
167 }
168
169 // Get the most reliable keysym value we can get from a key event
170 export function getKeysym(evt){
171
172 // We start with layout independent keys
173 var code = getKeycode(evt);
174 if (code in fixedkeys) {
175 return fixedkeys[code];
176 }
177
178 // Next with mildly layout or state sensitive stuff
179
180 // Like AltGraph
181 if (code === 'AltRight') {
182 if (evt.key === 'AltGraph') {
183 return KeyTable.XK_ISO_Level3_Shift;
184 } else {
185 return KeyTable.XK_Alt_R;
186 }
187 }
188
189 // Or the numpad
190 if (evt.location === 3) {
191 var key = evt.key;
192
193 // IE and Edge use some ancient version of the spec
194 // https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/8860571/
195 switch (key) {
196 case 'Up': key = 'ArrowUp'; break;
197 case 'Left': key = 'ArrowLeft'; break;
198 case 'Right': key = 'ArrowRight'; break;
199 case 'Down': key = 'ArrowDown'; break;
200 case 'Del': key = 'Delete'; break;
201 }
202
203 // Safari doesn't support KeyboardEvent.key yet
204 if ((key === undefined) && (evt.charCode)) {
205 key = String.fromCharCode(evt.charCode);
206 }
207
208 switch (key) {
209 case '0': return KeyTable.XK_KP_0;
210 case '1': return KeyTable.XK_KP_1;
211 case '2': return KeyTable.XK_KP_2;
212 case '3': return KeyTable.XK_KP_3;
213 case '4': return KeyTable.XK_KP_4;
214 case '5': return KeyTable.XK_KP_5;
215 case '6': return KeyTable.XK_KP_6;
216 case '7': return KeyTable.XK_KP_7;
217 case '8': return KeyTable.XK_KP_8;
218 case '9': return KeyTable.XK_KP_9;
219 // There is utter mayhem in the world when it comes to which
220 // character to use as a decimal separator...
221 case '.': return KeyTable.XK_KP_Decimal;
222 case ',': return KeyTable.XK_KP_Separator;
223 case 'Home': return KeyTable.XK_KP_Home;
224 case 'End': return KeyTable.XK_KP_End;
225 case 'PageUp': return KeyTable.XK_KP_Prior;
226 case 'PageDown': return KeyTable.XK_KP_Next;
227 case 'Insert': return KeyTable.XK_KP_Insert;
228 case 'Delete': return KeyTable.XK_KP_Delete;
229 case 'ArrowUp': return KeyTable.XK_KP_Up;
230 case 'ArrowLeft': return KeyTable.XK_KP_Left;
231 case 'ArrowRight': return KeyTable.XK_KP_Right;
232 case 'ArrowDown': return KeyTable.XK_KP_Down;
233 }
234 }
235
236 // Now we need to look at the Unicode symbol instead
237
238 var codepoint;
239
240 if ('key' in evt) {
241 // Special key? (FIXME: Should have been caught earlier)
242 if (evt.key.length !== 1) {
243 return null;
244 }
245
246 codepoint = evt.key.charCodeAt();
247 } else if ('charCode' in evt) {
248 codepoint = evt.charCode;
249 }
250
251 if (codepoint) {
252 return keysyms.lookup(codepoint);
253 }
254
255 return null;
256 }
257
258 // Takes a DOM keyboard event and:
259 // - determines which keysym it represents
260 // - determines a code identifying the key that was pressed (corresponding to the code/keyCode properties on the DOM event)
261 // - marks each event with an 'escape' property if a modifier was down which should be "escaped"
262 // This information is collected into an object which is passed to the next() function. (one call per event)
263 export function KeyEventDecoder (modifierState, next) {
264 "use strict";
265 function process(evt, type) {
266 var result = {type: type};
267 var code = getKeycode(evt);
268 if (code === 'Unidentified') {
269 // Unstable, but we don't have anything else to go on
270 // (don't use it for 'keypress' events thought since
271 // WebKit sets it to the same as charCode)
272 if (evt.keyCode && (evt.type !== 'keypress')) {
273 code = 'Platform' + evt.keyCode;
274 }
275 }
276 result.code = code;
277
278 var keysym = getKeysym(evt);
279
280 // Is this a case where we have to decide on the keysym right away, rather than waiting for the keypress?
281 // "special" keys like enter, tab or backspace don't send keypress events,
282 // and some browsers don't send keypresses at all if a modifier is down
283 if (keysym) {
284 result.keysym = keysym;
285 }
286
287 // Should we prevent the browser from handling the event?
288 // Doing so on a keydown (in most browsers) prevents keypress from being generated
289 // so only do that if we have to.
290 var suppress = type !== 'keydown' || !!keysym;
291
292 // if a char modifier is pressed, get the keys it consists of (on Windows, AltGr is equivalent to Ctrl+Alt)
293 var active = modifierState.activeCharModifier();
294
295 // If we have a char modifier down, and we're able to determine a keysym reliably
296 // then (a) we know to treat the modifier as a char modifier,
297 // and (b) we'll have to "escape" the modifier to undo the modifier when sending the char.
298 if (active && keysym) {
299 var isCharModifier = false;
300 for (var i = 0; i < active.length; ++i) {
301 if (active[i] === keysym) {
302 isCharModifier = true;
303 }
304 }
305 if (type === 'keypress' && !isCharModifier) {
306 result.escape = modifierState.activeCharModifier();
307 }
308 }
309
310 next(result);
311
312 return suppress;
313 }
314
315 return {
316 keydown: function(evt) {
317 modifierState.keydown(evt);
318 return process(evt, 'keydown');
319 },
320 keypress: function(evt) {
321 return process(evt, 'keypress');
322 },
323 keyup: function(evt) {
324 modifierState.keyup(evt);
325 return process(evt, 'keyup');
326 },
327 releaseAll: function() { next({type: 'releaseall'}); }
328 };
329 };
330
331 // Keeps track of which keys we (and the server) believe are down
332 // When a keyup is received, match it against this list, to determine the corresponding keysym(s)
333 // in some cases, a single key may produce multiple keysyms, so the corresponding keyup event must release all of these chars
334 // key repeat events should be merged into a single entry.
335 // Because we can't always identify which entry a keydown or keyup event corresponds to, we sometimes have to guess
336 export function TrackKeyState (next) {
337 "use strict";
338 var state = [];
339
340 return function (evt) {
341 var last = state.length !== 0 ? state[state.length-1] : null;
342
343 switch (evt.type) {
344 case 'keydown':
345 // insert a new entry if last seen key was different.
346 if (!last || evt.code === 'Unidentified' || last.code !== evt.code) {
347 last = {code: evt.code, keysyms: {}};
348 state.push(last);
349 }
350 if (evt.keysym) {
351 // make sure last event contains this keysym (a single "logical" keyevent
352 // can cause multiple key events to be sent to the VNC server)
353 last.keysyms[evt.keysym] = evt.keysym;
354 last.ignoreKeyPress = true;
355 next(evt);
356 }
357 break;
358 case 'keypress':
359 if (!last) {
360 last = {code: evt.code, keysyms: {}};
361 state.push(last);
362 }
363 if (!evt.keysym) {
364 console.log('keypress with no keysym:', evt);
365 }
366
367 // If we didn't expect a keypress, and already sent a keydown to the VNC server
368 // based on the keydown, make sure to skip this event.
369 if (evt.keysym && !last.ignoreKeyPress) {
370 last.keysyms[evt.keysym] = evt.keysym;
371 evt.type = 'keydown';
372 next(evt);
373 }
374 break;
375 case 'keyup':
376 if (state.length === 0) {
377 return;
378 }
379 var idx = null;
380 // do we have a matching key tracked as being down?
381 for (var i = 0; i !== state.length; ++i) {
382 if (state[i].code === evt.code) {
383 idx = i;
384 break;
385 }
386 }
387 // if we couldn't find a match (it happens), assume it was the last key pressed
388 if (idx === null) {
389 idx = state.length - 1;
390 }
391
392 var item = state.splice(idx, 1)[0];
393 // for each keysym tracked by this key entry, clone the current event and override the keysym
394 var clone = (function(){
395 function Clone(){}
396 return function (obj) { Clone.prototype=obj; return new Clone(); };
397 }());
398 for (var key in item.keysyms) {
399 var out = clone(evt);
400 out.keysym = item.keysyms[key];
401 next(out);
402 }
403 break;
404 case 'releaseall':
405 /* jshint shadow: true */
406 for (var i = 0; i < state.length; ++i) {
407 for (var key in state[i].keysyms) {
408 var keysym = state[i].keysyms[key];
409 next({code: 'Unidentified', keysym: keysym, type: 'keyup'});
410 }
411 }
412 /* jshint shadow: false */
413 state = [];
414 }
415 };
416 };
417
418 // Handles "escaping" of modifiers: if a char modifier is used to produce a keysym (such as AltGr-2 to generate an @),
419 // then the modifier must be "undone" before sending the @, and "redone" afterwards.
420 export function EscapeModifiers (next) {
421 "use strict";
422 return function(evt) {
423 if (evt.type !== 'keydown' || evt.escape === undefined) {
424 next(evt);
425 return;
426 }
427 // undo modifiers
428 for (var i = 0; i < evt.escape.length; ++i) {
429 next({type: 'keyup', code: 'Unidentified', keysym: evt.escape[i]});
430 }
431 // send the character event
432 next(evt);
433 // redo modifiers
434 /* jshint shadow: true */
435 for (var i = 0; i < evt.escape.length; ++i) {
436 next({type: 'keydown', code: 'Unidentified', keysym: evt.escape[i]});
437 }
438 /* jshint shadow: false */
439 };
440 };