Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 57 additions & 1 deletion guacamole-common-js/src/main/webapp/modules/Keyboard.js
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,14 @@ Guacamole.Keyboard = function Keyboard(element) {
*/
this.onkeyup = null;

/**
* Whether pressed Meta keys should be treated as Ctrl keys. When true,
* shortcuts using Meta will behave as if Ctrl were pressed.
*
* @type {!boolean}
*/
this.metaToCtrl = false;

/**
* 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
Expand Down Expand Up @@ -136,6 +144,33 @@ Guacamole.Keyboard = function Keyboard(element) {

}

/**
* Translates Meta keysyms to their Ctrl equivalents if configured.
*
* @private
* @param {!number} keysym
* The keysym to potentially translate.
*
* @returns {!number}
* The translated keysym.
*/
var mapMetaToCtrlKeysym = function mapMetaToCtrlKeysym(keysym) {

// Preserve original keysyms unless Meta-to-Ctrl translation is enabled
if (!guac_keyboard.metaToCtrl)
return keysym;

// Translate left/right Meta to left/right Ctrl
if (keysym === 0xFFE7)
return 0xFFE3;

if (keysym === 0xFFE8)
return 0xFFE4;

return keysym;

};

/**
* A key event having a corresponding timestamp. This event is non-specific.
* Its subclasses should be used instead when recording specific key
Expand Down Expand Up @@ -200,6 +235,21 @@ Guacamole.Keyboard = function Keyboard(element) {
*/
this.modifiers = orig ? Guacamole.Keyboard.ModifierState.fromKeyboardEvent(orig) : new Guacamole.Keyboard.ModifierState();

/**
* Whether Meta is currently physically pressed, prior to any
* metaToCtrl translation.
*
* @type {!boolean}
*/
this.physicalMeta = this.modifiers.meta;

// Treat Meta as Ctrl when configured, while preserving the physical
// Meta state separately for browser-quirk handling.
if (guac_keyboard.metaToCtrl && this.modifiers.meta) {
this.modifiers.ctrl = true;
this.modifiers.meta = false;
}

/**
* An arbitrary timestamp in milliseconds, indicating this event's
* position in time relative to other events.
Expand Down Expand Up @@ -286,7 +336,7 @@ Guacamole.Keyboard = function Keyboard(element) {

// If a key is pressed while meta is held down, the keyup will
// never be sent in Chrome (bug #108404)
if (this.modifiers.meta && this.keysym !== 0xFFE7 && this.keysym !== 0xFFE8)
if (this.physicalMeta && this.keysym !== 0xFFE7 && this.keysym !== 0xFFE8)
this.keyupReliable = false;

// We cannot rely on receiving keyup for Caps Lock on certain platforms
Expand All @@ -311,6 +361,9 @@ Guacamole.Keyboard = function Keyboard(element) {
|| this.modifiers.hyper)
this.reliable = true;

// Translate Meta keys to Ctrl keys if requested.
this.keysym = mapMetaToCtrlKeysym(this.keysym);

// Record most recently known keysym by associated key code
recentKeysym[this.keyCode] = this.keysym;

Expand Down Expand Up @@ -373,6 +426,9 @@ Guacamole.Keyboard = function Keyboard(element) {
this.keysym = keysym_from_keycode(this.keyCode, this.location)
|| keysym_from_key_identifier(this.key, this.location);

// Translate Meta keys to Ctrl keys if requested.
this.keysym = mapMetaToCtrlKeysym(this.keysym);

// Fall back to the most recently pressed keysym associated with the
// keyCode if the inferred key doesn't seem to actually be pressed
if (!guac_keyboard.pressed[this.keysym])
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,11 @@
"name" : "read-only",
"type" : "BOOLEAN",
"options" : [ "true" ]
},
{
"name" : "meta-to-ctrl",
"type" : "BOOLEAN",
"options" : [ "true" ]
}
]
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,11 @@
"name" : "read-only",
"type" : "BOOLEAN",
"options" : [ "true" ]
},
{
"name" : "meta-to-ctrl",
"type" : "BOOLEAN",
"options" : [ "true" ]
}
]
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,11 @@
"type" : "BOOLEAN",
"options" : [ "true" ]
},
{
"name" : "meta-to-ctrl",
"type" : "BOOLEAN",
"options" : [ "true" ]
},
{
"name" : "disable-server-input",
"type" : "BOOLEAN",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ angular.module('client').controller('clientController', ['$scope', '$routeParams

// Required types
const ConnectionGroup = $injector.get('ConnectionGroup');
const ClientIdentifier = $injector.get('ClientIdentifier');
const ManagedClient = $injector.get('ManagedClient');
const ManagedClientGroup = $injector.get('ManagedClientGroup');
const ManagedClientState = $injector.get('ManagedClientState');
Expand All @@ -37,6 +38,7 @@ angular.module('client').controller('clientController', ['$scope', '$routeParams
const authenticationService = $injector.get('authenticationService');
const connectionGroupService = $injector.get('connectionGroupService');
const clipboardService = $injector.get('clipboardService');
const connectionService = $injector.get('connectionService');
const dataSourceService = $injector.get('dataSourceService');
const guacClientManager = $injector.get('guacClientManager');
const guacFullscreen = $injector.get('guacFullscreen');
Expand Down Expand Up @@ -483,6 +485,75 @@ angular.module('client').controller('clientController', ['$scope', '$routeParams
// Hide the menu when the guacClientHideMenu event is received
$scope.$on('guacHideMenu', () => $scope.menu.shown = false);

/**
* Connection options that affect browser client behavior.
*
* @type {!Array.<{event: string, defaultValue: *, getValue: function(*): *}>}
*/
const connectionClientBehaviorDefinitions = [{
event : 'guacMetaToCtrlChanged',
defaultValue : false,
getValue : function getValue(connection) {
return connection.metaToCtrl === true;
}
}];

/**
* Emits all connection client behavior options.
*
* @param {*} [connection]
* Connection object returned by connectionService.
*/
const applyConnectionClientBehavior = function applyConnectionClientBehavior(connection) {
connectionClientBehaviorDefinitions.forEach(function applyDefinition(definition) {
const value = connection ?
definition.getValue(connection) : definition.defaultValue;

$scope.$emit(definition.event, value);
});
};

/**
* Refreshes connection-derived client behavior for the currently-focused
* client.
*
* @param {ManagedClient} focusedClient
* The currently-focused client, if any.
*/
const updateConnectionClientBehavior = function updateConnectionClientBehavior(focusedClient) {

// Immediately clear previous connection behavior to defaults.
applyConnectionClientBehavior();

if (!focusedClient)
return;

const identifier = ClientIdentifier.fromString(focusedClient.id);
if (identifier.type !== ClientIdentifier.Types.CONNECTION)
return;

// Guard async callbacks: only apply behavior if this client is still focused.
const focusedClientId = focusedClient.id;

connectionService.getConnection(identifier.dataSource, identifier.id)
.then(function connectionRetrieved(connection) {

if (!$scope.focusedClient || $scope.focusedClient.id !== focusedClientId)
return;

applyConnectionClientBehavior(connection);

}, function connectionRetrievalFailed() {

if (!$scope.focusedClient || $scope.focusedClient.id !== focusedClientId)
return;

applyConnectionClientBehavior();

});

};

// Automatically track and cache the currently-focused client
$scope.$on('guacClientFocused', function focusedClientChanged(event, newFocusedClient) {

Expand All @@ -498,6 +569,9 @@ angular.module('client').controller('clientController', ['$scope', '$routeParams
$scope.menu.connectionParameters = newFocusedClient ?
ManagedClient.getArgumentModel(newFocusedClient) : {};

// Keep client behavior synced with focused connection configuration
updateConnectionClientBehavior(newFocusedClient);

});

// Automatically update connection parameters that have been modified
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,12 @@ angular.module('index').controller('indexController', ['$scope', '$injector',
// Create event listeners at the global level
var keyboard = new Guacamole.Keyboard($document[0]);
keyboard.listenTo(sink.getElement());
keyboard.metaToCtrl = false;

// Track whether Meta should be treated as Ctrl for the active connection
$scope.$on('guacMetaToCtrlChanged', function metaToCtrlChanged(event, enabled) {
keyboard.metaToCtrl = !!enabled;
});

// Broadcast keydown events
keyboard.onkeydown = function onkeydown(keysym) {
Expand Down
10 changes: 9 additions & 1 deletion guacamole/src/main/frontend/src/app/rest/types/Connection.js
Original file line number Diff line number Diff line change
Expand Up @@ -113,8 +113,16 @@ angular.module('rest').factory('Connection', [function defineConnection() {
*/
this.lastActive = template.lastActive;

/**
* Whether the web client should treat Meta as Ctrl for this
* connection.
*
* @type Boolean
*/
this.metaToCtrl = !!template.metaToCtrl;

};

return Connection;

}]);
}]);
3 changes: 3 additions & 0 deletions guacamole/src/main/frontend/src/translations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -627,6 +627,7 @@
"FIELD_HEADER_PRINTER_NAME" : "Redirected printer name:",
"FIELD_HEADER_PRECONNECTION_BLOB" : "Preconnection BLOB (VM ID):",
"FIELD_HEADER_PRECONNECTION_ID" : "RDP source ID:",
"FIELD_HEADER_META_TO_CTRL" : "Treat Meta as Ctrl:",
"FIELD_HEADER_READ_ONLY" : "Read-only:",
"FIELD_HEADER_RECORDING_WRITE_EXISTING" : "@:APP.FIELD_HEADER_RECORDING_WRITE_EXISTING",
"FIELD_HEADER_RECORDING_EXCLUDE_MOUSE" : "Exclude mouse:",
Expand Down Expand Up @@ -753,6 +754,7 @@
"FIELD_HEADER_PORT" : "Port:",
"FIELD_HEADER_PRIVATE_KEY" : "Private key:",
"FIELD_HEADER_PUBLIC_KEY" : "Public key:",
"FIELD_HEADER_META_TO_CTRL" : "Treat Meta as Ctrl:",
"FIELD_HEADER_SCROLLBACK" : "Maximum scrollback size:",
"FIELD_HEADER_READ_ONLY" : "Read-only:",
"FIELD_HEADER_RECORDING_WRITE_EXISTING" : "@:APP.FIELD_HEADER_RECORDING_WRITE_EXISTING",
Expand Down Expand Up @@ -933,6 +935,7 @@
"FIELD_HEADER_PASSWORD" : "Password:",
"FIELD_HEADER_PORT" : "Port:",
"FIELD_HEADER_QUALITY_LEVEL" : "Display quality:",
"FIELD_HEADER_META_TO_CTRL" : "Treat Meta as Ctrl:",
"FIELD_HEADER_READ_ONLY" : "Read-only:",
"FIELD_HEADER_RECORDING_WRITE_EXISTING" : "@:APP.FIELD_HEADER_RECORDING_WRITE_EXISTING",
"FIELD_HEADER_RECORDING_EXCLUDE_MOUSE" : "Exclude mouse:",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,31 @@
@JsonInclude(value=Include.NON_NULL)
public class APIConnection {

/**
* The name of the connection parameter that controls whether Meta should
* be treated as Ctrl by the web client.
*/
private static final String META_TO_CTRL_PARAMETER_NAME = "meta-to-ctrl";

/**
* Returns whether the given boolean-like connection parameter is enabled.
*
* @param configuration
* The configuration containing the parameter.
*
* @param parameterName
* The name of the parameter to evaluate.
*
* @return
* true if the parameter value parses as true, false otherwise.
*/
private static boolean isEnabledParameter(GuacamoleConfiguration configuration,
String parameterName) {
if (configuration == null)
return false;
return Boolean.parseBoolean(configuration.getParameter(parameterName));
}

/**
* The name of this connection.
*/
Expand Down Expand Up @@ -84,6 +109,11 @@ public class APIConnection {
*/
private Date lastActive;

/**
* Whether Meta should be treated as Ctrl by the web client.
*/
private boolean metaToCtrl;

/**
* Create an empty APIConnection.
*/
Expand All @@ -110,6 +140,9 @@ public APIConnection(Connection connection)
// Set protocol from configuration
GuacamoleConfiguration configuration = connection.getConfiguration();
this.protocol = configuration.getProtocol();

// Set client behavior options from configuration
this.metaToCtrl = isEnabledParameter(configuration, META_TO_CTRL_PARAMETER_NAME);

// Associate any attributes
this.attributes = connection.getAttributes();
Expand Down Expand Up @@ -289,4 +322,24 @@ public void setLastActive(Date lastActive) {
this.lastActive = lastActive;
}

/**
* Returns whether Meta should be treated as Ctrl by the web client.
*
* @return
* true if Meta should be treated as Ctrl, false otherwise.
*/
public boolean isMetaToCtrl() {
return metaToCtrl;
}

/**
* Sets whether Meta should be treated as Ctrl by the web client.
*
* @param metaToCtrl
* true if Meta should be treated as Ctrl, false otherwise.
*/
public void setMetaToCtrl(boolean metaToCtrl) {
this.metaToCtrl = metaToCtrl;
}

}