]>
Commit | Line | Data |
---|---|---|
ae510306 SR |
1 | /* [module] |
2 | * import KeyTable from "./keysym"; | |
3 | * import keysyms from "./keysymdef"; | |
4 | */ | |
5 | ||
6 | var KeyboardUtil = {}; | |
7 | ||
8 | (function() { | |
4ef7566b | 9 | "use strict"; |
10 | ||
466a09f0 | 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 | |
14 | var substitutions = { | |
15 | // {S,s} with comma below -> {S,s} with cedilla | |
16 | 0x218 : 0x15e, | |
17 | 0x219 : 0x15f, | |
18 | // {T,t} with comma below -> {T,t} with cedilla | |
19 | 0x21a : 0x162, | |
20 | 0x21b : 0x163 | |
21 | }; | |
22 | ||
23 | var sub = substitutions[cp]; | |
24 | return sub ? sub : cp; | |
31f169e8 | 25 | } |
466a09f0 | 26 | |
4ef7566b | 27 | function isMac() { |
23078406 | 28 | return navigator && !!(/mac/i).exec(navigator.platform); |
4ef7566b | 29 | } |
30 | function isWindows() { | |
23078406 | 31 | return navigator && !!(/win/i).exec(navigator.platform); |
4ef7566b | 32 | } |
33 | function isLinux() { | |
23078406 | 34 | return navigator && !!(/linux/i).exec(navigator.platform); |
4ef7566b | 35 | } |
36 | ||
37 | // Return true if a modifier which is not the specified char modifier (and is not shift) is down | |
38 | function hasShortcutModifier(charModifier, currentModifiers) { | |
39 | var mods = {}; | |
40 | for (var key in currentModifiers) { | |
ae510306 | 41 | if (parseInt(key) !== KeyTable.XK_Shift_L) { |
4ef7566b | 42 | mods[key] = currentModifiers[key]; |
43 | } | |
44 | } | |
45 | ||
46 | var sum = 0; | |
47 | for (var k in currentModifiers) { | |
48 | if (mods[k]) { | |
49 | ++sum; | |
50 | } | |
51 | } | |
52 | if (hasCharModifier(charModifier, mods)) { | |
53 | return sum > charModifier.length; | |
54 | } | |
55 | else { | |
56 | return sum > 0; | |
57 | } | |
58 | } | |
59 | ||
60 | // Return true if the specified char modifier is currently down | |
61 | function hasCharModifier(charModifier, currentModifiers) { | |
62 | if (charModifier.length === 0) { return false; } | |
63 | ||
64 | for (var i = 0; i < charModifier.length; ++i) { | |
65 | if (!currentModifiers[charModifier[i]]) { | |
66 | return false; | |
67 | } | |
68 | } | |
69 | return true; | |
70 | } | |
71 | ||
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) { | |
4ef7566b | 75 | if (!charModifier) { |
76 | if (isMac()) { | |
77 | // on Mac, Option (AKA Alt) is used as a char modifier | |
ae510306 | 78 | charModifier = [KeyTable.XK_Alt_L]; |
4ef7566b | 79 | } |
80 | else if (isWindows()) { | |
81 | // on Windows, Ctrl+Alt is used as a char modifier | |
ae510306 | 82 | charModifier = [KeyTable.XK_Alt_L, KeyTable.XK_Control_L]; |
4ef7566b | 83 | } |
84 | else if (isLinux()) { | |
282834ca | 85 | // on Linux, ISO Level 3 Shift (AltGr) is used as a char modifier |
ae510306 | 86 | charModifier = [KeyTable.XK_ISO_Level3_Shift]; |
4ef7566b | 87 | } |
88 | else { | |
89 | charModifier = []; | |
90 | } | |
91 | } | |
92 | ||
93 | var state = {}; | |
ae510306 SR |
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; | |
4ef7566b | 99 | |
100 | function sync(evt, keysym) { | |
101 | var result = []; | |
102 | function syncKey(keysym) { | |
103 | return {keysym: keysyms.lookup(keysym), type: state[keysym] ? 'keydown' : 'keyup'}; | |
104 | } | |
105 | ||
282834ca | 106 | if (evt.ctrlKey !== undefined && |
ae510306 SR |
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)); | |
4ef7566b | 110 | } |
282834ca | 111 | if (evt.altKey !== undefined && |
ae510306 SR |
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)); | |
4ef7566b | 115 | } |
282834ca | 116 | if (evt.altGraphKey !== undefined && |
ae510306 SR |
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)); | |
4ef7566b | 120 | } |
282834ca | 121 | if (evt.shiftKey !== undefined && |
ae510306 SR |
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)); | |
4ef7566b | 125 | } |
282834ca | 126 | if (evt.metaKey !== undefined && |
ae510306 SR |
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)); | |
4ef7566b | 130 | } |
131 | return result; | |
132 | } | |
133 | function syncKeyEvent(evt, down) { | |
134 | var obj = getKeysym(evt); | |
135 | var keysym = obj ? obj.keysym : null; | |
136 | ||
137 | // first, apply the event itself, if relevant | |
138 | if (keysym !== null && state[keysym] !== undefined) { | |
139 | state[keysym] = down; | |
140 | } | |
141 | return sync(evt, keysym); | |
142 | } | |
143 | ||
144 | return { | |
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);}, | |
150 | ||
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; } | |
155 | }; | |
156 | } | |
157 | ||
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){ | |
c3f60524 JD |
161 | if ('keyCode' in evt && 'key' in evt) { |
162 | return evt.key + ':' + evt.keyCode; | |
4ef7566b | 163 | } |
c3f60524 | 164 | else if ('keyCode' in evt) { |
4ef7566b | 165 | return evt.keyCode; |
166 | } | |
c3f60524 JD |
167 | else { |
168 | return evt.key; | |
169 | } | |
4ef7566b | 170 | } |
171 | ||
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){ | |
175 | var codepoint; | |
176 | if (evt.char && evt.char.length === 1) { | |
177 | codepoint = evt.char.charCodeAt(); | |
178 | } | |
179 | else if (evt.charCode) { | |
180 | codepoint = evt.charCode; | |
181 | } | |
60a415ae | 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; | |
185 | } | |
4ef7566b | 186 | if (codepoint) { |
115eedf6 | 187 | return keysyms.fromUnicode(substituteCodepoint(codepoint)); |
4ef7566b | 188 | } |
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 | |
193 | if (evt.keyCode) { | |
194 | return keysyms.lookup(keysymFromKeyCode(evt.keyCode, evt.shiftKey)); | |
195 | } | |
196 | if (evt.which) { | |
197 | return keysyms.lookup(keysymFromKeyCode(evt.which, evt.shiftKey)); | |
198 | } | |
199 | return null; | |
200 | } | |
201 | ||
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') { | |
206 | return null; | |
207 | } | |
208 | // won't be accurate for azerty | |
209 | if (keycode >= 0x30 && keycode <= 0x39) { | |
210 | return keycode; // digit | |
211 | } | |
212 | if (keycode >= 0x41 && keycode <= 0x5a) { | |
213 | // remap to lowercase unless shift is down | |
214 | return shiftPressed ? keycode : keycode + 32; // A-Z | |
215 | } | |
216 | if (keycode >= 0x60 && keycode <= 0x69) { | |
ae510306 | 217 | return KeyTable.XK_KP_0 + (keycode - 0x60); // numpad 0-9 |
4ef7566b | 218 | } |
219 | ||
220 | switch(keycode) { | |
ae510306 SR |
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; | |
4ef7566b | 232 | } |
233 | ||
234 | return nonCharacterKey({keyCode: keycode}); | |
235 | } | |
236 | ||
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; | |
243 | ||
244 | if (keycode >= 0x70 && keycode <= 0x87) { | |
ae510306 | 245 | return KeyTable.XK_F1 + keycode - 0x70; // F1-F24 |
4ef7566b | 246 | } |
247 | switch (keycode) { | |
248 | ||
ae510306 SR |
249 | case 8 : return KeyTable.XK_BackSpace; |
250 | case 13 : return KeyTable.XK_Return; | |
282834ca | 251 | |
ae510306 | 252 | case 9 : return KeyTable.XK_Tab; |
4ef7566b | 253 | |
ae510306 SR |
254 | case 27 : return KeyTable.XK_Escape; |
255 | case 46 : return KeyTable.XK_Delete; | |
4ef7566b | 256 | |
ae510306 SR |
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; | |
4ef7566b | 262 | |
ae510306 SR |
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; | |
4ef7566b | 267 | |
ae510306 SR |
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 | |
4ef7566b | 271 | |
ae510306 SR |
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 | |
4ef7566b | 277 | default: return null; |
278 | } | |
279 | } | |
ae510306 SR |
280 | |
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; | |
4ef7566b | 289 | })(); |
290 | ||
ae510306 | 291 | KeyboardUtil.QEMUKeyEventDecoder = function(modifierState, next) { |
49637e43 DHB |
292 | "use strict"; |
293 | ||
294 | function sendAll(evts) { | |
295 | for (var i = 0; i < evts.length; ++i) { | |
296 | next(evts[i]); | |
297 | } | |
298 | } | |
299 | ||
300 | var numPadCodes = ["Numpad0", "Numpad1", "Numpad2", | |
301 | "Numpad3", "Numpad4", "Numpad5", "Numpad6", | |
302 | "Numpad7", "Numpad8", "Numpad9", "NumpadDecimal"]; | |
303 | ||
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 | |
309 | }; | |
310 | ||
311 | var numLockOnKeyCodes = [96, 97, 98, 99, 100, 101, 102, | |
312 | 103, 104, 105, 108, 110]; | |
313 | ||
314 | function isNumPadMultiKey(evt) { | |
315 | return (numPadCodes.indexOf(evt.code) !== -1); | |
316 | } | |
317 | ||
318 | function getNumPadKeySym(evt) { | |
319 | if (numLockOnKeyCodes.indexOf(evt.keyCode) !== -1) { | |
320 | return numLockOnKeySyms[evt.code]; | |
321 | } | |
322 | return 0; | |
323 | } | |
324 | ||
325 | function process(evt, type) { | |
326 | var result = {type: type}; | |
327 | result.code = evt.code; | |
328 | result.keysym = 0; | |
329 | ||
330 | if (isNumPadMultiKey(evt)) { | |
331 | result.keysym = getNumPadKeySym(evt); | |
332 | } | |
333 | ||
334 | var hasModifier = modifierState.hasShortcutModifier() || !!modifierState.activeCharModifier(); | |
335 | var isShift = evt.keyCode === 0x10 || evt.key === 'Shift'; | |
336 | ||
ae510306 | 337 | var suppress = !isShift && (type !== 'keydown' || modifierState.hasShortcutModifier() || !!KeyboardUtil.nonCharacterKey(evt)); |
49637e43 DHB |
338 | |
339 | next(result); | |
340 | return suppress; | |
341 | } | |
342 | return { | |
343 | keydown: function(evt) { | |
344 | sendAll(modifierState.keydown(evt)); | |
345 | return process(evt, 'keydown'); | |
346 | }, | |
347 | keypress: function(evt) { | |
348 | return true; | |
349 | }, | |
350 | keyup: function(evt) { | |
351 | sendAll(modifierState.keyup(evt)); | |
352 | return process(evt, 'keyup'); | |
353 | }, | |
354 | syncModifiers: function(evt) { | |
355 | sendAll(modifierState.syncAny(evt)); | |
356 | }, | |
357 | releaseAll: function() { next({type: 'releaseall'}); } | |
358 | }; | |
ae510306 | 359 | }; |
49637e43 | 360 | |
ae510306 | 361 | KeyboardUtil.TrackQEMUKeyState = function(next) { |
49637e43 DHB |
362 | "use strict"; |
363 | var state = []; | |
364 | ||
365 | return function (evt) { | |
366 | var last = state.length !== 0 ? state[state.length-1] : null; | |
367 | ||
368 | switch (evt.type) { | |
369 | case 'keydown': | |
370 | ||
371 | if (!last || last.code !== evt.code) { | |
372 | last = {code: evt.code}; | |
373 | ||
374 | if (state.length > 0 && state[state.length-1].code == 'ControlLeft') { | |
375 | if (evt.code !== 'AltRight') { | |
376 | next({code: 'ControlLeft', type: 'keydown', keysym: 0}); | |
377 | } else { | |
378 | state.pop(); | |
379 | } | |
380 | } | |
381 | state.push(last); | |
382 | } | |
383 | if (evt.code !== 'ControlLeft') { | |
384 | next(evt); | |
385 | } | |
386 | break; | |
387 | ||
388 | case 'keyup': | |
389 | if (state.length === 0) { | |
390 | return; | |
391 | } | |
392 | var idx = null; | |
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) { | |
396 | idx = i; | |
397 | break; | |
398 | } | |
399 | } | |
400 | // if we couldn't find a match (it happens), assume it was the last key pressed | |
401 | if (idx === null) { | |
402 | if (evt.code === 'ControlLeft') { | |
403 | return; | |
404 | } | |
405 | idx = state.length - 1; | |
406 | } | |
407 | ||
408 | state.splice(idx, 1); | |
409 | next(evt); | |
410 | break; | |
411 | case 'releaseall': | |
412 | /* jshint shadow: true */ | |
413 | for (var i = 0; i < state.length; ++i) { | |
414 | next({code: state[i].code, keysym: 0, type: 'keyup'}); | |
415 | } | |
416 | /* jshint shadow: false */ | |
417 | state = []; | |
418 | } | |
419 | }; | |
ae510306 | 420 | }; |
49637e43 | 421 | |
4ef7566b | 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) | |
ae510306 | 429 | KeyboardUtil.KeyEventDecoder = function(modifierState, next) { |
4ef7566b | 430 | "use strict"; |
431 | function sendAll(evts) { | |
432 | for (var i = 0; i < evts.length; ++i) { | |
433 | next(evts[i]); | |
434 | } | |
435 | } | |
436 | function process(evt, type) { | |
437 | var result = {type: type}; | |
ae510306 | 438 | var keyId = KeyboardUtil.getKey(evt); |
4ef7566b | 439 | if (keyId) { |
440 | result.keyId = keyId; | |
441 | } | |
442 | ||
ae510306 | 443 | var keysym = KeyboardUtil.getKeysym(evt); |
4ef7566b | 444 | |
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 | |
ae510306 | 449 | if (keysym && (type !== 'keydown' || KeyboardUtil.nonCharacterKey(evt) || hasModifier)) { |
4ef7566b | 450 | result.keysym = keysym; |
451 | } | |
452 | ||
453 | var isShift = evt.keyCode === 0x10 || evt.key === 'Shift'; | |
454 | ||
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. | |
ae510306 | 458 | var suppress = !isShift && (type !== 'keydown' || modifierState.hasShortcutModifier() || !!KeyboardUtil.nonCharacterKey(evt)); |
4ef7566b | 459 | |
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 | |
ae510306 | 462 | var stall = type === 'keydown' && modifierState.activeCharModifier() && !KeyboardUtil.nonCharacterKey(evt); |
4ef7566b | 463 | |
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(); | |
466 | ||
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; | |
475 | } | |
476 | } | |
477 | if (type === 'keypress' && !isCharModifier) { | |
478 | result.escape = modifierState.activeCharModifier(); | |
479 | } | |
480 | } | |
481 | ||
482 | if (stall) { | |
483 | // insert a fake "stall" event | |
484 | next({type: 'stall'}); | |
485 | } | |
486 | next(result); | |
487 | ||
488 | return suppress; | |
489 | } | |
490 | ||
491 | return { | |
492 | keydown: function(evt) { | |
493 | sendAll(modifierState.keydown(evt)); | |
494 | return process(evt, 'keydown'); | |
495 | }, | |
496 | keypress: function(evt) { | |
497 | return process(evt, 'keypress'); | |
498 | }, | |
499 | keyup: function(evt) { | |
500 | sendAll(modifierState.keyup(evt)); | |
501 | return process(evt, 'keyup'); | |
502 | }, | |
503 | syncModifiers: function(evt) { | |
504 | sendAll(modifierState.syncAny(evt)); | |
505 | }, | |
506 | releaseAll: function() { next({type: 'releaseall'}); } | |
507 | }; | |
ae510306 | 508 | }; |
4ef7566b | 509 | |
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 | |
ae510306 | 516 | KeyboardUtil.VerifyCharModifier = function(next) { |
4ef7566b | 517 | "use strict"; |
518 | var queue = []; | |
519 | var timer = null; | |
520 | function process() { | |
521 | if (timer) { | |
522 | return; | |
523 | } | |
31f169e8 SR |
524 | |
525 | var delayProcess = function () { | |
526 | clearTimeout(timer); | |
527 | timer = null; | |
528 | process(); | |
529 | }; | |
530 | ||
4ef7566b | 531 | while (queue.length !== 0) { |
532 | var cur = queue[0]; | |
533 | queue = queue.splice(1); | |
534 | switch (cur.type) { | |
535 | case 'stall': | |
536 | // insert a delay before processing available events. | |
31f169e8 SR |
537 | /* jshint loopfunc: true */ |
538 | timer = setTimeout(delayProcess, 5); | |
539 | /* jshint loopfunc: false */ | |
4ef7566b | 540 | return; |
541 | case 'keydown': | |
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; | |
549 | } | |
550 | cur.keysym = queue[0].keysym; | |
551 | queue = queue.splice(1); | |
552 | } | |
553 | break; | |
554 | } | |
555 | ||
556 | // swallow stall events, and pass all others to the next stage | |
557 | if (cur.type !== 'stall') { | |
558 | next(cur); | |
559 | } | |
560 | } | |
561 | } | |
562 | return function(evt) { | |
563 | queue.push(evt); | |
564 | process(); | |
565 | }; | |
ae510306 | 566 | }; |
4ef7566b | 567 | |
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 | |
ae510306 | 573 | KeyboardUtil.TrackKeyState = function(next) { |
4ef7566b | 574 | "use strict"; |
575 | var state = []; | |
576 | ||
577 | return function (evt) { | |
578 | var last = state.length !== 0 ? state[state.length-1] : null; | |
579 | ||
580 | switch (evt.type) { | |
581 | case 'keydown': | |
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: {}}; | |
585 | state.push(last); | |
586 | } | |
587 | if (evt.keysym) { | |
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; | |
592 | next(evt); | |
593 | } | |
594 | break; | |
595 | case 'keypress': | |
596 | if (!last) { | |
597 | last = {keyId: evt.keyId, keysyms: {}}; | |
598 | state.push(last); | |
599 | } | |
600 | if (!evt.keysym) { | |
601 | console.log('keypress with no keysym:', evt); | |
602 | } | |
603 | ||
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'; | |
609 | next(evt); | |
610 | } | |
611 | break; | |
612 | case 'keyup': | |
613 | if (state.length === 0) { | |
614 | return; | |
615 | } | |
616 | var idx = null; | |
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) { | |
620 | idx = i; | |
621 | break; | |
622 | } | |
623 | } | |
624 | // if we couldn't find a match (it happens), assume it was the last key pressed | |
625 | if (idx === null) { | |
626 | idx = state.length - 1; | |
627 | } | |
628 | ||
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 | |
31f169e8 SR |
631 | var clone = (function(){ |
632 | function Clone(){} | |
633 | return function (obj) { Clone.prototype=obj; return new Clone(); }; | |
634 | }()); | |
4ef7566b | 635 | for (var key in item.keysyms) { |
4ef7566b | 636 | var out = clone(evt); |
637 | out.keysym = item.keysyms[key]; | |
638 | next(out); | |
639 | } | |
640 | break; | |
641 | case 'releaseall': | |
31f169e8 | 642 | /* jshint shadow: true */ |
4ef7566b | 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'}); | |
647 | } | |
648 | } | |
31f169e8 | 649 | /* jshint shadow: false */ |
4ef7566b | 650 | state = []; |
651 | } | |
652 | }; | |
ae510306 | 653 | }; |
4ef7566b | 654 | |
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. | |
ae510306 | 657 | KeyboardUtil.EscapeModifiers = function(next) { |
4ef7566b | 658 | "use strict"; |
659 | return function(evt) { | |
660 | if (evt.type !== 'keydown' || evt.escape === undefined) { | |
661 | next(evt); | |
662 | return; | |
663 | } | |
664 | // undo modifiers | |
665 | for (var i = 0; i < evt.escape.length; ++i) { | |
666 | next({type: 'keyup', keyId: 0, keysym: keysyms.lookup(evt.escape[i])}); | |
667 | } | |
668 | // send the character event | |
669 | next(evt); | |
670 | // redo modifiers | |
31f169e8 | 671 | /* jshint shadow: true */ |
4ef7566b | 672 | for (var i = 0; i < evt.escape.length; ++i) { |
673 | next({type: 'keydown', keyId: 0, keysym: keysyms.lookup(evt.escape[i])}); | |
674 | } | |
31f169e8 | 675 | /* jshint shadow: false */ |
4ef7566b | 676 | }; |
ae510306 SR |
677 | }; |
678 | ||
679 | /* [module] export default KeyboardUtil; */ |