diff --git a/guacamole/src/main/webapp/app/client/directives/guacClient.js b/guacamole/src/main/webapp/app/client/directives/guacClient.js index 4216b4ea2..4b24593df 100644 --- a/guacamole/src/main/webapp/app/client/directives/guacClient.js +++ b/guacamole/src/main/webapp/app/client/directives/guacClient.js @@ -229,6 +229,12 @@ angular.module('client').directive('guacClient', [function guacClient() { displayElement = display.getElement(); displayContainer.appendChild(displayElement); + // Do nothing when the display element is clicked on + display.getElement().onclick = function(e) { + e.preventDefault(); + return false; + }; + }); // Update actual view scrollLeft when scroll properties change diff --git a/guacamole/src/main/webapp/app/client/directives/guacThumbnail.js b/guacamole/src/main/webapp/app/client/directives/guacThumbnail.js new file mode 100644 index 000000000..06a6c3c4f --- /dev/null +++ b/guacamole/src/main/webapp/app/client/directives/guacThumbnail.js @@ -0,0 +1,199 @@ +/* + * Copyright (C) 2014 Glyptodon LLC + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +/** + * A directive for displaying a Guacamole client as a non-interactive + * thumbnail. + */ +angular.module('client').directive('guacThumbnail', [function guacThumbnail() { + + return { + // Element only + restrict: 'E', + replace: true, + scope: { + + /** + * The client to display within this guacThumbnail directive. + * + * @type ManagedClient + */ + client : '=' + + }, + templateUrl: 'app/client/templates/guacThumbnail.html', + controller: ['$scope', '$injector', '$element', function guacThumbnailController($scope, $injector, $element) { + + // Required services + var $window = $injector.get('$window'); + + /** + * The optimal thumbnail width, in pixels. + * + * @type Number + */ + var THUMBNAIL_WIDTH = 320; + + /** + * The optimal thumbnail height, in pixels. + * + * @type Number + */ + var THUMBNAIL_HEIGHT = 240; + + /** + * The current Guacamole client instance. + * + * @type Guacamole.Client + */ + var client = null; + + /** + * The display of the current Guacamole client instance. + * + * @type Guacamole.Display + */ + var display = null; + + /** + * The element associated with the display of the current + * Guacamole client instance. + * + * @type Element + */ + var displayElement = null; + + /** + * The element which must contain the Guacamole display element. + * + * @type Element + */ + var displayContainer = $element.find('.display')[0]; + + /** + * The main containing element for the entire directive. + * + * @type Element + */ + var main = $element[0]; + + /** + * The element which functions as a detector for size changes. + * + * @type Element + */ + var resizeSensor = $element.find('.resize-sensor')[0]; + + /** + * Updates the scale of the attached Guacamole.Client based on current window + * size and "auto-fit" setting. + */ + var updateDisplayScale = function updateDisplayScale() { + + if (!display) return; + + // Fit within available area + display.scale(Math.min( + main.offsetWidth / Math.max(display.getWidth(), 1), + main.offsetHeight / Math.max(display.getHeight(), 1) + )); + + }; + + // Attach any given managed client + $scope.$watch('client', function attachManagedClient(managedClient) { + + // Remove any existing display + displayContainer.innerHTML = ""; + + // Only proceed if a client is given + if (!managedClient) + return; + + // Get Guacamole client instance + client = managedClient.client; + + // Attach possibly new display + display = client.getDisplay(); + + // Add display element + displayElement = display.getElement(); + displayContainer.appendChild(displayElement); + + }); + + // Update scale when display is resized + $scope.$watch('client.managedDisplay.size', function setDisplaySize(size) { + + var width; + var height; + + // If no display size yet, assume optimal thumbnail size + if (!size || size.width === 0 || size.height === 0) { + width = THUMBNAIL_WIDTH; + height = THUMBNAIL_HEIGHT; + } + + // Otherwise, generate size that fits within thumbnail bounds + else { + var scale = Math.min(THUMBNAIL_WIDTH / size.width, THUMBNAIL_HEIGHT / size.height, 1); + width = size.width * scale; + height = size.height * scale; + } + + // Generate dummy background image + var thumbnail = document.createElement("canvas"); + thumbnail.width = width; + thumbnail.height = height; + $scope.thumbnail = thumbnail.toDataURL("image/png"); + + $scope.$evalAsync(updateDisplayScale); + + }); + + // If the element is resized, attempt to resize client + resizeSensor.contentWindow.addEventListener('resize', function mainElementResized() { + + // Send new display size, if changed + if (client && display) { + + var pixelDensity = $window.devicePixelRatio || 1; + var width = main.offsetWidth * pixelDensity; + var height = main.offsetHeight * pixelDensity; + + if (display.getWidth() !== width || display.getHeight() !== height) + client.sendSize(width, height); + + } + + $scope.$apply(updateDisplayScale); + + }); + + // Do not allow nested elements to prevent handling of click events + main.addEventListener('click', function preventClickPropagation(e) { + e.stopPropagation(); + }, true); + + }] + }; +}]); \ No newline at end of file diff --git a/guacamole/src/main/webapp/app/client/styles/thumbnail-display.css b/guacamole/src/main/webapp/app/client/styles/thumbnail-display.css new file mode 100644 index 000000000..405ece1d8 --- /dev/null +++ b/guacamole/src/main/webapp/app/client/styles/thumbnail-display.css @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2013 Glyptodon LLC + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +div.thumbnail-main { + overflow: hidden; + width: 100%; + height: 100%; + position: relative; + font-size: 0px; +} + +.thumbnail-main img { + max-width: 100%; +} + +.thumbnail-main .display { + position: absolute; +} \ No newline at end of file diff --git a/guacamole/src/main/webapp/app/client/templates/guacThumbnail.html b/guacamole/src/main/webapp/app/client/templates/guacThumbnail.html new file mode 100644 index 000000000..14f85b6e7 --- /dev/null +++ b/guacamole/src/main/webapp/app/client/templates/guacThumbnail.html @@ -0,0 +1,34 @@ +
+ + + + + + +
+
+ + + + +
\ No newline at end of file diff --git a/guacamole/src/main/webapp/app/client/types/ManagedDisplay.js b/guacamole/src/main/webapp/app/client/types/ManagedDisplay.js index c70423f70..ad8482459 100644 --- a/guacamole/src/main/webapp/app/client/types/ManagedDisplay.js +++ b/guacamole/src/main/webapp/app/client/types/ManagedDisplay.js @@ -168,12 +168,6 @@ angular.module('client').factory('ManagedDisplay', ['$rootScope', }); }; - // Do nothing when the display element is clicked on - display.getElement().onclick = function(e) { - e.preventDefault(); - return false; - }; - return managedDisplay; }; diff --git a/guacamole/src/main/webapp/app/home/directives/guacRecentConnections.js b/guacamole/src/main/webapp/app/home/directives/guacRecentConnections.js index a819bc5b4..3b7f522ce 100644 --- a/guacamole/src/main/webapp/app/home/directives/guacRecentConnections.js +++ b/guacamole/src/main/webapp/app/home/directives/guacRecentConnections.js @@ -42,9 +42,50 @@ angular.module('home').directive('guacRecentConnections', [function guacRecentCo }, templateUrl: 'app/home/templates/guacRecentConnections.html', - controller: ['$scope', '$injector', 'guacHistory', 'RecentConnection', - function guacRecentConnectionsController($scope, $injector, guacHistory, RecentConnection) { + controller: ['$scope', '$injector', function guacRecentConnectionsController($scope, $injector) { + // Required types + var ActiveConnection = $injector.get('ActiveConnection'); + var RecentConnection = $injector.get('RecentConnection'); + + // Required services + var guacClientManager = $injector.get('guacClientManager'); + var guacHistory = $injector.get('guacHistory'); + + /** + * Array of all known and visible active connections. + * + * @type ActiveConnection[] + */ + $scope.activeConnections = []; + + /** + * Array of all known and visible recently-used connections. + * + * @type RecentConnection[] + */ + $scope.recentConnections = []; + + /** + * Returns whether recent connections are available for display. + * Note that, for the sake of this directive, recent connections + * include any currently-active connections, even if they are not + * yet in the history. + * + * @returns {Boolean} + * true if recent (or active) connections are present, false + * otherwise. + */ + $scope.hasRecentConnections = function hasRecentConnections() { + return !!($scope.activeConnections.length || $scope.recentConnections.length); + }; + + /** + * Map of all visible objects, connections or connection groups, by + * object identifier. + * + * @type Object. + */ var visibleObjects = {}; /** @@ -87,6 +128,8 @@ angular.module('home').directive('guacRecentConnections', [function guacRecentCo // Update visible objects when root group is set $scope.$watch("rootGroup", function setRootGroup(rootGroup) { + // Clear connection arrays + $scope.activeConnections = []; $scope.recentConnections = []; // Produce collection of visible objects @@ -94,11 +137,27 @@ angular.module('home').directive('guacRecentConnections', [function guacRecentCo if (rootGroup) addVisibleConnectionGroup(rootGroup); + // Add all active connections + for (var id in guacClientManager.managedClients) { + + // Get corresponding managed client + var client = guacClientManager.managedClients[id]; + + // Add active connections for clients with associated visible objects + if (id in visibleObjects) { + + var object = visibleObjects[id]; + $scope.activeConnections.push(new ActiveConnection(object.name, client)); + + } + + } + // Add any recent connections that are visible guacHistory.recentConnections.forEach(function addRecentConnection(historyEntry) { // Add recent connections for history entries with associated visible objects - if (historyEntry.id in visibleObjects) { + if (historyEntry.id in visibleObjects && !(historyEntry.id in guacClientManager.managedClients)) { var object = visibleObjects[historyEntry.id]; $scope.recentConnections.push(new RecentConnection(object.name, historyEntry)); diff --git a/guacamole/src/main/webapp/app/home/homeModule.js b/guacamole/src/main/webapp/app/home/homeModule.js index a3c840e21..146ad4ced 100644 --- a/guacamole/src/main/webapp/app/home/homeModule.js +++ b/guacamole/src/main/webapp/app/home/homeModule.js @@ -20,4 +20,4 @@ * THE SOFTWARE. */ -angular.module('home', ['history', 'groupList', 'rest']); +angular.module('home', ['client', 'history', 'groupList', 'rest']); diff --git a/guacamole/src/main/webapp/app/home/templates/guacRecentConnections.html b/guacamole/src/main/webapp/app/home/templates/guacRecentConnections.html index 763cda747..07cf4b52b 100644 --- a/guacamole/src/main/webapp/app/home/templates/guacRecentConnections.html +++ b/guacamole/src/main/webapp/app/home/templates/guacRecentConnections.html @@ -22,11 +22,28 @@ --> -

{{'HOME.INFO_NO_RECENT_CONNECTIONS' | translate}}

+

{{'HOME.INFO_NO_RECENT_CONNECTIONS' | translate}}

+ +
+ + + +
+ +
+ + +
+ {{activeConnection.name}} +
+ +
+
+
- +
diff --git a/guacamole/src/main/webapp/app/home/types/ActiveConnection.js b/guacamole/src/main/webapp/app/home/types/ActiveConnection.js new file mode 100644 index 000000000..aef6a9128 --- /dev/null +++ b/guacamole/src/main/webapp/app/home/types/ActiveConnection.js @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2014 Glyptodon LLC + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +/** + * Provides the ActiveConnection class used by the guacRecentConnections + * directive. + */ +angular.module('home').factory('ActiveConnection', [function defineActiveConnection() { + + /** + * A recently-user connection, visible to the current user, with an + * associated history entry. + * + * @constructor + */ + var ActiveConnection = function ActiveConnection(name, client) { + + /** + * The human-readable name of this connection. + * + * @type String + */ + this.name = name; + + /** + * The client associated with this active connection. + * + * @type ManagedClient + */ + this.client = client; + + }; + + return ActiveConnection; + +}]); diff --git a/guacamole/src/main/webapp/app/index/styles/lists.css b/guacamole/src/main/webapp/app/index/styles/lists.css index 4f462f41b..60b895243 100644 --- a/guacamole/src/main/webapp/app/index/styles/lists.css +++ b/guacamole/src/main/webapp/app/index/styles/lists.css @@ -55,10 +55,12 @@ margin: 0.5em; } -.connection .thumbnail img { +.connection .thumbnail > * { border: 1px solid black; + background: black; box-shadow: 1px 1px 5px black; max-width: 75%; + display: inline-block; } div.recent-connections .connection .thumbnail {