GUACAMOLE-724: Allow multiple tiled clients to be focused using Shift+Click and Ctrl+Click.

This commit is contained in:
Michael Jumper
2021-06-27 20:41:47 -07:00
parent 90f2270dab
commit 6f9e2a8b78
4 changed files with 218 additions and 10 deletions

View File

@@ -52,23 +52,64 @@ angular.module('client').directive('guacTiledClients', [function guacTiledClient
directive.controller = ['$scope', '$injector', '$element',
function guacTiledClientsController($scope, $injector, $element) {
// Required types
var ManagedClientGroup = $injector.get('ManagedClientGroup');
/**
* Assigns keyboard focus to the given client, allowing that client to
* receive and handle keyboard events. Multiple clients may have
* keyboard focus simultaneously.
* Returns a callback for guacClick that assigns or updates keyboard
* focus to the given client, allowing that client to receive and
* handle keyboard events. Multiple clients may have keyboard focus
* simultaneously.
*
* @param {ManagedClient} client
* The client that should receive keyboard focus.
*
* @return {guacClick~callback}
* The callback that guacClient should invoke when the given client
* has been clicked.
*/
$scope.assignFocus = function assignFocus(client) {
$scope.getFocusAssignmentCallback = function getFocusAssignmentCallback(client) {
return (shift, ctrl) => {
// Clear focus of all other clients
// Clear focus of all other clients if not selecting multiple
if (!shift && !ctrl) {
$scope.clientGroup.clients.forEach(client => {
client.clientProperties.focused = false;
});
}
client.clientProperties.focused = true;
// Fill in any gaps if performing rectangular multi-selection
// via shift-click
if (shift) {
var minRow = $scope.clientGroup.rows - 1;
var minColumn = $scope.clientGroup.columns - 1;
var maxRow = 0;
var maxColumn = 0;
// Determine extents of selected area
ManagedClientGroup.forEach($scope.clientGroup, (client, row, column) => {
if (client.clientProperties.focused) {
minRow = Math.min(minRow, row);
minColumn = Math.min(minColumn, column);
maxRow = Math.max(maxRow, row);
maxColumn = Math.max(maxColumn, column);
}
});
ManagedClientGroup.forEach($scope.clientGroup, (client, row, column) => {
client.clientProperties.focused =
row >= minRow
&& row <= maxRow
&& column >= minColumn
&& column <= maxColumn;
});
}
};
};
/**

View File

@@ -4,7 +4,7 @@
ng-repeat="client in clientGroup.clients"
ng-style="{ 'width' : getTileWidth(), 'height' : getTileHeight() }"
ng-class="{ 'focused' : client.clientProperties.focused }"
ng-click="assignFocus(client)">
guac-click="getFocusAssignmentCallback(client)">
<h3>{{ client.title }}</h3>
<guac-client client="client" emulate-absolute-mouse="emulateAbsoluteMouse"></guac-client>

View File

@@ -220,6 +220,47 @@ angular.module('client').factory('ManagedClientGroup', ['$injector', function de
};
/**
* A callback that is invoked for a ManagedClient within a ManagedClientGroup.
*
* @callback ManagedClientGroup~clientCallback
* @param {ManagedClient} client
* The relevant ManagedClient.
*
* @param {number} row
* The row number of the client within the tiled grid, where 0 is the
* first row.
*
* @param {number} column
* The column number of the client within the tiled grid, where 0 is
* the first column.
*
* @param {number} index
* The index of the client within the relevant
* {@link ManagedClientGroup#clients} array.
*/
/**
* Loops through each of the clients associated with the given
* ManagedClientGroup, invoking the given callback for each client.
*
* @param {ManagedClientGroup} group
* The ManagedClientGroup to loop through.
*
* @param {ManagedClientGroup~clientCallback} callback
* The callback to invoke for each of the clients within the given
* ManagedClientGroup.
*/
ManagedClientGroup.forEach = function forEach(group, callback) {
var current = 0;
for (var row = 0; row < group.rows; row++) {
for (var column = 0; column < group.columns; column++) {
callback(group.clients[current], row, column, current);
current++;
}
}
};
return ManagedClientGroup;
}]);

View File

@@ -0,0 +1,126 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
/**
* A directive which provides handling of click and click-like touch events.
* The state of Shift and Ctrl modifiers is tracked through these click events
* to allow for specific handling of Shift+Click and Ctrl+Click.
*/
angular.module('element').directive('guacClick', [function guacClick() {
return {
restrict: 'A',
link: function linkGuacClick($scope, $element, $attrs) {
/**
* A callback that is invoked by the guacClick directive when a
* click or click-like event is received.
*
* @callback guacClick~callback
* @param {boolean} shift
* Whether Shift was held down at the time the click occurred.
*
* @param {boolean} ctrl
* Whether Ctrl or Meta (the Mac "Command" key) was held down
* at the time the click occurred.
*/
/**
* The callback to invoke when a click or click-like event is
* received on the assocaited element.
*
* @type guacClick~callback
*/
var guacClick = $scope.$eval($attrs.guacClick);
/**
* The element which will register the click.
*
* @type Element
*/
var element = $element[0];
/**
* Whether either Shift key is currently pressed.
*
* @type boolean
*/
var shift = false;
/**
* Whether either Ctrl key is currently pressed. To allow the
* Command key to be used on Mac platforms, this flag also
* considers the state of either Meta key.
*
* @type boolean
*/
var ctrl = false;
/**
* Updates the state of the {@link shift} and {@link ctrl} flags
* based on which keys are currently marked as held down by the
* given Guacamole.Keyboard.
*
* @param {Guacamole.Keyboard} keyboard
* The Guacamole.Keyboard instance to read key states from.
*/
var updateModifiers = function updateModifiers(keyboard) {
shift = !!(
keyboard.pressed[0xFFE1] // Left shift
|| keyboard.pressed[0xFFE2] // Right shift
);
ctrl = !!(
keyboard.pressed[0xFFE3] // Left ctrl
|| keyboard.pressed[0xFFE4] // Right ctrl
|| keyboard.pressed[0xFFE7] // Left meta (command)
|| keyboard.pressed[0xFFE8] // Right meta (command)
);
};
// Update tracking of modifier states for each key press
$scope.$on('guacKeydown', function keydownListener(event, keysym, keyboard) {
updateModifiers(keyboard);
});
// Update tracking of modifier states for each key release
$scope.$on('guacKeyup', function keyupListener(event, keysym, keyboard) {
updateModifiers(keyboard);
});
// Fire provided callback for each mouse-initiated "click" event ...
element.addEventListener('click', function elementClicked(e) {
if (element.contains(e.target))
$scope.$apply(() => guacClick(shift, ctrl));
});
// ... and for touch-initiated click-like events
element.addEventListener('touchstart', function elementClicked(e) {
if (element.contains(e.target))
$scope.$apply(() => guacClick(shift, ctrl));
});
} // end guacClick link function
};
}]);