Skip to content

Commit 43e6214

Browse files
committed
GUACAMOLE-2207: Add support for CMD+Click.
1 parent e651e69 commit 43e6214

File tree

4 files changed

+203
-33
lines changed

4 files changed

+203
-33
lines changed

guacamole-common-js/src/main/webapp/modules/Keyboard.js

Lines changed: 103 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -285,8 +285,10 @@ Guacamole.Keyboard = function Keyboard(element) {
285285
this.keysym = keysym_from_key_identifier(this.keyIdentifier, this.location, this.modifiers.shift);
286286

287287
// If a key is pressed while meta is held down, the keyup will
288-
// never be sent in Chrome (bug #108404)
289-
if (this.modifiers.meta && this.keysym !== 0xFFE7 && this.keysym !== 0xFFE8)
288+
// never be sent in Chrome (bug #108404). Modifier keys are excluded
289+
// from this workaround as they have reliable keyup events and need
290+
// to be held down simultaneously with Meta.
291+
if (this.modifiers.meta && !isModifierKey(this.keysym))
290292
this.keyupReliable = false;
291293

292294
// We cannot rely on receiving keyup for Caps Lock on certain platforms
@@ -578,6 +580,37 @@ Guacamole.Keyboard = function Keyboard(element) {
578580
"ZenkakuHankaku": [0xFF2A]
579581
};
580582

583+
/**
584+
* All modifier key keysyms, grouped by modifier type.
585+
*
586+
* @private
587+
* @type {!Object.<string, number[]>}
588+
*/
589+
var modifierKeysymsByType = {
590+
shift: [0xFFE1, 0xFFE2], // Left shift, Right shift
591+
ctrl: [0xFFE3, 0xFFE4], // Left ctrl, Right ctrl
592+
alt: [0xFFE9, 0xFFEA, 0xFE03], // Left alt, Right alt, AltGr
593+
meta: [0xFFE7, 0xFFE8], // Left meta, Right meta
594+
hyper: [0xFFEB, 0xFFEC] // Left super/hyper, Right super/hyper
595+
};
596+
597+
/**
598+
* All modifier key keysyms for quick lookup.
599+
*
600+
* @private
601+
* @type {!Object.<number, boolean>}
602+
*/
603+
var modifierKeysyms = (function() {
604+
var lookup = {};
605+
for (var modifier in modifierKeysymsByType) {
606+
var keysyms = modifierKeysymsByType[modifier];
607+
for (var i = 0; i < keysyms.length; i++) {
608+
lookup[keysyms[i]] = true;
609+
}
610+
}
611+
return lookup;
612+
})();
613+
581614
/**
582615
* All keysyms which should not repeat when held down.
583616
*
@@ -706,6 +739,34 @@ Guacamole.Keyboard = function Keyboard(element) {
706739

707740
};
708741

742+
/**
743+
* Returns true if the given keysym corresponds to a Meta key (left or
744+
* right Meta/Command/Windows key).
745+
*
746+
* @param {!number} keysym
747+
* The keysym to check.
748+
*
749+
* @returns {!boolean}
750+
* true if the given keysym corresponds to a Meta key, false otherwise.
751+
*/
752+
var isMetaKey = function isMetaKey(keysym) {
753+
return modifierKeysymsByType.meta.indexOf(keysym) !== -1;
754+
};
755+
756+
/**
757+
* Returns true if the given keysym corresponds to a modifier key
758+
* (Shift, Ctrl, Alt, Meta, Hyper, AltGr).
759+
*
760+
* @param {!number} keysym
761+
* The keysym to check.
762+
*
763+
* @returns {!boolean}
764+
* true if the given keysym corresponds to a modifier key, false otherwise.
765+
*/
766+
var isModifierKey = function isModifierKey(keysym) {
767+
return modifierKeysyms[keysym] === true;
768+
};
769+
709770
function keysym_from_key_identifier(identifier, location, shifted) {
710771

711772
if (!identifier)
@@ -926,6 +987,39 @@ Guacamole.Keyboard = function Keyboard(element) {
926987

927988
};
928989

990+
/**
991+
* Handles a mouse event to resolve deferred Meta key events. When a Meta
992+
* key is pressed, it is deferred to determine if it's being used as a
993+
* modifier or as a standalone key. A mouse click with Meta held provides
994+
* the context needed to resolve this, enabling Cmd+Click functionality.
995+
*
996+
* @param {Guacamole.Mouse.Event} mouseEvent
997+
* The mouse event that occurred.
998+
*/
999+
this.handleMouseEvent = function(mouseEvent) {
1000+
1001+
// Only process mouse events that have meta modifier pressed
1002+
if (!mouseEvent.modifiers.meta)
1003+
return;
1004+
1005+
// Check if there's a pending Meta key waiting for context
1006+
var hasPendingMeta = eventLog.length > 0 &&
1007+
eventLog[0] instanceof KeydownEvent &&
1008+
isMetaKey(eventLog[0].keysym);
1009+
1010+
// Only add mouse event if there's a pending Meta key
1011+
if (hasPendingMeta) {
1012+
// Push mouse event onto the event log to provide context for the
1013+
// deferred Meta key. The mouse event will be silently dropped when
1014+
// processed as it's not a KeyEvent type.
1015+
eventLog.push(mouseEvent);
1016+
1017+
// Process the event log, which will now resolve the deferred Meta
1018+
// key using the mouse event's modifier state as context
1019+
interpret_events();
1020+
}
1021+
};
1022+
9291023
/**
9301024
* Resynchronizes the remote state of the given modifier with its
9311025
* corresponding local modifier state, as dictated by
@@ -1004,36 +1098,12 @@ Guacamole.Keyboard = function Keyboard(element) {
10041098
*/
10051099
var syncModifierStates = function syncModifierStates(keyEvent) {
10061100

1007-
// Resync state of alt
1008-
updateModifierState('alt', [
1009-
0xFFE9, // Left alt
1010-
0xFFEA, // Right alt
1011-
0xFE03 // AltGr
1012-
], keyEvent);
1013-
1014-
// Resync state of shift
1015-
updateModifierState('shift', [
1016-
0xFFE1, // Left shift
1017-
0xFFE2 // Right shift
1018-
], keyEvent);
1019-
1020-
// Resync state of ctrl
1021-
updateModifierState('ctrl', [
1022-
0xFFE3, // Left ctrl
1023-
0xFFE4 // Right ctrl
1024-
], keyEvent);
1025-
1026-
// Resync state of meta
1027-
updateModifierState('meta', [
1028-
0xFFE7, // Left meta
1029-
0xFFE8 // Right meta
1030-
], keyEvent);
1031-
1032-
// Resync state of hyper
1033-
updateModifierState('hyper', [
1034-
0xFFEB, // Left super/hyper
1035-
0xFFEC // Right super/hyper
1036-
], keyEvent);
1101+
// Resync state of all modifiers
1102+
updateModifierState('alt', modifierKeysymsByType.alt, keyEvent);
1103+
updateModifierState('shift', modifierKeysymsByType.shift, keyEvent);
1104+
updateModifierState('ctrl', modifierKeysymsByType.ctrl, keyEvent);
1105+
updateModifierState('meta', modifierKeysymsByType.meta, keyEvent);
1106+
updateModifierState('hyper', modifierKeysymsByType.hyper, keyEvent);
10371107

10381108
// Update state
10391109
guac_keyboard.modifiers = keyEvent.modifiers;
@@ -1152,7 +1222,7 @@ Guacamole.Keyboard = function Keyboard(element) {
11521222
// Defer handling of Meta until it is known to be functioning as a
11531223
// modifier (it may otherwise actually be an alternative method for
11541224
// pressing a single key, such as Meta+Left for Home on ChromeOS)
1155-
if (first.keysym === 0xFFE7 || first.keysym === 0xFFE8) {
1225+
if (isMetaKey(first.keysym)) {
11561226

11571227
// Defer handling until further events exist to provide context
11581228
if (eventLog.length === 1)

guacamole-common-js/src/main/webapp/modules/Mouse.js

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -510,6 +510,81 @@ Guacamole.Mouse.State.Buttons = {
510510

511511
};
512512

513+
/**
514+
* The state of all supported keyboard modifiers. This is the same structure
515+
* used for keyboard modifier state tracking.
516+
* @constructor
517+
*/
518+
Guacamole.Mouse.ModifierState = function() {
519+
520+
/**
521+
* Whether shift is currently pressed.
522+
*
523+
* @type {!boolean}
524+
*/
525+
this.shift = false;
526+
527+
/**
528+
* Whether ctrl is currently pressed.
529+
*
530+
* @type {!boolean}
531+
*/
532+
this.ctrl = false;
533+
534+
/**
535+
* Whether alt is currently pressed.
536+
*
537+
* @type {!boolean}
538+
*/
539+
this.alt = false;
540+
541+
/**
542+
* Whether meta (apple key) is currently pressed.
543+
*
544+
* @type {!boolean}
545+
*/
546+
this.meta = false;
547+
548+
/**
549+
* Whether hyper (windows key) is currently pressed.
550+
*
551+
* @type {!boolean}
552+
*/
553+
this.hyper = false;
554+
555+
};
556+
557+
/**
558+
* Returns the modifier state applicable to the mouse event given.
559+
*
560+
* @param {!MouseEvent} e
561+
* The mouse event to read.
562+
*
563+
* @returns {!Guacamole.Mouse.ModifierState}
564+
* The current state of keyboard modifiers.
565+
*/
566+
Guacamole.Mouse.ModifierState.fromMouseEvent = function(e) {
567+
568+
var state = new Guacamole.Mouse.ModifierState();
569+
570+
// Assign states from old flags
571+
state.shift = e.shiftKey;
572+
state.ctrl = e.ctrlKey;
573+
state.alt = e.altKey;
574+
state.meta = e.metaKey;
575+
576+
// Use DOM3 getModifierState() for others
577+
if (e.getModifierState) {
578+
state.hyper = e.getModifierState("OS")
579+
|| e.getModifierState("Super")
580+
|| e.getModifierState("Hyper")
581+
|| e.getModifierState("Win");
582+
}
583+
584+
return state;
585+
586+
};
587+
513588
/**
514589
* Base event type for all mouse events. The mouse producing the event may be
515590
* the user's local mouse (as with {@link Guacamole.Mouse}) or an emulated
@@ -547,6 +622,16 @@ Guacamole.Mouse.Event = function MouseEvent(type, state, events) {
547622
*/
548623
this.state = state;
549624

625+
/**
626+
* The state of all modifier keys at the time this event was received.
627+
* If the original DOM event is not a MouseEvent or modifier state is
628+
* otherwise unavailable, the state of all modifiers will be false.
629+
*
630+
* @type {!Guacamole.Mouse.ModifierState}
631+
*/
632+
var firstEvent = Array.isArray(events) ? events[0] : events;
633+
this.modifiers = firstEvent ? Guacamole.Mouse.ModifierState.fromMouseEvent(firstEvent) : new Guacamole.Mouse.ModifierState();
634+
550635
/**
551636
* @inheritdoc
552637
*/

guacamole/src/main/frontend/src/app/client/directives/guacClient.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -243,6 +243,11 @@ angular.module('client').directive('guacClient', [function guacClient() {
243243
if (!client || !display)
244244
return;
245245

246+
// Broadcast mousedown event before sending to allow keyboard to resolve
247+
// deferred modifier keys (especially CMD+click)
248+
if (event.type === 'mousedown')
249+
$rootScope.$broadcast('guacBeforeClientMouseDown', event, client);
250+
246251
event.stopPropagation();
247252
event.preventDefault();
248253

@@ -303,6 +308,11 @@ angular.module('client').directive('guacClient', [function guacClient() {
303308
if (!client || !display)
304309
return;
305310

311+
// Broadcast mousedown event before sending to allow keyboard to resolve
312+
// deferred modifier keys (especially CMD+click)
313+
if (event.type === 'mousedown')
314+
$rootScope.$broadcast('guacBeforeClientMouseDown', event, client);
315+
306316
event.stopPropagation();
307317
event.preventDefault();
308318

guacamole/src/main/frontend/src/app/index/controllers/indexController.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,11 @@ angular.module('index').controller('indexController', ['$scope', '$injector',
183183
var keyboard = new Guacamole.Keyboard($document[0]);
184184
keyboard.listenTo(sink.getElement());
185185

186+
// Resolve deferred Meta key events on mouse click to enable Cmd+Click
187+
$scope.$on('guacBeforeClientMouseDown', function(event, mouseEvent, client) {
188+
keyboard.handleMouseEvent(mouseEvent);
189+
});
190+
186191
// Broadcast keydown events
187192
keyboard.onkeydown = function onkeydown(keysym) {
188193

0 commit comments

Comments
 (0)