diff --git a/guacamole/src/main/frontend/src/app/client/controllers/clientController.js b/guacamole/src/main/frontend/src/app/client/controllers/clientController.js index 1bc5dd6fa..389170745 100644 --- a/guacamole/src/main/frontend/src/app/client/controllers/clientController.js +++ b/guacamole/src/main/frontend/src/app/client/controllers/clientController.js @@ -279,20 +279,37 @@ angular.module('client').controller('clientController', ['$scope', '$routeParams /** * The client which should be attached to the client UI. * - * @type ManagedClient + * @type ManagedClient[] */ - $scope.client = guacClientManager.getManagedClient($routeParams.id, $routeParams.params); + $scope.clients = (function getClients() { + + var clients = []; + + var ids = $routeParams.id.split(/[ +]/); + ids.forEach(function addClient(id) { + clients.push(guacClientManager.getManagedClient(id, $routeParams.params)); + }); + + return clients; + + })(); /** - * All active clients which are not the current client ($scope.client). + * All active clients which are not any current client ($scope.clients). * Each key is the ID of the connection used by that client. * * @type Object. */ $scope.otherClients = (function getOtherClients(clients) { + var otherClients = angular.extend({}, clients); - delete otherClients[$scope.client.id]; + + $scope.clients.forEach(function removeActiveCLient(client) { + delete otherClients[client.id]; + }); + return otherClients; + })(guacClientManager.getManagedClients()); /** @@ -526,7 +543,9 @@ angular.module('client').controller('clientController', ['$scope', '$routeParams $scope.menu.connectionParameters = ManagedClient.getArgumentModel($scope.client); // Disable client keyboard if the menu is shown - $scope.client.clientProperties.keyboardEnabled = !menuShown; + angular.forEach($scope.clients, function updateKeyboardEnabled(client) { + client.clientProperties.keyboardEnabled = !menuShown; + }); }); @@ -731,7 +750,15 @@ angular.module('client').controller('clientController', ['$scope', '$routeParams * otherwise. */ $scope.isConnectionUnstable = function isConnectionUnstable() { - return $scope.client && $scope.client.clientState.tunnelUnstable; + + var unstable = false; + + angular.forEach($scope.clients, function checkStability(client) { + unstable |= client.clientState.tunnelUnstable; + }); + + return unstable; + }; /** diff --git a/guacamole/src/main/frontend/src/app/client/directives/guacClient.js b/guacamole/src/main/frontend/src/app/client/directives/guacClient.js index 841d39271..6e47c26b2 100644 --- a/guacamole/src/main/frontend/src/app/client/directives/guacClient.js +++ b/guacamole/src/main/frontend/src/app/client/directives/guacClient.js @@ -421,7 +421,7 @@ angular.module('client').directive('guacClient', [function guacClient() { // Translate local keydown events to remote keydown events if keyboard is enabled $scope.$on('guacKeydown', function keydownListener(event, keysym, keyboard) { - if ($scope.client.clientProperties.keyboardEnabled && !event.defaultPrevented) { + if ($scope.client.clientProperties.keyboardEnabled && $scope.client.clientProperties.focused) { client.sendKeyEvent(1, keysym); event.preventDefault(); } @@ -429,7 +429,7 @@ angular.module('client').directive('guacClient', [function guacClient() { // Translate local keyup events to remote keyup events if keyboard is enabled $scope.$on('guacKeyup', function keyupListener(event, keysym, keyboard) { - if ($scope.client.clientProperties.keyboardEnabled && !event.defaultPrevented) { + if ($scope.client.clientProperties.keyboardEnabled && $scope.client.clientProperties.focused) { client.sendKeyEvent(0, keysym); event.preventDefault(); } @@ -437,12 +437,14 @@ angular.module('client').directive('guacClient', [function guacClient() { // Universally handle all synthetic keydown events $scope.$on('guacSyntheticKeydown', function syntheticKeydownListener(event, keysym) { - client.sendKeyEvent(1, keysym); + if ($scope.client.clientProperties.focused) + client.sendKeyEvent(1, keysym); }); // Universally handle all synthetic keyup events $scope.$on('guacSyntheticKeyup', function syntheticKeyupListener(event, keysym) { - client.sendKeyEvent(0, keysym); + if ($scope.client.clientProperties.focused) + client.sendKeyEvent(0, keysym); }); /** diff --git a/guacamole/src/main/frontend/src/app/client/directives/guacTiledClients.js b/guacamole/src/main/frontend/src/app/client/directives/guacTiledClients.js new file mode 100644 index 000000000..83f8b25e6 --- /dev/null +++ b/guacamole/src/main/frontend/src/app/client/directives/guacTiledClients.js @@ -0,0 +1,145 @@ +/* + * 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 displays one or more Guacamole clients in an evenly-tiled + * view. The number of rows and columns used for the arrangement of tiles is + * automatically determined by the number of clients present. + */ +angular.module('client').directive('guacTiledClients', [function guacTiledClients() { + + var directive = { + restrict: 'E', + templateUrl: 'app/client/templates/guacTiledClients.html', + }; + + directive.scope = { + + /** + * The Guacamole clients that should be displayed in an evenly-tiled + * grid arrangement. + * + * @type ManagedClient[] + */ + clients : '=' + + }; + + directive.controller = ['$scope', '$injector', '$element', + function guacTiledListController($scope, $injector, $element) { + + /** + * Returns the number of columns that should be used to evenly arrange + * all provided clients in a tiled grid. + * + * @returns {Number} + * The number of columns that should be used for the grid of + * clients. + */ + var getColumns = function getColumns() { + + if (!$scope.clients || !$scope.clients.length) + return 0; + + return Math.ceil(Math.sqrt($scope.clients.length)); + + }; + + /** + * Returns the number of rows that should be used to evenly arrange all + * provided clients in a tiled grid. + * + * @returns {Number} + * The number of rows that should be used for the grid of clients. + */ + var getRows = function getRows() { + + if (!$scope.clients || !$scope.clients.length) + return 0; + + return Math.ceil($scope.clients.length / getColumns()); + + }; + + /** + * Assigns 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. + */ + $scope.assignFocus = function assignFocus(client) { + client.clientProperties.focused = true; + }; + + /** + * Returns whether multiple clients are currently shown within the + * tiled grid. + * + * @returns {Boolean} + * true if two or more clients are currently present, false + * otherwise. + */ + $scope.hasMultipleClients = function hasMultipleClients() { + return $scope.clients && $scope.clients.length > 1; + }; + + /** + * Returns the CSS width that should be applied to each tile to + * achieve an even arrangement. + * + * @returns {String} + * The CSS width that should be applied to each tile. + */ + $scope.getTileWidth = function getTileWidth() { + return Math.floor(100 / getColumns()) + '%'; + }; + + /** + * Returns the CSS height that should be applied to each tile to + * achieve an even arrangement. + * + * @returns {String} + * The CSS height that should be applied to each tile. + */ + $scope.getTileHeight = function getTileHeight() { + return Math.floor(100 / getRows()) + '%'; + }; + + /** + * Returns the display title of the given Guacamole client. If the + * title is not yet known, a placeholder title will be returned. + * + * @param {ManagedClient} client + * The client whose title should be retrieved. + * + * @returns {String} + * The title of the given client, or a placeholder title if the + * client's title is not yet known. + */ + $scope.getClientTitle = function getClientTitle(client) { + return client.title || '...'; + }; + + }]; + + return directive; + +}]); diff --git a/guacamole/src/main/frontend/src/app/client/styles/client.css b/guacamole/src/main/frontend/src/app/client/styles/client.css index 9ec74b6b2..16bd2d4a7 100644 --- a/guacamole/src/main/frontend/src/app/client/styles/client.css +++ b/guacamole/src/main/frontend/src/app/client/styles/client.css @@ -103,7 +103,7 @@ body.client { flex: 0 0 auto; } -.client-view .client-body .main { +.client-view .client-body .tiled-client-list { position: absolute; left: 0; diff --git a/guacamole/src/main/frontend/src/app/client/styles/tiled-client-list.css b/guacamole/src/main/frontend/src/app/client/styles/tiled-client-list.css new file mode 100644 index 000000000..1b89de080 --- /dev/null +++ b/guacamole/src/main/frontend/src/app/client/styles/tiled-client-list.css @@ -0,0 +1,60 @@ +/* + * 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. + */ + +.tiled-client-list { + padding: 0; + margin: 0; + line-height: 0; +} + +.tiled-client-list li.client-tile { + position: relative; + display: inline-flex; + flex-direction: column; + line-height: 1.5; +} + +.tiled-client-list li.client-tile h3 { + margin: 0; + background: #444; + padding: 0 0.25em; + font-size: 0.8em; + color: white; + display: none; +} + +.tiled-client-list.multiple-clients li.client-tile h3 { + display: block; +} + +.tiled-client-list.multiple-clients li.client-tile { + border: 1px solid #444; +} + +.tiled-client-list li.client-tile.focused { + border-color: #3161a9; +} + +.tiled-client-list li.client-tile.focused h3 { + background-color: #3161a9; +} + +.tiled-client-list li.client-tile .main { + flex: 1; +} diff --git a/guacamole/src/main/frontend/src/app/client/templates/client.html b/guacamole/src/main/frontend/src/app/client/templates/client.html index edc5cb665..17f0c502f 100644 --- a/guacamole/src/main/frontend/src/app/client/templates/client.html +++ b/guacamole/src/main/frontend/src/app/client/templates/client.html @@ -8,8 +8,8 @@
- - + +
diff --git a/guacamole/src/main/frontend/src/app/client/templates/connection.html b/guacamole/src/main/frontend/src/app/client/templates/connection.html index 7ddc72bfa..5778f81a0 100644 --- a/guacamole/src/main/frontend/src/app/client/templates/connection.html +++ b/guacamole/src/main/frontend/src/app/client/templates/connection.html @@ -1,4 +1,5 @@
+ {{item.name}}
diff --git a/guacamole/src/main/frontend/src/app/client/templates/connectionGroup.html b/guacamole/src/main/frontend/src/app/client/templates/connectionGroup.html index 680491c3b..5b33b7a8c 100644 --- a/guacamole/src/main/frontend/src/app/client/templates/connectionGroup.html +++ b/guacamole/src/main/frontend/src/app/client/templates/connectionGroup.html @@ -1,4 +1,5 @@
+ {{item.name}}
diff --git a/guacamole/src/main/frontend/src/app/client/templates/guacTiledClients.html b/guacamole/src/main/frontend/src/app/client/templates/guacTiledClients.html new file mode 100644 index 000000000..f1ceb64ac --- /dev/null +++ b/guacamole/src/main/frontend/src/app/client/templates/guacTiledClients.html @@ -0,0 +1,13 @@ +
    + +
  • + +

    {{ getClientTitle(client) }}

    + +
  • + +
\ No newline at end of file diff --git a/guacamole/src/main/frontend/src/app/client/types/ClientProperties.js b/guacamole/src/main/frontend/src/app/client/types/ClientProperties.js index d5940e2be..a138812b2 100644 --- a/guacamole/src/main/frontend/src/app/client/types/ClientProperties.js +++ b/guacamole/src/main/frontend/src/app/client/types/ClientProperties.js @@ -69,12 +69,20 @@ angular.module('client').factory('ClientProperties', ['$injector', function defi this.maxScale = template.maxScale || 3; /** - * Whether or not the client should listen to keyboard events. + * Whether this client should listen to keyboard events that it + * receives. * * @type Boolean */ this.keyboardEnabled = template.keyboardEnabled || true; - + + /** + * Whether this client should receive keyboard events. + * + * @type Boolean + */ + this.focused = template.focused || false; + /** * Whether translation of touch to mouse events should emulate an * absolute pointer device, or a relative pointer device.