]>
Commit | Line | Data |
---|---|---|
3ae0bb09 SR |
1 | import KeyTable from "./keysym.js"; |
2 | import keysyms from "./keysymdef.js"; | |
3 | ||
6d6f0db0 SR |
4 | function isMac() { |
5 | return navigator && !!(/mac/i).exec(navigator.platform); | |
6 | } | |
7 | function isWindows() { | |
8 | return navigator && !!(/win/i).exec(navigator.platform); | |
9 | } | |
10 | function isLinux() { | |
11 | return navigator && !!(/linux/i).exec(navigator.platform); | |
12 | } | |
13 | ||
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) { | |
16 | var mods = {}; | |
17 | for (var key in currentModifiers) { | |
18 | if (parseInt(key) !== KeyTable.XK_Shift_L) { | |
19 | mods[key] = currentModifiers[key]; | |
20 | } | |
31f169e8 | 21 | } |
466a09f0 | 22 | |
6d6f0db0 SR |
23 | var sum = 0; |
24 | for (var k in currentModifiers) { | |
25 | if (mods[k]) { | |
26 | ++sum; | |
27 | } | |
4ef7566b | 28 | } |
6d6f0db0 SR |
29 | if (hasCharModifier(charModifier, mods)) { |
30 | return sum > charModifier.length; | |
4ef7566b | 31 | } |
6d6f0db0 SR |
32 | else { |
33 | return sum > 0; | |
4ef7566b | 34 | } |
6d6f0db0 | 35 | } |
4ef7566b | 36 | |
6d6f0db0 SR |
37 | // Return true if the specified char modifier is currently down |
38 | export function hasCharModifier(charModifier, currentModifiers) { | |
39 | if (charModifier.length === 0) { return false; } | |
40 | ||
41 | for (var i = 0; i < charModifier.length; ++i) { | |
42 | if (!currentModifiers[charModifier[i]]) { | |
43 | return false; | |
4ef7566b | 44 | } |
6d6f0db0 SR |
45 | } |
46 | return true; | |
47 | } | |
4ef7566b | 48 | |
6d6f0db0 SR |
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) { | |
52 | if (!charModifier) { | |
53 | if (isMac()) { | |
54 | // on Mac, Option (AKA Alt) is used as a char modifier | |
55 | charModifier = [KeyTable.XK_Alt_L]; | |
56 | } | |
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]; | |
4ef7566b | 60 | } |
6d6f0db0 SR |
61 | else if (isLinux()) { |
62 | // on Linux, ISO Level 3 Shift (AltGr) is used as a char modifier | |
63 | charModifier = [KeyTable.XK_ISO_Level3_Shift]; | |
4ef7566b | 64 | } |
65 | else { | |
6d6f0db0 | 66 | charModifier = []; |
4ef7566b | 67 | } |
68 | } | |
69 | ||
6d6f0db0 SR |
70 | var state = {}; |
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; | |
76 | ||
77 | function sync(evt, keysym) { | |
78 | var result = []; | |
79 | function syncKey(keysym) { | |
524d67f2 | 80 | return {keysym: keysym, type: state[keysym] ? 'keydown' : 'keyup'}; |
6d6f0db0 SR |
81 | } |
82 | ||
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)); | |
87 | } | |
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)); | |
92 | } | |
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)); | |
97 | } | |
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)); | |
102 | } | |
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)); | |
107 | } | |
108 | return result; | |
4ef7566b | 109 | } |
6d6f0db0 | 110 | function syncKeyEvent(evt, down) { |
524d67f2 | 111 | var keysym = getKeysym(evt); |
4ef7566b | 112 | |
6d6f0db0 SR |
113 | // first, apply the event itself, if relevant |
114 | if (keysym !== null && state[keysym] !== undefined) { | |
115 | state[keysym] = down; | |
4ef7566b | 116 | } |
6d6f0db0 SR |
117 | return sync(evt, keysym); |
118 | } | |
4ef7566b | 119 | |
6d6f0db0 SR |
120 | return { |
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);}, | |
126 | ||
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; } | |
131 | }; | |
132 | } | |
4ef7566b | 133 | |
6d6f0db0 SR |
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; | |
4ef7566b | 139 | } |
6d6f0db0 SR |
140 | else if ('keyCode' in evt) { |
141 | return evt.keyCode; | |
142 | } | |
143 | else { | |
144 | return evt.key; | |
145 | } | |
146 | } | |
147 | ||
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){ | |
151 | var codepoint; | |
152 | if (evt.char && evt.char.length === 1) { | |
153 | codepoint = evt.char.charCodeAt(); | |
154 | } | |
155 | else if (evt.charCode) { | |
156 | codepoint = evt.charCode; | |
4ef7566b | 157 | } |
6d6f0db0 SR |
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; | |
161 | } | |
162 | if (codepoint) { | |
0a865e15 | 163 | return keysyms.lookup(codepoint); |
6d6f0db0 SR |
164 | } |
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 | |
169 | if (evt.keyCode) { | |
524d67f2 | 170 | return keysymFromKeyCode(evt.keyCode, evt.shiftKey); |
6d6f0db0 SR |
171 | } |
172 | if (evt.which) { | |
524d67f2 | 173 | return keysymFromKeyCode(evt.which, evt.shiftKey); |
6d6f0db0 SR |
174 | } |
175 | return null; | |
176 | } | |
4ef7566b | 177 | |
6d6f0db0 SR |
178 | // Given a keycode, try to predict which keysym it might be. |
179 | // If the keycode is unknown, null is returned. | |
a5c8a755 | 180 | function keysymFromKeyCode(keycode, shiftPressed) { |
6d6f0db0 | 181 | if (typeof(keycode) !== 'number') { |
4ef7566b | 182 | return null; |
183 | } | |
6d6f0db0 SR |
184 | // won't be accurate for azerty |
185 | if (keycode >= 0x30 && keycode <= 0x39) { | |
186 | return keycode; // digit | |
187 | } | |
188 | if (keycode >= 0x41 && keycode <= 0x5a) { | |
189 | // remap to lowercase unless shift is down | |
190 | return shiftPressed ? keycode : keycode + 32; // A-Z | |
191 | } | |
192 | if (keycode >= 0x60 && keycode <= 0x69) { | |
193 | return KeyTable.XK_KP_0 + (keycode - 0x60); // numpad 0-9 | |
194 | } | |
4ef7566b | 195 | |
6d6f0db0 SR |
196 | switch(keycode) { |
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; | |
4ef7566b | 208 | } |
209 | ||
6d6f0db0 SR |
210 | return nonCharacterKey({keyCode: keycode}); |
211 | } | |
4ef7566b | 212 | |
6d6f0db0 SR |
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 | |
a5c8a755 | 215 | function nonCharacterKey(evt) { |
6d6f0db0 SR |
216 | // evt.key not implemented yet |
217 | if (!evt.keyCode) { return null; } | |
218 | var keycode = evt.keyCode; | |
4ef7566b | 219 | |
6d6f0db0 SR |
220 | if (keycode >= 0x70 && keycode <= 0x87) { |
221 | return KeyTable.XK_F1 + keycode - 0x70; // F1-F24 | |
222 | } | |
223 | switch (keycode) { | |
282834ca | 224 | |
6d6f0db0 SR |
225 | case 8 : return KeyTable.XK_BackSpace; |
226 | case 13 : return KeyTable.XK_Return; | |
4ef7566b | 227 | |
6d6f0db0 | 228 | case 9 : return KeyTable.XK_Tab; |
4ef7566b | 229 | |
6d6f0db0 SR |
230 | case 27 : return KeyTable.XK_Escape; |
231 | case 46 : return KeyTable.XK_Delete; | |
4ef7566b | 232 | |
6d6f0db0 SR |
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; | |
4ef7566b | 238 | |
6d6f0db0 SR |
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; | |
4ef7566b | 243 | |
6d6f0db0 SR |
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 | |
247 | ||
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; | |
4ef7566b | 254 | } |
6d6f0db0 | 255 | } |
ae510306 | 256 | |
6d6f0db0 | 257 | export function QEMUKeyEventDecoder (modifierState, next) { |
49637e43 DHB |
258 | "use strict"; |
259 | ||
260 | function sendAll(evts) { | |
261 | for (var i = 0; i < evts.length; ++i) { | |
262 | next(evts[i]); | |
263 | } | |
264 | } | |
265 | ||
266 | var numPadCodes = ["Numpad0", "Numpad1", "Numpad2", | |
267 | "Numpad3", "Numpad4", "Numpad5", "Numpad6", | |
268 | "Numpad7", "Numpad8", "Numpad9", "NumpadDecimal"]; | |
269 | ||
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 | |
275 | }; | |
276 | ||
277 | var numLockOnKeyCodes = [96, 97, 98, 99, 100, 101, 102, | |
278 | 103, 104, 105, 108, 110]; | |
279 | ||
280 | function isNumPadMultiKey(evt) { | |
281 | return (numPadCodes.indexOf(evt.code) !== -1); | |
282 | } | |
283 | ||
284 | function getNumPadKeySym(evt) { | |
285 | if (numLockOnKeyCodes.indexOf(evt.keyCode) !== -1) { | |
286 | return numLockOnKeySyms[evt.code]; | |
287 | } | |
288 | return 0; | |
289 | } | |
290 | ||
291 | function process(evt, type) { | |
292 | var result = {type: type}; | |
293 | result.code = evt.code; | |
294 | result.keysym = 0; | |
295 | ||
296 | if (isNumPadMultiKey(evt)) { | |
297 | result.keysym = getNumPadKeySym(evt); | |
298 | } | |
299 | ||
300 | var hasModifier = modifierState.hasShortcutModifier() || !!modifierState.activeCharModifier(); | |
301 | var isShift = evt.keyCode === 0x10 || evt.key === 'Shift'; | |
302 | ||
6d6f0db0 | 303 | var suppress = !isShift && (type !== 'keydown' || modifierState.hasShortcutModifier() || !!nonCharacterKey(evt)); |
49637e43 DHB |
304 | |
305 | next(result); | |
306 | return suppress; | |
307 | } | |
308 | return { | |
309 | keydown: function(evt) { | |
310 | sendAll(modifierState.keydown(evt)); | |
311 | return process(evt, 'keydown'); | |
312 | }, | |
313 | keypress: function(evt) { | |
314 | return true; | |
315 | }, | |
316 | keyup: function(evt) { | |
317 | sendAll(modifierState.keyup(evt)); | |
318 | return process(evt, 'keyup'); | |
319 | }, | |
320 | syncModifiers: function(evt) { | |
321 | sendAll(modifierState.syncAny(evt)); | |
322 | }, | |
323 | releaseAll: function() { next({type: 'releaseall'}); } | |
324 | }; | |
ae510306 | 325 | }; |
49637e43 | 326 | |
6d6f0db0 | 327 | export function TrackQEMUKeyState (next) { |
49637e43 DHB |
328 | "use strict"; |
329 | var state = []; | |
330 | ||
331 | return function (evt) { | |
332 | var last = state.length !== 0 ? state[state.length-1] : null; | |
333 | ||
334 | switch (evt.type) { | |
335 | case 'keydown': | |
336 | ||
337 | if (!last || last.code !== evt.code) { | |
338 | last = {code: evt.code}; | |
339 | ||
340 | if (state.length > 0 && state[state.length-1].code == 'ControlLeft') { | |
341 | if (evt.code !== 'AltRight') { | |
342 | next({code: 'ControlLeft', type: 'keydown', keysym: 0}); | |
343 | } else { | |
344 | state.pop(); | |
345 | } | |
346 | } | |
347 | state.push(last); | |
348 | } | |
349 | if (evt.code !== 'ControlLeft') { | |
350 | next(evt); | |
351 | } | |
352 | break; | |
353 | ||
354 | case 'keyup': | |
355 | if (state.length === 0) { | |
356 | return; | |
357 | } | |
358 | var idx = null; | |
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) { | |
362 | idx = i; | |
363 | break; | |
364 | } | |
365 | } | |
366 | // if we couldn't find a match (it happens), assume it was the last key pressed | |
367 | if (idx === null) { | |
368 | if (evt.code === 'ControlLeft') { | |
369 | return; | |
370 | } | |
371 | idx = state.length - 1; | |
372 | } | |
373 | ||
374 | state.splice(idx, 1); | |
375 | next(evt); | |
376 | break; | |
377 | case 'releaseall': | |
378 | /* jshint shadow: true */ | |
379 | for (var i = 0; i < state.length; ++i) { | |
380 | next({code: state[i].code, keysym: 0, type: 'keyup'}); | |
381 | } | |
382 | /* jshint shadow: false */ | |
383 | state = []; | |
384 | } | |
385 | }; | |
ae510306 | 386 | }; |
49637e43 | 387 | |
4ef7566b | 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) | |
6d6f0db0 | 395 | export function KeyEventDecoder (modifierState, next) { |
4ef7566b | 396 | "use strict"; |
397 | function sendAll(evts) { | |
398 | for (var i = 0; i < evts.length; ++i) { | |
399 | next(evts[i]); | |
400 | } | |
401 | } | |
402 | function process(evt, type) { | |
403 | var result = {type: type}; | |
6d6f0db0 | 404 | var keyId = getKey(evt); |
4ef7566b | 405 | if (keyId) { |
406 | result.keyId = keyId; | |
407 | } | |
408 | ||
6d6f0db0 | 409 | var keysym = getKeysym(evt); |
4ef7566b | 410 | |
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 | |
6d6f0db0 | 415 | if (keysym && (type !== 'keydown' || nonCharacterKey(evt) || hasModifier)) { |
4ef7566b | 416 | result.keysym = keysym; |
417 | } | |
418 | ||
419 | var isShift = evt.keyCode === 0x10 || evt.key === 'Shift'; | |
420 | ||
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. | |
6d6f0db0 | 424 | var suppress = !isShift && (type !== 'keydown' || modifierState.hasShortcutModifier() || !!nonCharacterKey(evt)); |
4ef7566b | 425 | |
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 | |
6d6f0db0 | 428 | var stall = type === 'keydown' && modifierState.activeCharModifier() && !nonCharacterKey(evt); |
4ef7566b | 429 | |
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(); | |
432 | ||
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) { | |
524d67f2 | 439 | if (active[i] === keysym) { |
4ef7566b | 440 | isCharModifier = true; |
441 | } | |
442 | } | |
443 | if (type === 'keypress' && !isCharModifier) { | |
444 | result.escape = modifierState.activeCharModifier(); | |
445 | } | |
446 | } | |
447 | ||
448 | if (stall) { | |
449 | // insert a fake "stall" event | |
450 | next({type: 'stall'}); | |
451 | } | |
452 | next(result); | |
453 | ||
454 | return suppress; | |
455 | } | |
456 | ||
457 | return { | |
458 | keydown: function(evt) { | |
459 | sendAll(modifierState.keydown(evt)); | |
460 | return process(evt, 'keydown'); | |
461 | }, | |
462 | keypress: function(evt) { | |
463 | return process(evt, 'keypress'); | |
464 | }, | |
465 | keyup: function(evt) { | |
466 | sendAll(modifierState.keyup(evt)); | |
467 | return process(evt, 'keyup'); | |
468 | }, | |
469 | syncModifiers: function(evt) { | |
470 | sendAll(modifierState.syncAny(evt)); | |
471 | }, | |
472 | releaseAll: function() { next({type: 'releaseall'}); } | |
473 | }; | |
ae510306 | 474 | }; |
4ef7566b | 475 | |
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 | |
6d6f0db0 | 482 | export function VerifyCharModifier (next) { |
4ef7566b | 483 | "use strict"; |
484 | var queue = []; | |
485 | var timer = null; | |
486 | function process() { | |
487 | if (timer) { | |
488 | return; | |
489 | } | |
31f169e8 SR |
490 | |
491 | var delayProcess = function () { | |
492 | clearTimeout(timer); | |
493 | timer = null; | |
494 | process(); | |
495 | }; | |
496 | ||
4ef7566b | 497 | while (queue.length !== 0) { |
498 | var cur = queue[0]; | |
499 | queue = queue.splice(1); | |
500 | switch (cur.type) { | |
501 | case 'stall': | |
502 | // insert a delay before processing available events. | |
31f169e8 SR |
503 | /* jshint loopfunc: true */ |
504 | timer = setTimeout(delayProcess, 5); | |
505 | /* jshint loopfunc: false */ | |
4ef7566b | 506 | return; |
507 | case 'keydown': | |
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 | |
524d67f2 | 513 | if (queue[0].escape && (!cur.keysym || cur.keysym !== queue[0].keysym)) { |
4ef7566b | 514 | cur.escape = queue[0].escape; |
515 | } | |
516 | cur.keysym = queue[0].keysym; | |
517 | queue = queue.splice(1); | |
518 | } | |
519 | break; | |
520 | } | |
521 | ||
522 | // swallow stall events, and pass all others to the next stage | |
523 | if (cur.type !== 'stall') { | |
524 | next(cur); | |
525 | } | |
526 | } | |
527 | } | |
528 | return function(evt) { | |
529 | queue.push(evt); | |
530 | process(); | |
531 | }; | |
ae510306 | 532 | }; |
4ef7566b | 533 | |
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 | |
6d6f0db0 | 539 | export function TrackKeyState (next) { |
4ef7566b | 540 | "use strict"; |
541 | var state = []; | |
542 | ||
543 | return function (evt) { | |
544 | var last = state.length !== 0 ? state[state.length-1] : null; | |
545 | ||
546 | switch (evt.type) { | |
547 | case 'keydown': | |
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: {}}; | |
551 | state.push(last); | |
552 | } | |
553 | if (evt.keysym) { | |
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) | |
524d67f2 | 556 | last.keysyms[evt.keysym] = evt.keysym; |
4ef7566b | 557 | last.ignoreKeyPress = true; |
558 | next(evt); | |
559 | } | |
560 | break; | |
561 | case 'keypress': | |
562 | if (!last) { | |
563 | last = {keyId: evt.keyId, keysyms: {}}; | |
564 | state.push(last); | |
565 | } | |
566 | if (!evt.keysym) { | |
567 | console.log('keypress with no keysym:', evt); | |
568 | } | |
569 | ||
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) { | |
524d67f2 | 573 | last.keysyms[evt.keysym] = evt.keysym; |
4ef7566b | 574 | evt.type = 'keydown'; |
575 | next(evt); | |
576 | } | |
577 | break; | |
578 | case 'keyup': | |
579 | if (state.length === 0) { | |
580 | return; | |
581 | } | |
582 | var idx = null; | |
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) { | |
586 | idx = i; | |
587 | break; | |
588 | } | |
589 | } | |
590 | // if we couldn't find a match (it happens), assume it was the last key pressed | |
591 | if (idx === null) { | |
592 | idx = state.length - 1; | |
593 | } | |
594 | ||
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 | |
31f169e8 SR |
597 | var clone = (function(){ |
598 | function Clone(){} | |
599 | return function (obj) { Clone.prototype=obj; return new Clone(); }; | |
600 | }()); | |
4ef7566b | 601 | for (var key in item.keysyms) { |
4ef7566b | 602 | var out = clone(evt); |
603 | out.keysym = item.keysyms[key]; | |
604 | next(out); | |
605 | } | |
606 | break; | |
607 | case 'releaseall': | |
31f169e8 | 608 | /* jshint shadow: true */ |
4ef7566b | 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'}); | |
613 | } | |
614 | } | |
31f169e8 | 615 | /* jshint shadow: false */ |
4ef7566b | 616 | state = []; |
617 | } | |
618 | }; | |
ae510306 | 619 | }; |
4ef7566b | 620 | |
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. | |
6d6f0db0 | 623 | export function EscapeModifiers (next) { |
4ef7566b | 624 | "use strict"; |
625 | return function(evt) { | |
626 | if (evt.type !== 'keydown' || evt.escape === undefined) { | |
627 | next(evt); | |
628 | return; | |
629 | } | |
630 | // undo modifiers | |
631 | for (var i = 0; i < evt.escape.length; ++i) { | |
524d67f2 | 632 | next({type: 'keyup', keyId: 0, keysym: evt.escape[i]}); |
4ef7566b | 633 | } |
634 | // send the character event | |
635 | next(evt); | |
636 | // redo modifiers | |
31f169e8 | 637 | /* jshint shadow: true */ |
4ef7566b | 638 | for (var i = 0; i < evt.escape.length; ++i) { |
524d67f2 | 639 | next({type: 'keydown', keyId: 0, keysym: evt.escape[i]}); |
4ef7566b | 640 | } |
31f169e8 | 641 | /* jshint shadow: false */ |
4ef7566b | 642 | }; |
ae510306 | 643 | }; |