diff --git a/guacamole-common-js/src/main/webapp/modules/Keyboard.js b/guacamole-common-js/src/main/webapp/modules/Keyboard.js index 876c88ac0..e41b03ed5 100644 --- a/guacamole-common-js/src/main/webapp/modules/Keyboard.js +++ b/guacamole-common-js/src/main/webapp/modules/Keyboard.js @@ -55,6 +55,47 @@ Guacamole.Keyboard = function(element) { */ this.onkeyup = null; + /** + * Set of known platform-specific or browser-specific quirks which must be + * accounted for to properly interpret key events, even if the only way to + * reliably detect that quirk is to platform/browser-sniff. + * + * @private + * @type {Object.} + */ + var quirks = { + + /** + * Whether keyup events are universally unreliable. + * + * @type {Boolean} + */ + keyupUnreliable: false, + + /** + * Whether the Alt key is actually a modifier for typable keys and is + * thus never used for keyboard shortcuts. + * + * @type {Boolean} + */ + altIsTypableOnly: false + + }; + + // Set quirk flags depending on platform/browser, if such information is + // available + if (navigator && navigator.platform) { + + // All keyup events are unreliable on iOS (sadly) + if (navigator.platform.match(/ipad|iphone|ipod/i)) + quirks.keyupUnreliable = true; + + // The Alt key on Mac is never used for keyboard shortcuts + else if (navigator.platform.match(/^mac/i)) + quirks.altIsTypableOnly = true; + + } + /** * A key event having a corresponding timestamp. This event is non-specific. * Its subclasses should be used instead when recording specific key @@ -175,6 +216,14 @@ Guacamole.Keyboard = function(element) { this.keysym = keysym_from_key_identifier(key, location) || keysym_from_keycode(keyCode, location); + /** + * Whether the keyup following this keydown event is known to be + * reliable. If false, we cannot rely on the keyup event to occur. + * + * @type {Boolean} + */ + this.keyupReliable = !quirks.keyupUnreliable; + // DOM3 and keyCode are reliable sources if the corresponding key is // not a printable key if (this.keysym && !isPrintable(this.keysym)) @@ -184,9 +233,13 @@ Guacamole.Keyboard = function(element) { if (!this.keysym && key_identifier_sane(keyCode, keyIdentifier)) this.keysym = keysym_from_key_identifier(keyIdentifier, location, guac_keyboard.modifiers.shift); + // If a key is pressed while meta is held down, the keyup will + // never be sent in Chrome (bug #108404) + if (guac_keyboard.modifiers.meta && this.keysym !== 0xFFE7 && this.keysym !== 0xFFE8) + this.keyupReliable = false; + // Determine whether default action for Alt+combinations must be prevented - var prevent_alt = !guac_keyboard.modifiers.ctrl - && !(navigator && navigator.platform && navigator.platform.match(/^mac/i)); + var prevent_alt = !guac_keyboard.modifiers.ctrl && !quirks.altIsTypableOnly; // Determine whether default action for Ctrl+combinations must be prevented var prevent_ctrl = !guac_keyboard.modifiers.alt; @@ -804,6 +857,37 @@ Guacamole.Keyboard = function(element) { }; + /** + * Given the remote and local state of a particular key, resynchronizes the + * remote state of that key with the local state through pressing or + * releasing keysyms. + * + * @private + * @param {Boolean} remoteState + * Whether the key is currently pressed remotely. + * + * @param {Boolean} localState + * Whether the key is currently pressed remotely locally. If the state + * of the key is not known, this may be undefined. + * + * @param {Number[]} keysyms + * The keysyms which represent the key being updated. + */ + var updateModifierState = function updateModifierState(remoteState, localState, keysyms) { + + // Release all related keys if modifier is implicitly released + if (remoteState && localState === false) { + for (var i = 0; i < keysyms.length; i++) { + guac_keyboard.release(keysyms[i]); + } + } + + // Press if modifier is implicitly pressed + else if (!remoteState && localState) + guac_keyboard.press(keysyms[0]); + + }; + /** * Given a keyboard event, updates the local modifier state and remote * key state based on the modifier flags within the event. This function @@ -813,41 +897,41 @@ Guacamole.Keyboard = function(element) { * @param {KeyboardEvent} e * The keyboard event containing the flags to update. */ - var update_modifier_state = function update_modifier_state(e) { + var syncModifierStates = function syncModifierStates(e) { // Get state var state = Guacamole.Keyboard.ModifierState.fromKeyboardEvent(e); - // Release alt if implicitly released - if (guac_keyboard.modifiers.alt && state.alt === false) { - guac_keyboard.release(0xFFE9); // Left alt - guac_keyboard.release(0xFFEA); // Right alt - guac_keyboard.release(0xFE03); // AltGr - } + // Resync state of alt + updateModifierState(guac_keyboard.modifiers.alt, state.alt, [ + 0xFFE9, // Left alt + 0xFFEA, // Right alt + 0xFE03 // AltGr + ]); - // Release shift if implicitly released - if (guac_keyboard.modifiers.shift && state.shift === false) { - guac_keyboard.release(0xFFE1); // Left shift - guac_keyboard.release(0xFFE2); // Right shift - } + // Resync state of shift + updateModifierState(guac_keyboard.modifiers.shift, state.shift, [ + 0xFFE1, // Left shift + 0xFFE2 // Right shift + ]); - // Release ctrl if implicitly released - if (guac_keyboard.modifiers.ctrl && state.ctrl === false) { - guac_keyboard.release(0xFFE3); // Left ctrl - guac_keyboard.release(0xFFE4); // Right ctrl - } + // Resync state of ctrl + updateModifierState(guac_keyboard.modifiers.ctrl, state.ctrl, [ + 0xFFE3, // Left ctrl + 0xFFE4 // Right ctrl + ]); - // Release meta if implicitly released - if (guac_keyboard.modifiers.meta && state.meta === false) { - guac_keyboard.release(0xFFE7); // Left meta - guac_keyboard.release(0xFFE8); // Right meta - } + // Resync state of meta + updateModifierState(guac_keyboard.modifiers.meta, state.meta, [ + 0xFFE7, // Left meta + 0xFFE8 // Right meta + ]); - // Release hyper if implicitly released - if (guac_keyboard.modifiers.hyper && state.hyper === false) { - guac_keyboard.release(0xFFEB); // Left hyper - guac_keyboard.release(0xFFEC); // Right hyper - } + // Resync state of hyper + updateModifierState(guac_keyboard.modifiers.hyper, state.hyper, [ + 0xFFEB, // Left hyper + 0xFFEC // Right hyper + ]); // Update state guac_keyboard.modifiers = state; @@ -966,9 +1050,9 @@ Guacamole.Keyboard = function(element) { var defaultPrevented = !guac_keyboard.press(keysym); recentKeysym[first.keyCode] = keysym; - // If a key is pressed while meta is held down, the keyup will - // never be sent in Chrome, so send it now. (bug #108404) - if (guac_keyboard.modifiers.meta && keysym !== 0xFFE7 && keysym !== 0xFFE8) + // Release the key now if we cannot rely on the associated + // keyup event + if (!first.keyupReliable) guac_keyboard.release(keysym); // Record whether default was prevented @@ -984,7 +1068,7 @@ Guacamole.Keyboard = function(element) { } // end if keydown // Keyup event - else if (first instanceof KeyupEvent) { + else if (first instanceof KeyupEvent && !quirks.keyupUnreliable) { // Release specific key if known var keysym = first.keysym; @@ -1003,7 +1087,8 @@ Guacamole.Keyboard = function(element) { } // end if keyup - // Ignore any other type of event (keypress by itself is invalid) + // Ignore any other type of event (keypress by itself is invalid, and + // unreliable keyup events should simply be dumped) else return eventLog.shift(); @@ -1052,7 +1137,7 @@ Guacamole.Keyboard = function(element) { else if (e.which) keyCode = e.which; // Fix modifier states - update_modifier_state(e); + syncModifierStates(e); // Ignore (but do not prevent) the "composition" keycode sent by some // browsers when an IME is in use (see: http://lists.w3.org/Archives/Public/www-dom/2010JulSep/att-0182/keyCode-spec.html) @@ -1080,7 +1165,7 @@ Guacamole.Keyboard = function(element) { else if (e.which) charCode = e.which; // Fix modifier states - update_modifier_state(e); + syncModifierStates(e); // Log event var keypressEvent = new KeypressEvent(charCode); @@ -1105,7 +1190,7 @@ Guacamole.Keyboard = function(element) { else if (e.which) keyCode = e.which; // Fix modifier states - update_modifier_state(e); + syncModifierStates(e); // Log event, call for interpretation var keyupEvent = new KeyupEvent(keyCode, e.keyIdentifier, e.key, getEventLocation(e));