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 last result of calling the onkeydown handler for each key, indexed
259      * by keysym. This is used to prevent/allow default actions for key events,
260      * even when the onkeydown handler cannot be called again because the key
261      * is (theoretically) still pressed.
262      */
263     var last_keydown_result = {};
264 
265     /**
266      * The keysym associated with a given keycode when keydown fired.
267      * @private
268      */
269     var keydownChar = [];
270 
271     /**
272      * Timeout before key repeat starts.
273      * @private
274      */
275     var key_repeat_timeout = null;
276 
277     /**
278      * Interval which presses and releases the last key pressed while that
279      * key is still being held down.
280      * @private
281      */
282     var key_repeat_interval = null;
283 
284     /**
285      * Given an array of keysyms indexed by location, returns the keysym
286      * for the given location, or the keysym for the standard location if
287      * undefined.
288      * 
289      * @param {Array} keysyms An array of keysyms, where the index of the
290      *                        keysym in the array is the location value.
291      * @param {Number} location The location on the keyboard corresponding to
292      *                          the key pressed, as defined at:
293      *                          http://www.w3.org/TR/DOM-Level-3-Events/#events-KeyboardEvent
294      */
295     function get_keysym(keysyms, location) {
296 
297         if (!keysyms)
298             return null;
299 
300         return keysyms[location] || keysyms[0];
301     }
302 
303     function keysym_from_key_identifier(shifted, identifier, location) {
304 
305         var typedCharacter;
306 
307         // If identifier is U+xxxx, decode Unicode character 
308         var unicodePrefixLocation = identifier.indexOf("U+");
309         if (unicodePrefixLocation >= 0) {
310             var hex = identifier.substring(unicodePrefixLocation+2);
311             typedCharacter = String.fromCharCode(parseInt(hex, 16));
312         }
313 
314         // If single character, use that as typed character
315         else if (identifier.length === 1)
316             typedCharacter = identifier;
317 
318         // Otherwise, look up corresponding keysym
319         else
320             return get_keysym(keyidentifier_keysym[identifier], location);
321 
322         // Convert case if shifted
323         if (shifted)
324             typedCharacter = typedCharacter.toUpperCase();
325         else
326             typedCharacter = typedCharacter.toLowerCase();
327 
328         // Get codepoint
329         var codepoint = typedCharacter.charCodeAt(0);
330         return keysym_from_charcode(codepoint);
331 
332     }
333 
334     function isControlCharacter(codepoint) {
335         return codepoint <= 0x1F || (codepoint >= 0x7F && codepoint <= 0x9F);
336     }
337 
338     function keysym_from_charcode(codepoint) {
339 
340         // Keysyms for control characters
341         if (isControlCharacter(codepoint)) return 0xFF00 | codepoint;
342 
343         // Keysyms for ASCII chars
344         if (codepoint >= 0x0000 && codepoint <= 0x00FF)
345             return codepoint;
346 
347         // Keysyms for Unicode
348         if (codepoint >= 0x0100 && codepoint <= 0x10FFFF)
349             return 0x01000000 | codepoint;
350 
351         return null;
352 
353     }
354 
355     function keysym_from_keycode(keyCode, location) {
356 
357         var keysyms;
358 
359         // If not shifted, just return unshifted keysym
360         if (!guac_keyboard.modifiers.shift)
361             keysyms = unshiftedKeysym[keyCode];
362 
363         // Otherwise, return shifted keysym, if defined
364         else
365             keysyms = shiftedKeysym[keyCode] || unshiftedKeysym[keyCode];
366 
367         return get_keysym(keysyms, location);
368 
369     }
370 
371     /**
372      * Marks a key as pressed, firing the keydown event if registered. Key
373      * repeat for the pressed key will start after a delay if that key is
374      * not a modifier.
375      * 
376      * @private
377      * @param keysym The keysym of the key to press.
378      * @return {Boolean} true if event should NOT be canceled, false otherwise.
379      */
380     function press_key(keysym) {
381 
382         // Don't bother with pressing the key if the key is unknown
383         if (keysym === null) return;
384 
385         // Only press if released
386         if (!guac_keyboard.pressed[keysym]) {
387 
388             // Mark key as pressed
389             guac_keyboard.pressed[keysym] = true;
390 
391             // Send key event
392             if (guac_keyboard.onkeydown) {
393                 var result = guac_keyboard.onkeydown(keysym);
394                 last_keydown_result[keysym] = result;
395 
396                 // Stop any current repeat
397                 window.clearTimeout(key_repeat_timeout);
398                 window.clearInterval(key_repeat_interval);
399 
400                 // Repeat after a delay as long as pressed
401                 if (!no_repeat[keysym])
402                     key_repeat_timeout = window.setTimeout(function() {
403                         key_repeat_interval = window.setInterval(function() {
404                             guac_keyboard.onkeyup(keysym);
405                             guac_keyboard.onkeydown(keysym);
406                         }, 50);
407                     }, 500);
408 
409                 return result;
410             }
411         }
412 
413         // Return the last keydown result by default, resort to false if unknown
414         return last_keydown_result[keysym] || false;
415 
416     }
417 
418     /**
419      * Marks a key as released, firing the keyup event if registered.
420      * 
421      * @private
422      * @param keysym The keysym of the key to release.
423      */
424     function release_key(keysym) {
425 
426         // Only release if pressed
427         if (guac_keyboard.pressed[keysym]) {
428             
429             // Mark key as released
430             delete guac_keyboard.pressed[keysym];
431 
432             // Stop repeat
433             window.clearTimeout(key_repeat_timeout);
434             window.clearInterval(key_repeat_interval);
435 
436             // Send key event
437             if (keysym !== null && guac_keyboard.onkeyup)
438                 guac_keyboard.onkeyup(keysym);
439 
440         }
441 
442     }
443 
444     /**
445      * Given a keyboard event, updates the local modifier state and remote
446      * key state based on the modifier flags within the event. This function
447      * pays no attention to keycodes.
448      * 
449      * @param {KeyboardEvent} e The keyboard event containing the flags to update.
450      */
451     function update_modifier_state(e) {
452 
453         // Get state
454         var state = Guacamole.Keyboard.ModifierState.fromKeyboardEvent(e);
455 
456         // Release alt if implicitly released
457         if (guac_keyboard.modifiers.alt && state.alt === false) {
458             release_key(0xFFE9); // Left alt
459             release_key(0xFFEA); // Right alt (or AltGr)
460         }
461 
462         // Release shift if implicitly released
463         if (guac_keyboard.modifiers.shift && state.shift === false) {
464             release_key(0xFFE1); // Left shift
465             release_key(0xFFE2); // Right shift
466         }
467 
468         // Release ctrl if implicitly released
469         if (guac_keyboard.modifiers.ctrl && state.ctrl === false) {
470             release_key(0xFFE3); // Left ctrl 
471             release_key(0xFFE4); // Right ctrl 
472         }
473 
474         // Release meta if implicitly released
475         if (guac_keyboard.modifiers.meta && state.meta === false) {
476             release_key(0xFFE7); // Left meta 
477             release_key(0xFFE8); // Right meta 
478         }
479 
480         // Release hyper if implicitly released
481         if (guac_keyboard.modifiers.hyper && state.hyper === false) {
482             release_key(0xFFEB); // Left hyper
483             release_key(0xFFEC); // Right hyper
484         }
485 
486         // Update state
487         guac_keyboard.modifiers = state;
488 
489     }
490 
491     // When key pressed
492     element.addEventListener("keydown", function(e) {
493 
494         // Only intercept if handler set
495         if (!guac_keyboard.onkeydown) return;
496 
497         var keynum;
498         if (window.event) keynum = window.event.keyCode;
499         else if (e.which) keynum = e.which;
500 
501         // Get key location
502         var location = e.location || e.keyLocation || 0;
503 
504         // Ignore any unknown key events
505         if (!keynum) {
506             e.preventDefault();
507             return;
508         }
509 
510         // Fix modifier states
511         update_modifier_state(e);
512 
513         // Ignore (but do not prevent) the "composition" keycode sent by some
514         // browsers when an IME is in use (see: http://lists.w3.org/Archives/Public/www-dom/2010JulSep/att-0182/keyCode-spec.html)
515         if (keynum === 229)
516             return;
517 
518         // Try to get keysym from keycode
519         var keysym = keysym_from_keycode(keynum, location);
520 
521         // Also try to get get keysym from e.key 
522         if (e.key)
523             keysym = keysym || keysym_from_key_identifier(
524                 guac_keyboard.modifiers.shift, e.key, location);
525 
526         // If no e.key, use e.keyIdentifier if absolutely necessary (can be buggy)
527         else {
528 
529             var keypress_unlikely =  guac_keyboard.modifiers.ctrl
530                                   || guac_keyboard.modifiers.alt
531                                   || guac_keyboard.modifiers.meta
532                                   || guac_keyboard.modifiers.hyper;
533 
534             if (keypress_unlikely && e.keyIdentifier)
535                 keysym = keysym || keysym_from_key_identifier(
536                     guac_keyboard.modifiers.shift, e.keyIdentifier, location);
537 
538         }
539 
540         // Press key if known
541         if (keysym !== null) {
542 
543             keydownChar[keynum] = keysym;
544             if (!press_key(keysym))
545                 e.preventDefault();
546             
547             // If a key is pressed while meta is held down, the keyup will
548             // never be sent in Chrome, so send it now. (bug #108404)
549             if (guac_keyboard.modifiers.meta && keysym !== 0xFFE7 && keysym !== 0xFFE8)
550                 release_key(keysym);
551 
552         }
553 
554     }, true);
555 
556     // When key pressed
557     element.addEventListener("keypress", function(e) {
558 
559         // Only intercept if handler set
560         if (!guac_keyboard.onkeydown && !guac_keyboard.onkeyup) return;
561 
562         var keynum;
563         if (window.event) keynum = window.event.keyCode;
564         else if (e.which) keynum = e.which;
565 
566         var keysym = keysym_from_charcode(keynum);
567 
568         // Fix modifier states
569         update_modifier_state(e);
570 
571         // If event identified as a typable character, and we're holding Ctrl+Alt,
572         // assume Ctrl+Alt is actually AltGr, and release both.
573         if (!isControlCharacter(keynum) && guac_keyboard.modifiers.ctrl && guac_keyboard.modifiers.alt) {
574             release_key(0xFFE3); // Left ctrl
575             release_key(0xFFE4); // Right ctrl
576             release_key(0xFFE9); // Left alt
577             release_key(0xFFEA); // Right alt
578         }
579 
580         // Send press + release if keysym known
581         if (keysym !== null) {
582             if (!press_key(keysym))
583                 e.preventDefault();
584             release_key(keysym);
585         }
586         else
587             e.preventDefault();
588 
589     }, true);
590 
591     // When key released
592     element.addEventListener("keyup", function(e) {
593 
594         // Only intercept if handler set
595         if (!guac_keyboard.onkeyup) return;
596 
597         e.preventDefault();
598 
599         var keynum;
600         if (window.event) keynum = window.event.keyCode;
601         else if (e.which) keynum = e.which;
602         
603         // Fix modifier states
604         update_modifier_state(e);
605 
606         // Send release event if original key known
607         var keysym = keydownChar[keynum];
608         if (keysym !== null)
609             release_key(keysym);
610 
611         // Clear character record
612         keydownChar[keynum] = null;
613 
614     }, true);
615 
616 };
617 
618 /**
619  * The state of all supported keyboard modifiers.
620  * @constructor
621  */
622 Guacamole.Keyboard.ModifierState = function() {
623     
624     /**
625      * Whether shift is currently pressed.
626      * @type Boolean
627      */
628     this.shift = false;
629     
630     /**
631      * Whether ctrl is currently pressed.
632      * @type Boolean
633      */
634     this.ctrl = false;
635     
636     /**
637      * Whether alt is currently pressed.
638      * @type Boolean
639      */
640     this.alt = false;
641     
642     /**
643      * Whether meta (apple key) is currently pressed.
644      * @type Boolean
645      */
646     this.meta = false;
647 
648     /**
649      * Whether hyper (windows key) is currently pressed.
650      * @type Boolean
651      */
652     this.hyper = false;
653     
654 };
655 
656 /**
657  * Returns the modifier state applicable to the keyboard event given.
658  * 
659  * @param {KeyboardEvent} e The keyboard event to read.
660  * @returns {Guacamole.Keyboard.ModifierState} The current state of keyboard
661  *                                             modifiers.
662  */
663 Guacamole.Keyboard.ModifierState.fromKeyboardEvent = function(e) {
664     
665     var state = new Guacamole.Keyboard.ModifierState();
666 
667     // Assign states from old flags
668     state.shift = e.shiftKey;
669     state.ctrl  = e.ctrlKey;
670     state.alt   = e.altKey;
671     state.meta  = e.metaKey;
672 
673     // Use DOM3 getModifierState() for others
674     if (e.getModifierState) {
675         state.hyper = e.getModifierState("OS")
676                    || e.getModifierState("Super")
677                    || e.getModifierState("Hyper")
678                    || e.getModifierState("Win");
679     }
680 
681     return state;
682     
683 };
684