1 /*
  2  * Copyright (C) 2013 Glyptodon LLC
  3  *
  4  * Permission is hereby granted, free of charge, to any person obtaining a copy
  5  * of this software and associated documentation files (the "Software"), to deal
  6  * in the Software without restriction, including without limitation the rights
  7  * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  8  * copies of the Software, and to permit persons to whom the Software is
  9  * furnished to do so, subject to the following conditions:
 10  *
 11  * The above copyright notice and this permission notice shall be included in
 12  * all copies or substantial portions of the Software.
 13  *
 14  * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 15  * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 16  * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 17  * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 18  * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 19  * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 20  * THE SOFTWARE.
 21  */
 22 
 23 var Guacamole = Guacamole || {};
 24 
 25 /**
 26  * Provides cross-browser and cross-keyboard keyboard for a specific element.
 27  * Browser and keyboard layout variation is abstracted away, providing events
 28  * which represent keys as their corresponding X11 keysym.
 29  * 
 30  * @constructor
 31  * @param {Element} element The Element to use to provide keyboard events.
 32  */
 33 Guacamole.Keyboard = function(element) {
 34 
 35     /**
 36      * Reference to this Guacamole.Keyboard.
 37      * @private
 38      */
 39     var guac_keyboard = this;
 40 
 41     /**
 42      * Fired whenever the user presses a key with the element associated
 43      * with this Guacamole.Keyboard in focus.
 44      * 
 45      * @event
 46      * @param {Number} keysym The keysym of the key being pressed.
 47      * @return {Boolean} true if the key event should be allowed through to the
 48      *                   browser, false otherwise.
 49      */
 50     this.onkeydown = null;
 51 
 52     /**
 53      * Fired whenever the user releases a key with the element associated
 54      * with this Guacamole.Keyboard in focus.
 55      * 
 56      * @event
 57      * @param {Number} keysym The keysym of the key being released.
 58      */
 59     this.onkeyup = null;
 60 
 61     /**
 62      * Map of known JavaScript keycodes which do not map to typable characters
 63      * to their unshifted X11 keysym equivalents.
 64      * @private
 65      */
 66     var unshiftedKeysym = {
 67         8:   [0xFF08], // backspace
 68         9:   [0xFF09], // tab
 69         13:  [0xFF0D], // enter
 70         16:  [0xFFE1, 0xFFE1, 0xFFE2], // shift
 71         17:  [0xFFE3, 0xFFE3, 0xFFE4], // ctrl
 72         18:  [0xFFE9, 0xFFE9, 0xFFEA], // alt
 73         19:  [0xFF13], // pause/break
 74         20:  [0xFFE5], // caps lock
 75         27:  [0xFF1B], // escape
 76         32:  [0x0020], // space
 77         33:  [0xFF55], // page up
 78         34:  [0xFF56], // page down
 79         35:  [0xFF57], // end
 80         36:  [0xFF50], // home
 81         37:  [0xFF51], // left arrow
 82         38:  [0xFF52], // up arrow
 83         39:  [0xFF53], // right arrow
 84         40:  [0xFF54], // down arrow
 85         45:  [0xFF63], // insert
 86         46:  [0xFFFF], // delete
 87         91:  [0xFFEB], // left window key (hyper_l)
 88         92:  [0xFF67], // right window key (menu key?)
 89         93:  null,     // select key
 90         112: [0xFFBE], // f1
 91         113: [0xFFBF], // f2
 92         114: [0xFFC0], // f3
 93         115: [0xFFC1], // f4
 94         116: [0xFFC2], // f5
 95         117: [0xFFC3], // f6
 96         118: [0xFFC4], // f7
 97         119: [0xFFC5], // f8
 98         120: [0xFFC6], // f9
 99         121: [0xFFC7], // f10
100         122: [0xFFC8], // f11
101         123: [0xFFC9], // f12
102         144: [0xFF7F], // num lock
103         145: [0xFF14]  // scroll lock
104     };
105 
106     /**
107      * Map of known JavaScript keyidentifiers which do not map to typable
108      * characters to their unshifted X11 keysym equivalents.
109      * @private
110      */
111     var keyidentifier_keysym = {
112         "Again": [0xFF66],
113         "AllCandidates": [0xFF3D],
114         "Alphanumeric": [0xFF30],
115         "Alt": [0xFFE9, 0xFFE9, 0xFFEA],
116         "Attn": [0xFD0E],
117         "AltGraph": [0xFFEA],
118         "ArrowDown": [0xFF54],
119         "ArrowLeft": [0xFF51],
120         "ArrowRight": [0xFF53],
121         "ArrowUp": [0xFF52],
122         "Backspace": [0xFF08],
123         "CapsLock": [0xFFE5],
124         "Cancel": [0xFF69],
125         "Clear": [0xFF0B],
126         "Convert": [0xFF21],
127         "Copy": [0xFD15],
128         "Crsel": [0xFD1C],
129         "CrSel": [0xFD1C],
130         "CodeInput": [0xFF37],
131         "Compose": [0xFF20],
132         "Control": [0xFFE3, 0xFFE3, 0xFFE4],
133         "ContextMenu": [0xFF67],
134         "Delete": [0xFFFF],
135         "Down": [0xFF54],
136         "End": [0xFF57],
137         "Enter": [0xFF0D],
138         "EraseEof": [0xFD06],
139         "Escape": [0xFF1B],
140         "Execute": [0xFF62],
141         "Exsel": [0xFD1D],
142         "ExSel": [0xFD1D],
143         "F1": [0xFFBE],
144         "F2": [0xFFBF],
145         "F3": [0xFFC0],
146         "F4": [0xFFC1],
147         "F5": [0xFFC2],
148         "F6": [0xFFC3],
149         "F7": [0xFFC4],
150         "F8": [0xFFC5],
151         "F9": [0xFFC6],
152         "F10": [0xFFC7],
153         "F11": [0xFFC8],
154         "F12": [0xFFC9],
155         "F13": [0xFFCA],
156         "F14": [0xFFCB],
157         "F15": [0xFFCC],
158         "F16": [0xFFCD],
159         "F17": [0xFFCE],
160         "F18": [0xFFCF],
161         "F19": [0xFFD0],
162         "F20": [0xFFD1],
163         "F21": [0xFFD2],
164         "F22": [0xFFD3],
165         "F23": [0xFFD4],
166         "F24": [0xFFD5],
167         "Find": [0xFF68],
168         "GroupFirst": [0xFE0C],
169         "GroupLast": [0xFE0E],
170         "GroupNext": [0xFE08],
171         "GroupPrevious": [0xFE0A],
172         "FullWidth": null,
173         "HalfWidth": null,
174         "HangulMode": [0xFF31],
175         "Hankaku": [0xFF29],
176         "HanjaMode": [0xFF34],
177         "Help": [0xFF6A],
178         "Hiragana": [0xFF25],
179         "HiraganaKatakana": [0xFF27],
180         "Home": [0xFF50],
181         "Hyper": [0xFFED, 0xFFED, 0xFFEE],
182         "Insert": [0xFF63],
183         "JapaneseHiragana": [0xFF25],
184         "JapaneseKatakana": [0xFF26],
185         "JapaneseRomaji": [0xFF24],
186         "JunjaMode": [0xFF38],
187         "KanaMode": [0xFF2D],
188         "KanjiMode": [0xFF21],
189         "Katakana": [0xFF26],
190         "Left": [0xFF51],
191         "Meta": [0xFFE7],
192         "ModeChange": [0xFF7E],
193         "NumLock": [0xFF7F],
194         "PageDown": [0xFF55],
195         "PageUp": [0xFF56],
196         "Pause": [0xFF13],
197         "Play": [0xFD16],
198         "PreviousCandidate": [0xFF3E],
199         "PrintScreen": [0xFD1D],
200         "Redo": [0xFF66],
201         "Right": [0xFF53],
202         "RomanCharacters": null,
203         "Scroll": [0xFF14],
204         "Select": [0xFF60],
205         "Separator": [0xFFAC],
206         "Shift": [0xFFE1, 0xFFE1, 0xFFE2],
207         "SingleCandidate": [0xFF3C],
208         "Super": [0xFFEB, 0xFFEB, 0xFFEC],
209         "Tab": [0xFF09],
210         "Up": [0xFF52],
211         "Undo": [0xFF65],
212         "Win": [0xFFEB],
213         "Zenkaku": [0xFF28],
214         "ZenkakuHankaku": [0xFF2A]
215     };
216 
217     /**
218      * Map of known JavaScript keycodes which do not map to typable characters
219      * to their shifted X11 keysym equivalents. Keycodes must only be listed
220      * here if their shifted X11 keysym equivalents differ from their unshifted
221      * equivalents.
222      * @private
223      */
224     var shiftedKeysym = {
225         18:  [0xFFE7, 0xFFE7, 0xFFEA]  // alt
226     };
227 
228     /**
229      * All keysyms which should not repeat when held down.
230      * @private
231      */
232     var no_repeat = {
233         0xFFE1: true, // Left shift
234         0xFFE2: true, // Right shift
235         0xFFE3: true, // Left ctrl 
236         0xFFE4: true, // Right ctrl 
237         0xFFE7: true, // Left meta 
238         0xFFE8: true, // Right meta 
239         0xFFE9: true, // Left alt
240         0xFFEA: true, // Right alt (or AltGr)
241         0xFFEB: true, // Left hyper
242         0xFFEC: true  // Right hyper
243     };
244 
245     /**
246      * All modifiers and their states.
247      */
248     this.modifiers = new Guacamole.Keyboard.ModifierState();
249         
250     /**
251      * The state of every key, indexed by keysym. If a particular key is
252      * pressed, the value of pressed for that keysym will be true. If a key
253      * is not currently pressed, it will not be defined. 
254      */
255     this.pressed = {};
256 
257     /**
258      * The keysym associated with a given keycode when keydown fired.
259      * @private
260      */
261     var keydownChar = [];
262 
263     /**
264      * Timeout before key repeat starts.
265      * @private
266      */
267     var key_repeat_timeout = null;
268 
269     /**
270      * Interval which presses and releases the last key pressed while that
271      * key is still being held down.
272      * @private
273      */
274     var key_repeat_interval = null;
275 
276     /**
277      * Given an array of keysyms indexed by location, returns the keysym
278      * for the given location, or the keysym for the standard location if
279      * undefined.
280      * 
281      * @param {Array} keysyms An array of keysyms, where the index of the
282      *                        keysym in the array is the location value.
283      * @param {Number} location The location on the keyboard corresponding to
284      *                          the key pressed, as defined at:
285      *                          http://www.w3.org/TR/DOM-Level-3-Events/#events-KeyboardEvent
286      */
287     function get_keysym(keysyms, location) {
288 
289         if (!keysyms)
290             return null;
291 
292         return keysyms[location] || keysyms[0];
293     }
294 
295     function keysym_from_key_identifier(shifted, identifier, location) {
296 
297         var typedCharacter;
298 
299         // If identifier is U+xxxx, decode Unicode character 
300         var unicodePrefixLocation = identifier.indexOf("U+");
301         if (unicodePrefixLocation >= 0) {
302             var hex = identifier.substring(unicodePrefixLocation+2);
303             typedCharacter = String.fromCharCode(parseInt(hex, 16));
304         }
305 
306         // If single character, use that as typed character
307         else if (identifier.length === 1)
308             typedCharacter = identifier;
309 
310         // Otherwise, look up corresponding keysym
311         else
312             return get_keysym(keyidentifier_keysym[identifier], location);
313 
314         // Convert case if shifted
315         if (shifted)
316             typedCharacter = typedCharacter.toUpperCase();
317         else
318             typedCharacter = typedCharacter.toLowerCase();
319 
320         // Get codepoint
321         var codepoint = typedCharacter.charCodeAt(0);
322         return keysym_from_charcode(codepoint);
323 
324     }
325 
326     function isControlCharacter(codepoint) {
327         return codepoint <= 0x1F || (codepoint >= 0x7F && codepoint <= 0x9F);
328     }
329 
330     function keysym_from_charcode(codepoint) {
331 
332         // Keysyms for control characters
333         if (isControlCharacter(codepoint)) return 0xFF00 | codepoint;
334 
335         // Keysyms for ASCII chars
336         if (codepoint >= 0x0000 && codepoint <= 0x00FF)
337             return codepoint;
338 
339         // Keysyms for Unicode
340         if (codepoint >= 0x0100 && codepoint <= 0x10FFFF)
341             return 0x01000000 | codepoint;
342 
343         return null;
344 
345     }
346 
347     function keysym_from_keycode(keyCode, location) {
348 
349         var keysyms;
350 
351         // If not shifted, just return unshifted keysym
352         if (!guac_keyboard.modifiers.shift)
353             keysyms = unshiftedKeysym[keyCode];
354 
355         // Otherwise, return shifted keysym, if defined
356         else
357             keysyms = shiftedKeysym[keyCode] || unshiftedKeysym[keyCode];
358 
359         return get_keysym(keysyms, location);
360 
361     }
362 
363     /**
364      * Marks a key as pressed, firing the keydown event if registered. Key
365      * repeat for the pressed key will start after a delay if that key is
366      * not a modifier.
367      * 
368      * @private
369      * @param keysym The keysym of the key to press.
370      * @return {Boolean} true if event should NOT be canceled, false otherwise.
371      */
372     function press_key(keysym) {
373 
374         // Don't bother with pressing the key if the key is unknown
375         if (keysym === null) return;
376 
377         // Only press if released
378         if (!guac_keyboard.pressed[keysym]) {
379 
380             // Mark key as pressed
381             guac_keyboard.pressed[keysym] = true;
382 
383             // Send key event
384             if (guac_keyboard.onkeydown) {
385                 var result = guac_keyboard.onkeydown(keysym);
386 
387                 // Stop any current repeat
388                 window.clearTimeout(key_repeat_timeout);
389                 window.clearInterval(key_repeat_interval);
390 
391                 // Repeat after a delay as long as pressed
392                 if (!no_repeat[keysym])
393                     key_repeat_timeout = window.setTimeout(function() {
394                         key_repeat_interval = window.setInterval(function() {
395                             guac_keyboard.onkeyup(keysym);
396                             guac_keyboard.onkeydown(keysym);
397                         }, 50);
398                     }, 500);
399 
400                 return result;
401             }
402         }
403 
404         return false;
405 
406     }
407 
408     /**
409      * Marks a key as released, firing the keyup event if registered.
410      * 
411      * @private
412      * @param keysym The keysym of the key to release.
413      */
414     function release_key(keysym) {
415 
416         // Only release if pressed
417         if (guac_keyboard.pressed[keysym]) {
418             
419             // Mark key as released
420             delete guac_keyboard.pressed[keysym];
421 
422             // Stop repeat
423             window.clearTimeout(key_repeat_timeout);
424             window.clearInterval(key_repeat_interval);
425 
426             // Send key event
427             if (keysym !== null && guac_keyboard.onkeyup)
428                 guac_keyboard.onkeyup(keysym);
429 
430         }
431 
432     }
433 
434     /**
435      * Given a keyboard event, updates the local modifier state and remote
436      * key state based on the modifier flags within the event. This function
437      * pays no attention to keycodes.
438      * 
439      * @param {KeyboardEvent} e The keyboard event containing the flags to update.
440      */
441     function update_modifier_state(e) {
442 
443         // Get state
444         var state = Guacamole.Keyboard.ModifierState.fromKeyboardEvent(e);
445 
446         // Release alt if implicitly released
447         if (guac_keyboard.modifiers.alt && state.alt === false) {
448             release_key(0xFFE9); // Left alt
449             release_key(0xFFEA); // Right alt (or AltGr)
450         }
451 
452         // Release shift if implicitly released
453         if (guac_keyboard.modifiers.shift && state.shift === false) {
454             release_key(0xFFE1); // Left shift
455             release_key(0xFFE2); // Right shift
456         }
457 
458         // Release ctrl if implicitly released
459         if (guac_keyboard.modifiers.ctrl && state.ctrl === false) {
460             release_key(0xFFE3); // Left ctrl 
461             release_key(0xFFE4); // Right ctrl 
462         }
463 
464         // Release meta if implicitly released
465         if (guac_keyboard.modifiers.meta && state.meta === false) {
466             release_key(0xFFE7); // Left meta 
467             release_key(0xFFE8); // Right meta 
468         }
469 
470         // Release hyper if implicitly released
471         if (guac_keyboard.modifiers.hyper && state.hyper === false) {
472             release_key(0xFFEB); // Left hyper
473             release_key(0xFFEC); // Right hyper
474         }
475 
476         // Update state
477         guac_keyboard.modifiers = state;
478 
479     }
480 
481     // When key pressed
482     element.addEventListener("keydown", function(e) {
483 
484         // Only intercept if handler set
485         if (!guac_keyboard.onkeydown) return;
486 
487         var keynum;
488         if (window.event) keynum = window.event.keyCode;
489         else if (e.which) keynum = e.which;
490 
491         // Get key location
492         var location = e.location || e.keyLocation || 0;
493 
494         // Ignore any unknown key events
495         if (!keynum && !identifier) {
496             e.preventDefault();
497             return;
498         }
499 
500         // Fix modifier states
501         update_modifier_state(e);
502 
503         // Try to get keysym from keycode
504         var keysym = keysym_from_keycode(keynum, location);
505 
506         // Also try to get get keysym from e.key 
507         if (e.key)
508             keysym = keysym || keysym_from_key_identifier(
509                 guac_keyboard.modifiers.shift, e.key, location);
510 
511         // If no e.key, use e.keyIdentifier if absolutely necessary (can be buggy)
512         else {
513 
514             var keypress_unlikely =  guac_keyboard.modifiers.ctrl
515                                   || guac_keyboard.modifiers.alt
516                                   || guac_keyboard.modifiers.meta
517                                   || guac_keyboard.modifiers.hyper;
518 
519             if (keypress_unlikely && e.keyIdentifier)
520                 keysym = keysym || keysym_from_key_identifier(
521                     guac_keyboard.modifiers.shift, e.keyIdentifier, location);
522 
523         }
524 
525         // Press key if known
526         if (keysym !== null) {
527 
528             keydownChar[keynum] = keysym;
529             if (!press_key(keysym))
530                 e.preventDefault();
531             
532             // If a key is pressed while meta is held down, the keyup will
533             // never be sent in Chrome, so send it now. (bug #108404)
534             if (guac_keyboard.modifiers.meta && keysym !== 0xFFE7 && keysym !== 0xFFE8)
535                 release_key(keysym);
536 
537         }
538 
539     }, true);
540 
541     // When key pressed
542     element.addEventListener("keypress", function(e) {
543 
544         // Only intercept if handler set
545         if (!guac_keyboard.onkeydown && !guac_keyboard.onkeyup) return;
546 
547         var keynum;
548         if (window.event) keynum = window.event.keyCode;
549         else if (e.which) keynum = e.which;
550 
551         var keysym = keysym_from_charcode(keynum);
552 
553         // Fix modifier states
554         update_modifier_state(e);
555 
556         // If event identified as a typable character, and we're holding Ctrl+Alt,
557         // assume Ctrl+Alt is actually AltGr, and release both.
558         if (!isControlCharacter(keynum) && guac_keyboard.modifiers.ctrl && guac_keyboard.modifiers.alt) {
559             release_key(0xFFE3); // Left ctrl
560             release_key(0xFFE4); // Right ctrl
561             release_key(0xFFE9); // Left alt
562             release_key(0xFFEA); // Right alt
563         }
564 
565         // Send press + release if keysym known
566         if (keysym !== null) {
567             if (!press_key(keysym))
568                 e.preventDefault();
569             release_key(keysym);
570         }
571         else
572             e.preventDefault();
573 
574     }, true);
575 
576     // When key released
577     element.addEventListener("keyup", function(e) {
578 
579         // Only intercept if handler set
580         if (!guac_keyboard.onkeyup) return;
581 
582         e.preventDefault();
583 
584         var keynum;
585         if (window.event) keynum = window.event.keyCode;
586         else if (e.which) keynum = e.which;
587         
588         // Fix modifier states
589         update_modifier_state(e);
590 
591         // Send release event if original key known
592         var keysym = keydownChar[keynum];
593         if (keysym !== null)
594             release_key(keysym);
595 
596         // Clear character record
597         keydownChar[keynum] = null;
598 
599     }, true);
600 
601 };
602 
603 /**
604  * The state of all supported keyboard modifiers.
605  * @constructor
606  */
607 Guacamole.Keyboard.ModifierState = function() {
608     
609     /**
610      * Whether shift is currently pressed.
611      * @type Boolean
612      */
613     this.shift = false;
614     
615     /**
616      * Whether ctrl is currently pressed.
617      * @type Boolean
618      */
619     this.ctrl = false;
620     
621     /**
622      * Whether alt is currently pressed.
623      * @type Boolean
624      */
625     this.alt = false;
626     
627     /**
628      * Whether meta (apple key) is currently pressed.
629      * @type Boolean
630      */
631     this.meta = false;
632 
633     /**
634      * Whether hyper (windows key) is currently pressed.
635      * @type Boolean
636      */
637     this.hyper = false;
638     
639 };
640 
641 /**
642  * Returns the modifier state applicable to the keyboard event given.
643  * 
644  * @param {KeyboardEvent} e The keyboard event to read.
645  * @returns {Guacamole.Keyboard.ModifierState} The current state of keyboard
646  *                                             modifiers.
647  */
648 Guacamole.Keyboard.ModifierState.fromKeyboardEvent = function(e) {
649     
650     var state = new Guacamole.Keyboard.ModifierState();
651 
652     // Assign states from old flags
653     state.shift = e.shiftKey;
654     state.ctrl  = e.ctrlKey;
655     state.alt   = e.altKey;
656     state.meta  = e.metaKey;
657 
658     // Use DOM3 getModifierState() for others
659     if (e.getModifierState) {
660         state.hyper = e.getModifierState("OS")
661                    || e.getModifierState("Super")
662                    || e.getModifierState("Hyper")
663                    || e.getModifierState("Win");
664     }
665 
666     return state;
667     
668 };
669