]>
Commit | Line | Data |
---|---|---|
3ae0bb09 SR |
1 | import KeyTable from "./keysym.js"; |
2 | import keysyms from "./keysymdef.js"; | |
80cb8ffd | 3 | import vkeys from "./vkeys.js"; |
3ae0bb09 | 4 | |
6d6f0db0 SR |
5 | function isMac() { |
6 | return navigator && !!(/mac/i).exec(navigator.platform); | |
7 | } | |
8 | function isWindows() { | |
9 | return navigator && !!(/win/i).exec(navigator.platform); | |
10 | } | |
11 | function 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 | |
16 | export 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 |
39 | export 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 | |
52 | export 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 |
136 | export 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 | |
197 | export 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 | 227 | function 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 | 262 | function 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 | 304 | export 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 | 374 | export 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 | 442 | export 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 | 535 | export 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 | 592 | export 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 | 676 | export 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 | }; |