From 35ca205653b792bb56ec68c8b94cc2ebabc07470 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Sun, 28 Dec 2014 19:27:04 -0800 Subject: [PATCH 01/16] GUAC-963: Proof-of-concept ManagedClient implementation. Remove guacClientFactory and guacTunnelFactory (functionality replaced by ManagedClient). --- .../client/controllers/clientController.js | 76 ++-- .../app/client/directives/guacClient.js | 213 ++-------- .../app/client/services/guacClientFactory.js | 176 -------- .../app/client/services/guacClientManager.js | 97 +++++ .../app/client/services/guacTunnelFactory.js | 89 ---- .../webapp/app/client/templates/client.html | 11 +- .../webapp/app/client/types/ManagedClient.js | 399 ++++++++++++++++++ .../app/client/types/ManagedClientState.js | 159 +++++++ .../app/index/controllers/indexController.js | 31 +- 9 files changed, 742 insertions(+), 509 deletions(-) delete mode 100644 guacamole/src/main/webapp/app/client/services/guacClientFactory.js create mode 100644 guacamole/src/main/webapp/app/client/services/guacClientManager.js delete mode 100644 guacamole/src/main/webapp/app/client/services/guacTunnelFactory.js create mode 100644 guacamole/src/main/webapp/app/client/types/ManagedClient.js create mode 100644 guacamole/src/main/webapp/app/client/types/ManagedClientState.js diff --git a/guacamole/src/main/webapp/app/client/controllers/clientController.js b/guacamole/src/main/webapp/app/client/controllers/clientController.js index 63411b4e6..ed853fdca 100644 --- a/guacamole/src/main/webapp/app/client/controllers/clientController.js +++ b/guacamole/src/main/webapp/app/client/controllers/clientController.js @@ -33,6 +33,7 @@ angular.module('client').controller('clientController', ['$scope', '$routeParams // Required services var connectionGroupService = $injector.get('connectionGroupService'); var connectionService = $injector.get('connectionService'); + var guacClientManager = $injector.get('guacClientManager'); /** * The minimum number of pixels a drag gesture must move to result in the @@ -149,7 +150,7 @@ angular.module('client').controller('clientController', ['$scope', '$routeParams name : "CLIENT.ACTION_RECONNECT", // Handle reconnect action callback : function reconnectCallback() { - $scope.id = uniqueId; + $scope.client = guacClientManager.replaceManagedClient(uniqueId, $routeParams.params); $scope.showStatus(false); } }; @@ -164,12 +165,6 @@ angular.module('client').controller('clientController', ['$scope', '$routeParams remaining: 15 }; - // Client settings and state - $scope.clientProperties = new ClientProperties(); - - // Initialize clipboard data to an empty string - $scope.clipboardData = ""; - // Hide menu by default $scope.menuShown = false; @@ -198,8 +193,7 @@ angular.module('client').controller('clientController', ['$scope', '$routeParams * as well as any extra parameters if set. */ var uniqueId = $routeParams.type + '/' + $routeParams.id; - $scope.id = uniqueId; - $scope.connectionParameters = $routeParams.params || ''; + $scope.client = guacClientManager.getManagedClient(uniqueId, $routeParams.params); // Pull connection name from server switch ($routeParams.type) { @@ -266,9 +260,9 @@ angular.module('client').controller('clientController', ['$scope', '$routeParams } // Scroll display if absolute mouse is in use - else if ($scope.clientProperties.emulateAbsoluteMouse) { - $scope.clientProperties.scrollLeft -= deltaX; - $scope.clientProperties.scrollTop -= deltaY; + else if ($scope.client.clientProperties.emulateAbsoluteMouse) { + $scope.client.clientProperties.scrollLeft -= deltaX; + $scope.client.clientProperties.scrollTop -= deltaY; } return false; @@ -305,7 +299,7 @@ angular.module('client').controller('clientController', ['$scope', '$routeParams $scope.clientPinch = function clientPinch(inProgress, startLength, currentLength, centerX, centerY) { // Do not handle pinch gestures while relative mouse is in use - if (!$scope.clientProperties.emulateAbsoluteMouse) + if (!$scope.client.clientProperties.emulateAbsoluteMouse) return false; // Stop gesture if not in progress @@ -316,26 +310,26 @@ angular.module('client').controller('clientController', ['$scope', '$routeParams // Set initial scale if gesture has just started if (!initialScale) { - initialScale = $scope.clientProperties.scale; - initialCenterX = (centerX + $scope.clientProperties.scrollLeft) / initialScale; - initialCenterY = (centerY + $scope.clientProperties.scrollTop) / initialScale; + initialScale = $scope.client.clientProperties.scale; + initialCenterX = (centerX + $scope.client.clientProperties.scrollLeft) / initialScale; + initialCenterY = (centerY + $scope.client.clientProperties.scrollTop) / initialScale; } // Determine new scale absolutely var currentScale = initialScale * currentLength / startLength; // Fix scale within limits - scroll will be miscalculated otherwise - currentScale = Math.max(currentScale, $scope.clientProperties.minScale); - currentScale = Math.min(currentScale, $scope.clientProperties.maxScale); + currentScale = Math.max(currentScale, $scope.client.clientProperties.minScale); + currentScale = Math.min(currentScale, $scope.client.clientProperties.maxScale); // Update scale based on pinch distance $scope.autoFit = false; - $scope.clientProperties.autoFit = false; - $scope.clientProperties.scale = currentScale; + $scope.client.clientProperties.autoFit = false; + $scope.client.clientProperties.scale = currentScale; // Scroll display to keep original pinch location centered within current pinch - $scope.clientProperties.scrollLeft = initialCenterX * currentScale - centerX; - $scope.clientProperties.scrollTop = initialCenterY * currentScale - centerY; + $scope.client.clientProperties.scrollLeft = initialCenterX * currentScale - centerX; + $scope.client.clientProperties.scrollTop = initialCenterY * currentScale - centerY; return false; @@ -357,7 +351,7 @@ angular.module('client').controller('clientController', ['$scope', '$routeParams $scope.$broadcast('guacClipboard', 'text/plain', $scope.clipboardData); // Disable client keyboard if the menu is shown - $scope.clientProperties.keyboardEnabled = !menuShown; + $scope.client.clientProperties.keyboardEnabled = !menuShown; }); @@ -385,7 +379,7 @@ angular.module('client').controller('clientController', ['$scope', '$routeParams keyboard.reset(); // Toggle the menu - $scope.safeApply(function() { + $scope.$apply(function() { $scope.menuShown = !$scope.menuShown; }); } @@ -478,33 +472,33 @@ angular.module('client').controller('clientController', ['$scope', '$routeParams }); $scope.formattedScale = function formattedScale() { - return Math.round($scope.clientProperties.scale * 100); + return Math.round($scope.client.clientProperties.scale * 100); }; $scope.zoomIn = function zoomIn() { $scope.autoFit = false; - $scope.clientProperties.autoFit = false; - $scope.clientProperties.scale += 0.1; + $scope.client.clientProperties.autoFit = false; + $scope.client.clientProperties.scale += 0.1; }; $scope.zoomOut = function zoomOut() { - $scope.clientProperties.autoFit = false; - $scope.clientProperties.scale -= 0.1; + $scope.client.clientProperties.autoFit = false; + $scope.client.clientProperties.scale -= 0.1; }; $scope.autoFit = true; $scope.changeAutoFit = function changeAutoFit() { - if ($scope.autoFit && $scope.clientProperties.minScale) { - $scope.clientProperties.autoFit = true; + if ($scope.autoFit && $scope.client.clientProperties.minScale) { + $scope.client.clientProperties.autoFit = true; } else { - $scope.clientProperties.autoFit = false; - $scope.clientProperties.scale = 1; + $scope.client.clientProperties.autoFit = false; + $scope.client.clientProperties.scale = 1; } }; $scope.autoFitDisabled = function() { - return $scope.clientProperties.minZoom >= 1; + return $scope.client.clientProperties.minZoom >= 1; }; /** @@ -568,7 +562,7 @@ angular.module('client').controller('clientController', ['$scope', '$routeParams var downloadNotificationIDs = {}; $scope.$on('guacClientFileDownloadStart', function handleClientFileDownloadStart(event, guacClient, streamIndex, mimetype, filename) { - $scope.safeApply(function() { + $scope.$apply(function() { var notification = { className : 'download', @@ -583,7 +577,7 @@ angular.module('client').controller('clientController', ['$scope', '$routeParams }); $scope.$on('guacClientFileDownloadProgress', function handleClientFileDownloadProgress(event, guacClient, streamIndex, mimetype, filename, length) { - $scope.safeApply(function() { + $scope.$apply(function() { var notification = downloadNotifications[streamIndex]; if (notification) @@ -593,7 +587,7 @@ angular.module('client').controller('clientController', ['$scope', '$routeParams }); $scope.$on('guacClientFileDownloadEnd', function handleClientFileDownloadEnd(event, guacClient, streamIndex, mimetype, filename, blob) { - $scope.safeApply(function() { + $scope.$apply(function() { var notification = downloadNotifications[streamIndex]; var notificationID = downloadNotificationIDs[streamIndex]; @@ -629,7 +623,7 @@ angular.module('client').controller('clientController', ['$scope', '$routeParams var uploadNotificationIDs = {}; $scope.$on('guacClientFileUploadStart', function handleClientFileUploadStart(event, guacClient, streamIndex, mimetype, filename, length) { - $scope.safeApply(function() { + $scope.$apply(function() { var notification = { className : 'upload', @@ -644,7 +638,7 @@ angular.module('client').controller('clientController', ['$scope', '$routeParams }); $scope.$on('guacClientFileUploadProgress', function handleClientFileUploadProgress(event, guacClient, streamIndex, mimetype, filename, length, offset) { - $scope.safeApply(function() { + $scope.$apply(function() { var notification = uploadNotifications[streamIndex]; if (notification) @@ -654,7 +648,7 @@ angular.module('client').controller('clientController', ['$scope', '$routeParams }); $scope.$on('guacClientFileUploadEnd', function handleClientFileUploadEnd(event, guacClient, streamIndex, mimetype, filename, length) { - $scope.safeApply(function() { + $scope.$apply(function() { var notification = uploadNotifications[streamIndex]; var notificationID = uploadNotificationIDs[streamIndex]; @@ -683,7 +677,7 @@ angular.module('client').controller('clientController', ['$scope', '$routeParams }); $scope.$on('guacClientFileUploadError', function handleClientFileUploadError(event, guacClient, streamIndex, mimetype, fileName, length, status) { - $scope.safeApply(function() { + $scope.$apply(function() { var notification = uploadNotifications[streamIndex]; var notificationID = uploadNotificationIDs[streamIndex]; diff --git a/guacamole/src/main/webapp/app/client/directives/guacClient.js b/guacamole/src/main/webapp/app/client/directives/guacClient.js index 1ef3bcdec..8cc3db40d 100644 --- a/guacamole/src/main/webapp/app/client/directives/guacClient.js +++ b/guacamole/src/main/webapp/app/client/directives/guacClient.js @@ -32,46 +32,19 @@ angular.module('client').directive('guacClient', [function guacClient() { scope: { /** - * Parameters for controlling client state. + * The client to display within this guacClient directive. * - * @type ClientProperties|Object + * @type ManagedClient */ - clientProperties : '=', + client : '=' - /** - * The ID of the Guacamole connection to connect to. - * - * @type String - */ - id : '=', - - /** - * Arbitrary URL-encoded parameters to append to the connection - * string when connecting. - * - * @type String - */ - connectionParameters : '=' - }, templateUrl: 'app/client/templates/guacClient.html', controller: ['$scope', '$injector', '$element', function guacClientController($scope, $injector, $element) { - - /* - * Safe $apply implementation from Alex Vanston: - * https://coderwall.com/p/ngisma - */ - $scope.safeApply = function(fn) { - var phase = this.$root.$$phase; - if(phase === '$apply' || phase === '$digest') { - if(fn && (typeof(fn) === 'function')) { - fn(); - } - } else { - this.$apply(fn); - } - }; - + + // Required services + var $window = $injector.get('$window'); + /** * Whether the local, hardware mouse cursor is in use. * @@ -146,14 +119,6 @@ angular.module('client').directive('guacClient', [function guacClient() { */ var touchPad = new Guacamole.Mouse.Touchpad(displayContainer); - var $window = $injector.get('$window'), - guacAudio = $injector.get('guacAudio'), - guacVideo = $injector.get('guacVideo'), - guacHistory = $injector.get('guacHistory'), - guacTunnelFactory = $injector.get('guacTunnelFactory'), - guacClientFactory = $injector.get('guacClientFactory'), - authenticationService = $injector.get('authenticationService'); - /** * Updates the scale of the attached Guacamole.Client based on current window * size and "auto-fit" setting. @@ -163,60 +128,20 @@ angular.module('client').directive('guacClient', [function guacClient() { if (!display) return; // Calculate scale to fit screen - $scope.clientProperties.minScale = Math.min( + $scope.client.clientProperties.minScale = Math.min( main.offsetWidth / Math.max(display.getWidth(), 1), main.offsetHeight / Math.max(display.getHeight(), 1) ); // Calculate appropriate maximum zoom level - $scope.clientProperties.maxScale = Math.max($scope.clientProperties.minScale, 3); + $scope.client.clientProperties.maxScale = Math.max($scope.client.clientProperties.minScale, 3); // Clamp zoom level, maintain auto-fit - if (display.getScale() < $scope.clientProperties.minScale || $scope.clientProperties.autoFit) - $scope.clientProperties.scale = $scope.clientProperties.minScale; + if (display.getScale() < $scope.client.clientProperties.minScale || $scope.client.clientProperties.autoFit) + $scope.client.clientProperties.scale = $scope.client.clientProperties.minScale; - else if (display.getScale() > $scope.clientProperties.maxScale) - $scope.clientProperties.scale = $scope.clientProperties.maxScale; - - }; - - /** - * Returns the string of connection parameters to be passed to the - * Guacamole client during connection. This string generally - * contains the desired connection ID, display resolution, and - * supported audio/video codecs. - * - * @returns {String} The string of connection parameters to be - * passed to the Guacamole client. - */ - var getConnectString = function getConnectString() { - - // Calculate optimal width/height for display - var pixel_density = $window.devicePixelRatio || 1; - var optimal_dpi = pixel_density * 96; - var optimal_width = $window.innerWidth * pixel_density; - var optimal_height = $window.innerHeight * pixel_density; - - // Build base connect string - var connectString = - "id=" + encodeURIComponent($scope.id) - + "&authToken=" + encodeURIComponent(authenticationService.getCurrentToken()) - + "&width=" + Math.floor(optimal_width) - + "&height=" + Math.floor(optimal_height) - + "&dpi=" + Math.floor(optimal_dpi) - + ($scope.connectionParameters ? '&' + $scope.connectionParameters : ''); - - // Add audio mimetypes to connect_string - guacAudio.supported.forEach(function(mimetype) { - connectString += "&audio=" + encodeURIComponent(mimetype); - }); - - // Add video mimetypes to connect_string - guacVideo.supported.forEach(function(mimetype) { - connectString += "&video=" + encodeURIComponent(mimetype); - }); - - return connectString; + else if (display.getScale() > $scope.client.clientProperties.maxScale) + $scope.client.clientProperties.scale = $scope.client.clientProperties.maxScale; }; @@ -322,82 +247,36 @@ angular.module('client').directive('guacClient', [function guacClient() { * SCROLLING */ - $scope.$watch('clientProperties.scrollLeft', function scrollLeftChanged(scrollLeft) { + $scope.$watch('client.clientProperties.scrollLeft', function scrollLeftChanged(scrollLeft) { main.scrollLeft = scrollLeft; - $scope.clientProperties.scrollLeft = main.scrollLeft; + $scope.client.clientProperties.scrollLeft = main.scrollLeft; }); - $scope.$watch('clientProperties.scrollTop', function scrollTopChanged(scrollTop) { + $scope.$watch('client.clientProperties.scrollTop', function scrollTopChanged(scrollTop) { main.scrollTop = scrollTop; - $scope.clientProperties.scrollTop = main.scrollTop; + $scope.client.clientProperties.scrollTop = main.scrollTop; }); - /* - * CONNECT / RECONNECT - */ + // Attach any given managed client + $scope.$watch('client', function(managedClient) { - /** - * Store the thumbnail of the currently connected client within - * the connection history under the given ID. If the client is not - * connected, or if no ID is given, this function has no effect. - * - * @param {String} id - * The ID of the history entry to update. - */ - var updateHistoryEntry = function updateHistoryEntry(id) { + // Remove any existing display + displayContainer.innerHTML = ""; - // Update stored thumbnail of previous connection - if (id && display && display.getWidth() > 0 && display.getHeight() > 0) { - - // Get screenshot - var canvas = display.flatten(); - - // Calculate scale of thumbnail (max 320x240, max zoom 100%) - var scale = Math.min(320 / canvas.width, 240 / canvas.height, 1); - - // Create thumbnail canvas - var thumbnail = document.createElement("canvas"); - thumbnail.width = canvas.width*scale; - thumbnail.height = canvas.height*scale; - - // Scale screenshot to thumbnail - var context = thumbnail.getContext("2d"); - context.drawImage(canvas, - 0, 0, canvas.width, canvas.height, - 0, 0, thumbnail.width, thumbnail.height - ); - - guacHistory.updateThumbnail(id, thumbnail.toDataURL("image/png")); - - } - - }; - - // Connect to given ID whenever ID changes - $scope.$watch('id', function(id, previousID) { - - // If a client is already attached, ensure it is disconnected - if (client) - client.disconnect(); - - // Update stored thumbnail of previous connection - updateHistoryEntry(previousID); - - // Only proceed if a new client is attached - if (!id) + // Only proceed if a client is given + if (!managedClient) return; - // Get new client instance - var tunnel = guacTunnelFactory.getInstance($scope); - client = guacClientFactory.getInstance($scope, tunnel); + // Get Guacamole client instance + client = managedClient.client; - // Init display + // Attach possibly new display display = client.getDisplay(); - display.scale($scope.clientProperties.scale); + display.scale($scope.client.clientProperties.scale); // Update the scale of the display when the client display size changes. display.onresize = function() { - $scope.safeApply(updateDisplayScale); + $scope.$apply(updateDisplayScale); }; // Use local cursor if possible, update localCursor flag @@ -407,7 +286,6 @@ angular.module('client').directive('guacClient', [function guacClient() { // Add display element displayElement = display.getElement(); - displayContainer.innerHTML = ""; displayContainer.appendChild(displayElement); // Do nothing when the display element is clicked on. @@ -416,17 +294,6 @@ angular.module('client').directive('guacClient', [function guacClient() { return false; }; - // Connect - client.connect(getConnectString()); - - }); - - // Clean up when client directive is destroyed - $scope.$on('$destroy', function destroyClient() { - - // Update stored thumbnail of current connection - updateHistoryEntry($scope.id); - }); /* @@ -434,7 +301,7 @@ angular.module('client').directive('guacClient', [function guacClient() { */ // Watch for changes to mouse emulation mode - $scope.$watch('clientProperties.emulateAbsoluteMouse', function(emulateAbsoluteMouse) { + $scope.$watch('client.clientProperties.emulateAbsoluteMouse', function(emulateAbsoluteMouse) { if (!client || !display) return; @@ -483,14 +350,14 @@ angular.module('client').directive('guacClient', [function guacClient() { */ // Adjust scale if modified externally - $scope.$watch('clientProperties.scale', function changeScale(scale) { + $scope.$watch('client.clientProperties.scale', function changeScale(scale) { // Fix scale within limits - scale = Math.max(scale, $scope.clientProperties.minScale); - scale = Math.min(scale, $scope.clientProperties.maxScale); + scale = Math.max(scale, $scope.client.clientProperties.minScale); + scale = Math.min(scale, $scope.client.clientProperties.maxScale); // If at minimum zoom level, hide scroll bars - if (scale === $scope.clientProperties.minScale) + if (scale === $scope.client.clientProperties.minScale) main.style.overflow = "hidden"; // If not at minimum zoom level, show scroll bars @@ -501,15 +368,15 @@ angular.module('client').directive('guacClient', [function guacClient() { if (display) display.scale(scale); - if (scale !== $scope.clientProperties.scale) - $scope.clientProperties.scale = scale; + if (scale !== $scope.client.clientProperties.scale) + $scope.client.clientProperties.scale = scale; }); // If autofit is set, the scale should be set to the minimum scale, filling the screen - $scope.$watch('clientProperties.autoFit', function changeAutoFit(autoFit) { + $scope.$watch('client.clientProperties.autoFit', function changeAutoFit(autoFit) { if(autoFit) - $scope.clientProperties.scale = $scope.clientProperties.minScale; + $scope.client.clientProperties.scale = $scope.client.clientProperties.minScale; }); // If the element is resized, attempt to resize client @@ -527,7 +394,7 @@ angular.module('client').directive('guacClient', [function guacClient() { } - $scope.safeApply(updateDisplayScale); + $scope.$apply(updateDisplayScale); }); @@ -537,7 +404,7 @@ angular.module('client').directive('guacClient', [function guacClient() { // Listen for broadcasted keydown events and fire the appropriate listeners $scope.$on('guacKeydown', function keydownListener(event, keysym, keyboard) { - if ($scope.clientProperties.keyboardEnabled && !event.defaultPrevented) { + if ($scope.client.clientProperties.keyboardEnabled && !event.defaultPrevented) { client.sendKeyEvent(1, keysym); event.preventDefault(); } @@ -545,7 +412,7 @@ angular.module('client').directive('guacClient', [function guacClient() { // Listen for broadcasted keyup events and fire the appropriate listeners $scope.$on('guacKeyup', function keyupListener(event, keysym, keyboard) { - if ($scope.clientProperties.keyboardEnabled && !event.defaultPrevented) { + if ($scope.client.clientProperties.keyboardEnabled && !event.defaultPrevented) { client.sendKeyEvent(0, keysym); event.preventDefault(); } diff --git a/guacamole/src/main/webapp/app/client/services/guacClientFactory.js b/guacamole/src/main/webapp/app/client/services/guacClientFactory.js deleted file mode 100644 index 692e538ce..000000000 --- a/guacamole/src/main/webapp/app/client/services/guacClientFactory.js +++ /dev/null @@ -1,176 +0,0 @@ -/* - * 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 service for creating Guacamole clients. - */ -angular.module('client').factory('guacClientFactory', ['$rootScope', - function guacClientFactory($rootScope) { - - var service = {}; - - /** - * Returns a new Guacamole client instance which connects using the - * provided tunnel. - * - * @param {Scope} $scope The current scope. - * @param {Guacamole.Tunnel} tunnel The tunnel to connect through. - * @returns {Guacamole.Client} A new Guacamole client instance. - */ - service.getInstance = function getClientInstance($scope, tunnel) { - - // Instantiate client - var guacClient = new Guacamole.Client(tunnel); - - /* - * Fire guacClientStateChange events when client state changes. - */ - guacClient.onstatechange = function onClientStateChange(clientState) { - $scope.safeApply(function() { - - switch (clientState) { - - // Idle - case 0: - $scope.$emit('guacClientStateChange', guacClient, "idle"); - break; - - // Connecting - case 1: - $scope.$emit('guacClientStateChange', guacClient, "connecting"); - break; - - // Connected + waiting - case 2: - $scope.$emit('guacClientStateChange', guacClient, "waiting"); - break; - - // Connected - case 3: - $scope.$emit('guacClientStateChange', guacClient, "connected"); - break; - - // Disconnecting / disconnected are handled by tunnel instead - case 4: - case 5: - break; - - } - - }); - }; - - /* - * Fire guacClientName events when a new name is received. - */ - guacClient.onname = function onClientName(name) { - $scope.safeApply(function() { - $scope.$emit('guacClientName', guacClient, name); - }); - }; - - /* - * Disconnect and fire guacClientError when the client receives an - * error. - */ - guacClient.onerror = function onClientError(status) { - $scope.safeApply(function() { - - // Disconnect, if connected - guacClient.disconnect(); - - $scope.$emit('guacClientError', guacClient, status.code); - - }); - }; - - /* - * Fire guacClientClipboard events after new clipboard data is received. - */ - guacClient.onclipboard = function onClientClipboard(stream, mimetype) { - $scope.safeApply(function() { - - // Only text/plain is supported for now - if (mimetype !== "text/plain") { - stream.sendAck("Only text/plain supported", Guacamole.Status.Code.UNSUPPORTED); - return; - } - - var reader = new Guacamole.StringReader(stream); - var data = ""; - - // Append any received data to buffer - reader.ontext = function clipboard_text_received(text) { - data += text; - stream.sendAck("Received", Guacamole.Status.Code.SUCCESS); - }; - - // Emit event when done - reader.onend = function clipboard_text_end() { - $scope.$emit('guacClientClipboard', guacClient, mimetype, data); - }; - - }); - }; - - /* - * Fire guacFileStart, guacFileProgress, and guacFileEnd events during - * the receipt of files. - */ - guacClient.onfile = function onClientFile(stream, mimetype, filename) { - $scope.safeApply(function() { - - // Begin file download - var guacFileStartEvent = $scope.$emit('guacClientFileDownloadStart', guacClient, stream.index, mimetype, filename); - if (!guacFileStartEvent.defaultPrevented) { - - var blob_reader = new Guacamole.BlobReader(stream, mimetype); - - // Update progress as data is received - blob_reader.onprogress = function onprogress() { - $scope.$emit('guacClientFileDownloadProgress', guacClient, stream.index, mimetype, filename, blob_reader.getLength()); - stream.sendAck("Received", Guacamole.Status.Code.SUCCESS); - }; - - // When complete, prompt for download - blob_reader.onend = function onend() { - $scope.$emit('guacClientFileDownloadEnd', guacClient, stream.index, mimetype, filename, blob_reader.getBlob()); - }; - - stream.sendAck("Ready", Guacamole.Status.Code.SUCCESS); - - } - - // Respond with UNSUPPORTED if download (default action) canceled within event handler - else - stream.sendAck("Download canceled", Guacamole.Status.Code.UNSUPPORTED); - - }); - }; - - return guacClient; - - }; - - return service; - -}]); diff --git a/guacamole/src/main/webapp/app/client/services/guacClientManager.js b/guacamole/src/main/webapp/app/client/services/guacClientManager.js new file mode 100644 index 000000000..8e200e3ab --- /dev/null +++ b/guacamole/src/main/webapp/app/client/services/guacClientManager.js @@ -0,0 +1,97 @@ +/* + * 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 service for managing several active Guacamole clients. + */ +angular.module('client').factory('guacClientManager', ['ManagedClient', + function guacClientManager(ManagedClient) { + + var service = {}; + + /** + * Map of all active managed clients. Each key is the ID of the connection + * used by that client. + * + * @type Object. + */ + service.managedClients = {}; + + /** + * Creates a new ManagedClient associated with the connection having the + * given ID. If such a ManagedClient already exists, it is disconnected and + * replaced. + * + * @param {String} id + * The ID of the connection whose ManagedClient should be retrieved. + * + * @param {String} [connectionParameters] + * Any additional HTTP parameters to pass while connecting. This + * parameter only has an effect if a new connection is established as + * a result of this function call. + * + * @returns {ManagedClient} + * The ManagedClient associated with the connection having the given + * ID. + */ + service.replaceManagedClient = function replaceManagedClient(id, connectionParameters) { + + // Disconnect any existing client + if (id in service.managedClients) + service.managedClients[id].client.disconnect(); + + // Set new client + return service.managedClients[id] = ManagedClient.getInstance(id, connectionParameters); + + }; + + /** + * Returns the ManagedClient associated with the connection having the + * given ID. If no such ManagedClient exists, a new ManagedClient is + * created. + * + * @param {String} id + * The ID of the connection whose ManagedClient should be retrieved. + * + * @param {String} [connectionParameters] + * Any additional HTTP parameters to pass while connecting. This + * parameter only has an effect if a new connection is established as + * a result of this function call. + * + * @returns {ManagedClient} + * The ManagedClient associated with the connection having the given + * ID. + */ + service.getManagedClient = function getManagedClient(id, connectionParameters) { + + // Create new managed client if it doesn't already exist + if (!(id in service.managedClients)) + service.managedClients[id] = ManagedClient.getInstance(id, connectionParameters); + + // Return existing client + return service.managedClients[id]; + + }; + + return service; + +}]); diff --git a/guacamole/src/main/webapp/app/client/services/guacTunnelFactory.js b/guacamole/src/main/webapp/app/client/services/guacTunnelFactory.js deleted file mode 100644 index 52e259be3..000000000 --- a/guacamole/src/main/webapp/app/client/services/guacTunnelFactory.js +++ /dev/null @@ -1,89 +0,0 @@ -/* - * 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 service for creating Guacamole tunnels. - */ -angular.module('client').factory('guacTunnelFactory', ['$rootScope', '$window', - function guacTunnelFactory($rootScope, $window) { - - var service = {}; - - /** - * Returns a new Guacamole tunnel instance, using an implementation that is - * supported by the web browser. - * - * @param {Scope} $scope The current scope. - * @returns {Guacamole.Tunnel} A new Guacamole tunnel instance. - */ - service.getInstance = function getTunnelInstance($scope) { - - var tunnel; - - // If WebSocket available, try to use it. - if ($window.WebSocket) - tunnel = new Guacamole.ChainedTunnel( - new Guacamole.WebSocketTunnel('websocket-tunnel'), - new Guacamole.HTTPTunnel('tunnel') - ); - - // If no WebSocket, then use HTTP. - else - tunnel = new Guacamole.HTTPTunnel('tunnel'); - - // Fire events for tunnel errors - tunnel.onerror = function onTunnelError(status) { - $scope.safeApply(function() { - $scope.$emit('guacTunnelError', tunnel, status.code); - }); - }; - - // Fire events for tunnel state changes - tunnel.onstatechange = function onTunnelStateChange(state) { - $scope.safeApply(function() { - - switch (state) { - - case Guacamole.Tunnel.State.CONNECTING: - $scope.$emit('guacTunnelStateChange', tunnel, 'connecting'); - break; - - case Guacamole.Tunnel.State.OPEN: - $scope.$emit('guacTunnelStateChange', tunnel, 'open'); - break; - - case Guacamole.Tunnel.State.CLOSED: - $scope.$emit('guacTunnelStateChange', tunnel, 'closed'); - break; - - } - - }); - }; - - return tunnel; - - }; - - return service; - -}]); diff --git a/guacamole/src/main/webapp/app/client/templates/client.html b/guacamole/src/main/webapp/app/client/templates/client.html index 879a53cec..401970d00 100644 --- a/guacamole/src/main/webapp/app/client/templates/client.html +++ b/guacamole/src/main/webapp/app/client/templates/client.html @@ -28,10 +28,7 @@
- +
@@ -60,7 +57,7 @@

{{'CLIENT.SECTION_HEADER_CLIPBOARD' | translate}}

{{'CLIENT.HELP_CLIPBOARD' | translate}}

- +

{{'CLIENT.SECTION_HEADER_INPUT_METHOD' | translate}}

@@ -93,7 +90,7 @@
- +

@@ -102,7 +99,7 @@
- +

diff --git a/guacamole/src/main/webapp/app/client/types/ManagedClient.js b/guacamole/src/main/webapp/app/client/types/ManagedClient.js new file mode 100644 index 000000000..ce2d92e5b --- /dev/null +++ b/guacamole/src/main/webapp/app/client/types/ManagedClient.js @@ -0,0 +1,399 @@ +/* + * 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 ManagedClient class used by the guacClientManager service. + */ +angular.module('client').factory('ManagedClient', ['$rootScope', '$injector', + function defineManagedClient($rootScope, $injector) { + + // Required types + var ClientProperties = $injector.get('ClientProperties'); + var ManagedClientState = $injector.get('ManagedClientState'); + + // Required services + var $window = $injector.get('$window'); + var $document = $injector.get('$document'); + var authenticationService = $injector.get('authenticationService'); + var guacAudio = $injector.get('guacAudio'); + var guacHistory = $injector.get('guacHistory'); + var guacVideo = $injector.get('guacVideo'); + + /** + * Object which serves as a surrogate interface, encapsulating a Guacamole + * client while it is active, allowing it to be detached and reattached + * from different client views. + * + * @constructor + * @param {ManagedClient|Object} [template={}] + * The object whose properties should be copied within the new + * ManagedClient. + */ + var ManagedClient = function ManagedClient(template) { + + // Use empty object by default + template = template || {}; + + /** + * The ID of the connection associated with this client. + * + * @type String + */ + this.id = template.id; + + /** + * The actual underlying Guacamole client. + * + * @type Guacamole.Client + */ + this.client = template.client; + + /** + * The tunnel being used by the underlying Guacamole client. + * + * @type Guacamole.Tunnel + */ + this.tunnel = template.tunnel; + + /** + * The name returned via the Guacamole protocol for this connection, if + * any. + * + * @type String + */ + this.name = template.name; + + /** + * The current clipboard contents. + * + * @type String + */ + this.clipboardData = template.clipboardData; + + /** + * The current state of the Guacamole client (idle, connecting, + * connected, terminated with error, etc.). + * + * @type ManagedClientState + */ + this.clientState = template.clientState || new ManagedClientState(); + + /** + * Properties associated with the display and behavior of the Guacamole + * client. + * + * @type ClientProperties + */ + this.clientProperties = template.clientProperties || new ClientProperties(); + + }; + + /** + * Returns the string of connection parameters to be passed to the + * Guacamole client during connection. This string generally contains the + * desired connection ID, display resolution, and supported audio/video + * codecs. + * + * @param {String} id + * The ID of the connection or group to connect to. + * + * @param {String} [connectionParameters] + * Any additional HTTP parameters to pass while connecting. + * + * @returns {String} + * The string of connection parameters to be passed to the Guacamole + * client. + */ + var getConnectString = function getConnectString(id, connectionParameters) { + + // Calculate optimal width/height for display + var pixel_density = $window.devicePixelRatio || 1; + var optimal_dpi = pixel_density * 96; + var optimal_width = $window.innerWidth * pixel_density; + var optimal_height = $window.innerHeight * pixel_density; + + // Build base connect string + var connectString = + "id=" + encodeURIComponent(id) + + "&authToken=" + encodeURIComponent(authenticationService.getCurrentToken()) + + "&width=" + Math.floor(optimal_width) + + "&height=" + Math.floor(optimal_height) + + "&dpi=" + Math.floor(optimal_dpi) + + (connectionParameters ? '&' + connectionParameters : ''); + + // Add audio mimetypes to connect_string + guacAudio.supported.forEach(function(mimetype) { + connectString += "&audio=" + encodeURIComponent(mimetype); + }); + + // Add video mimetypes to connect_string + guacVideo.supported.forEach(function(mimetype) { + connectString += "&video=" + encodeURIComponent(mimetype); + }); + + return connectString; + + }; + + /** + * Store the thumbnail of the given managed client within the connection + * history under its associated ID. If the client is not connected, this + * function has no effect. + * + * @param {String} managedClient + * The client whose history entry should be updated. + */ + var updateHistoryEntry = function updateHistoryEntry(managedClient) { + + var display = managedClient.client.getDisplay(); + + // Update stored thumbnail of previous connection + if (display && display.getWidth() > 0 && display.getHeight() > 0) { + + // Get screenshot + var canvas = display.flatten(); + + // Calculate scale of thumbnail (max 320x240, max zoom 100%) + var scale = Math.min(320 / canvas.width, 240 / canvas.height, 1); + + // Create thumbnail canvas + var thumbnail = $document[0].createElement("canvas"); + thumbnail.width = canvas.width*scale; + thumbnail.height = canvas.height*scale; + + // Scale screenshot to thumbnail + var context = thumbnail.getContext("2d"); + context.drawImage(canvas, + 0, 0, canvas.width, canvas.height, + 0, 0, thumbnail.width, thumbnail.height + ); + + guacHistory.updateThumbnail(managedClient.id, thumbnail.toDataURL("image/png")); + + } + + }; + + /** + * Creates a new ManagedClient, connecting it to the specified connection + * or group. + * + * @param {String} id + * The ID of the connection or group to connect to. + * + * @param {String} [connectionParameters] + * Any additional HTTP parameters to pass while connecting. + * + * @returns {ManagedClient} + * A new ManagedClient instance which is connected to the connection or + * connection group having the given ID. + */ + ManagedClient.getInstance = function getInstance(id, connectionParameters) { + + var tunnel; + + // If WebSocket available, try to use it. + if ($window.WebSocket) + tunnel = new Guacamole.ChainedTunnel( + new Guacamole.WebSocketTunnel('websocket-tunnel'), + new Guacamole.HTTPTunnel('tunnel') + ); + + // If no WebSocket, then use HTTP. + else + tunnel = new Guacamole.HTTPTunnel('tunnel'); + + // Get new client instance + var client = new Guacamole.Client(tunnel); + + // Associate new managed client with new client and tunnel + var managedClient = new ManagedClient({ + id : id, + client : client, + tunnel : tunnel + }); + + // Fire events for tunnel errors + tunnel.onerror = function tunnelError(status) { + $rootScope.$apply(function handleTunnelError() { + ManagedClientState.setConnectionState(managedClient.clientState, + ManagedClientState.ConnectionState.TUNNEL_ERROR, + status.code); + }); + }; + + // Update connection state as tunnel state changes + tunnel.onstatechange = function tunnelStateChanged(state) { + $rootScope.$apply(function updateTunnelState() { + + switch (state) { + + // Connection is being established + case Guacamole.Tunnel.State.CONNECTING: + ManagedClientState.setConnectionState(managedClient.clientState, + ManagedClientState.ConnectionState.CONNECTING); + break; + + // Connection has closed + case Guacamole.Tunnel.State.CLOSED: + + updateHistoryEntry(managedClient); + + ManagedClientState.setConnectionState(managedClient.clientState, + ManagedClientState.ConnectionState.DISCONNECTED); + break; + + } + + }); + }; + + // Update connection state as client state changes + client.onstatechange = function clientStateChanged(clientState) { + $rootScope.$apply(function updateClientState() { + + switch (clientState) { + + // Idle + case 0: + ManagedClientState.setConnectionState(managedClient.clientState, + ManagedClientState.ConnectionState.IDLE); + break; + + // Connected + waiting + case 2: + ManagedClientState.setConnectionState(managedClient.clientState, + ManagedClientState.ConnectionState.WAITING); + break; + + // Connected + case 3: + ManagedClientState.setConnectionState(managedClient.clientState, + ManagedClientState.ConnectionState.DISCONNECTED); + break; + + // Connecting, disconnecting, and disconnected are all + // either ignored or handled by tunnel state + + case 1: // Connecting + case 4: // Disconnecting + case 5: // Disconnected + break; + + } + + }); + }; + + // Update stored name if name changes + client.onname = function clientNameChanged(name) { + $rootScope.$apply(function updateName() { + managedClient.name = name; + }); + }; + + // Disconnect and update status when the client receives an error + client.onerror = function clientError(status) { + $rootScope.$apply(function handleClientError() { + + // Disconnect, if connected + client.disconnect(); + + // Update state + ManagedClientState.setConnectionState(managedClient.clientState, + ManagedClientState.ConnectionState.CLIENT_ERROR, + status.code); + + }); + }; + + // Handle any received clipboard data + client.onclipboard = function clientClipboardReceived(stream, mimetype) { + + // Only text/plain is supported for now + if (mimetype !== "text/plain") { + stream.sendAck("Only text/plain supported", Guacamole.Status.Code.UNSUPPORTED); + return; + } + + var reader = new Guacamole.StringReader(stream); + var data = ""; + + // Append any received data to buffer + reader.ontext = function clipboard_text_received(text) { + data += text; + stream.sendAck("Received", Guacamole.Status.Code.SUCCESS); + }; + + // Update state when done + reader.onend = function clipboard_text_end() { + $rootScope.$apply(function updateClipboard() { + managedClient.clipboardData = data; + }); + }; + + }; + + /* TODO: Restore file transfer again */ + + /* + // Handle any received files + client.onfile = function onClientFile(stream, mimetype, filename) { + + // Begin file download + var guacFileStartEvent = $rootScope.$emit('guacClientFileDownloadStart', client, stream.index, mimetype, filename); + if (!guacFileStartEvent.defaultPrevented) { + + var blob_reader = new Guacamole.BlobReader(stream, mimetype); + + // Update progress as data is received + blob_reader.onprogress = function onprogress() { + $rootScope.$emit('guacClientFileDownloadProgress', client, stream.index, mimetype, filename, blob_reader.getLength()); + stream.sendAck("Received", Guacamole.Status.Code.SUCCESS); + }; + + // When complete, prompt for download + blob_reader.onend = function onend() { + $rootScope.$emit('guacClientFileDownloadEnd', client, stream.index, mimetype, filename, blob_reader.getBlob()); + }; + + stream.sendAck("Ready", Guacamole.Status.Code.SUCCESS); + + } + + // Respond with UNSUPPORTED if download (default action) canceled within event handler + else + stream.sendAck("Download canceled", Guacamole.Status.Code.UNSUPPORTED); + + }; + */ + + // Connect the Guacamole client + client.connect(getConnectString(id, connectionParameters)); + + return managedClient; + + }; + + return ManagedClient; + +}]); \ No newline at end of file diff --git a/guacamole/src/main/webapp/app/client/types/ManagedClientState.js b/guacamole/src/main/webapp/app/client/types/ManagedClientState.js new file mode 100644 index 000000000..4ab0922a8 --- /dev/null +++ b/guacamole/src/main/webapp/app/client/types/ManagedClientState.js @@ -0,0 +1,159 @@ +/* + * 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 ManagedClient class used by the guacClientManager service. + */ +angular.module('client').factory('ManagedClientState', [function defineManagedClientState() { + + /** + * Object which represents the state of a Guacamole client and its tunnel, + * including any error conditions. + * + * @constructor + * @param {ManagedClientState|Object} [template={}] + * The object whose properties should be copied within the new + * ManagedClientState. + */ + var ManagedClientState = function ManagedClientState(template) { + + // Use empty object by default + template = template || {}; + + /** + * The current connection state. Valid values are described by + * ManagedClientState.ConnectionState. + * + * @type String + * @default ManagedClientState.ConnectionState.IDLE + */ + this.connectionState = template.connectionState || ManagedClientState.ConnectionState.IDLE; + + /** + * The status code of the current error condition, if connectionState + * is CLIENT_ERROR or TUNNEL_ERROR. For all other connectionState + * values, this will be @link{Guacamole.Status.Code.SUCCESS}. + * + * @type Number + * @default Guacamole.Status.Code.SUCCESS + */ + this.statusCode = template.statusCode || Guacamole.Status.Code.SUCCESS; + + }; + + /** + * Valid connection state strings. Each state string is associated with a + * specific state of a Guacamole connection. + */ + ManagedClientState.ConnectionState = { + + /** + * The Guacamole connection has not yet been attempted. + * + * @type String + */ + IDLE : "IDLE", + + /** + * The Guacamole connection is being established. + * + * @type String + */ + CONNECTING : "CONNECTING", + + /** + * The Guacamole connection has been successfully established, and the + * client is now waiting for receipt of initial graphical data. + * + * @type String + */ + WAITING : "WAITING", + + /** + * The Guacamole connection has been successfully established, and + * initial graphical data has been received. + * + * @type String + */ + CONNECTED : "CONNECTED", + + /** + * The Guacamole connection has terminated successfully. No errors are + * indicated. + * + * @type String + */ + DISCONNECTED : "DISCONNECTED", + + /** + * The Guacamole connection has terminated due to an error reported by + * the client. The associated error code is stored in statusCode. + * + * @type String + */ + CLIENT_ERROR : "CLIENT_ERROR", + + /** + * The Guacamole connection has terminated due to an error reported by + * the tunnel. The associated error code is stored in statusCode. + * + * @type String + */ + TUNNEL_ERROR : "TUNNEL_ERROR" + + }; + + /** + * Sets the current client state and, if given, the associated status code. + * If an error is already represented, this function has no effect. + * + * @param {ManagedClientState} clientState + * The ManagedClientState to update. + * + * @param {String} connectionState + * The connection state to assign to the given ManagedClientState, as + * listed within ManagedClientState.ConnectionState. + * + * @param {Number} [statusCode] + * The status code to assign to the given ManagedClientState, if any, + * as listed within Guacamole.Status.Code. If no status code is + * specified, the status code of the ManagedClientState is not touched. + */ + ManagedClientState.setConnectionState = function(clientState, connectionState, statusCode) { + + // Do not set state after an error is registered + if (clientState.connectionState === ManagedClientState.ConnectionState.TUNNEL_ERROR + || clientState.connectionState === ManagedClientState.ConnectionState.CLIENT_ERROR) + return; + + // Update connection state + clientState.connectionState = connectionState; + + // Set status code, if given + if (statusCode) + clientState.statusCode = statusCode; + + }; + + return ManagedClientState; + +}]); \ No newline at end of file diff --git a/guacamole/src/main/webapp/app/index/controllers/indexController.js b/guacamole/src/main/webapp/app/index/controllers/indexController.js index c927e22e2..6ef71eac5 100644 --- a/guacamole/src/main/webapp/app/index/controllers/indexController.js +++ b/guacamole/src/main/webapp/app/index/controllers/indexController.js @@ -26,32 +26,17 @@ angular.module('index').controller('indexController', ['$scope', '$injector', function indexController($scope, $injector) { - // Get class dependencies + // Required types var PermissionSet = $injector.get("PermissionSet"); - // Get services - var permissionService = $injector.get("permissionService"), - authenticationService = $injector.get("authenticationService"), - $q = $injector.get("$q"), - $document = $injector.get("$document"), - $window = $injector.get("$window"), - $location = $injector.get("$location"); + // Required services + var $document = $injector.get("$document"); + var $location = $injector.get("$location"); + var $q = $injector.get("$q"); + var $window = $injector.get("$window"); + var authenticationService = $injector.get("authenticationService"); + var permissionService = $injector.get("permissionService"); - /* - * Safe $apply implementation from Alex Vanston: - * https://coderwall.com/p/ngisma - */ - $scope.safeApply = function(fn) { - var phase = this.$root.$$phase; - if(phase === '$apply' || phase === '$digest') { - if(fn && (typeof(fn) === 'function')) { - fn(); - } - } else { - this.$apply(fn); - } - }; - /** * The current status notification, or false if no status is currently * shown. From b30e3ce1804126e9a002e44b497bfc00f02aa0f9 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Sun, 28 Dec 2014 19:50:42 -0800 Subject: [PATCH 02/16] GUAC-963: Update status dialog according to client state. --- .../client/controllers/clientController.js | 120 +++++++++--------- .../webapp/app/client/types/ManagedClient.js | 2 +- .../src/main/webapp/translations/en_US.json | 14 +- 3 files changed, 70 insertions(+), 66 deletions(-) diff --git a/guacamole/src/main/webapp/app/client/controllers/clientController.js b/guacamole/src/main/webapp/app/client/controllers/clientController.js index ed853fdca..56d310b6d 100644 --- a/guacamole/src/main/webapp/app/client/controllers/clientController.js +++ b/guacamole/src/main/webapp/app/client/controllers/clientController.js @@ -27,8 +27,8 @@ angular.module('client').controller('clientController', ['$scope', '$routeParams function clientController($scope, $routeParams, $injector) { // Required types - var ClientProperties = $injector.get('ClientProperties'); - var ScrollState = $injector.get('ScrollState'); + var ManagedClientState = $injector.get('ManagedClientState'); + var ScrollState = $injector.get('ScrollState'); // Required services var connectionGroupService = $injector.get('connectionGroupService'); @@ -391,83 +391,79 @@ angular.module('client').controller('clientController', ['$scope', '$routeParams delete keysCurrentlyPressed[keysym]; }); - // Show status dialog when client status changes - $scope.$on('guacClientStateChange', function clientStateChangeListener(event, client, status) { + // Show status dialog when connection status changes + $scope.$watch('client.clientState.connectionState', function clientStateChanged(connectionState) { - // Show new status if not yet connected - if (status !== "connected") { + // Hide status if no known state + if (!connectionState) { + $scope.showStatus(false); + return; + } + + // Get any associated status code + var status = $scope.client.clientState.statusCode; + + // Connecting + if (connectionState === ManagedClientState.ConnectionState.CONNECTING + || connectionState === ManagedClientState.ConnectionState.WAITING) { $scope.showStatus({ title: "CLIENT.DIALOG_HEADER_CONNECTING", - text: "CLIENT.TEXT_CLIENT_STATUS_" + status.toUpperCase() + text: "CLIENT.TEXT_CLIENT_STATUS_" + connectionState.toUpperCase() }); } - // Hide status upon connecting - else - $scope.showStatus(false); + // Client error + else if (connectionState === ManagedClientState.ConnectionState.CLIENT_ERROR) { - }); + // Determine translation name of error + var errorName = (status in CLIENT_ERRORS) ? status.toString(16).toUpperCase() : "DEFAULT"; - // Show status dialog when client errors occur - $scope.$on('guacClientError', function clientErrorListener(event, client, status) { + // Determine whether the reconnect countdown applies + var countdown = (status in CLIENT_AUTO_RECONNECT) ? RECONNECT_COUNTDOWN : null; - // Determine translation name of error - var errorName = (status in CLIENT_ERRORS) ? status.toString(16).toUpperCase() : "DEFAULT"; + // Show error status + $scope.showStatus({ + className: "error", + title: "CLIENT.DIALOG_HEADER_CONNECTION_ERROR", + text: "CLIENT.ERROR_CLIENT_" + errorName, + countdown: countdown, + actions: [ RECONNECT_ACTION ] + }); - // Determine whether the reconnect countdown applies - var countdown = (status in CLIENT_AUTO_RECONNECT) ? RECONNECT_COUNTDOWN : null; + } - // Override any existing status - $scope.showStatus(false); + // Tunnel error + else if (connectionState === ManagedClientState.ConnectionState.TUNNEL_ERROR) { - // Show error status - $scope.showStatus({ - className: "error", - title: "CLIENT.DIALOG_HEADER_CONNECTION_ERROR", - text: "CLIENT.ERROR_CLIENT_" + errorName, - countdown: countdown, - actions: [ RECONNECT_ACTION ] - }); + // Determine translation name of error + var errorName = (status in TUNNEL_ERRORS) ? status.toString(16).toUpperCase() : "DEFAULT"; - }); + // Determine whether the reconnect countdown applies + var countdown = (status in TUNNEL_AUTO_RECONNECT) ? RECONNECT_COUNTDOWN : null; - // Show status dialog when tunnel status changes - $scope.$on('guacTunnelStateChange', function tunnelStateChangeListener(event, tunnel, status) { + // Show error status + $scope.showStatus({ + className: "error", + title: "CLIENT.DIALOG_HEADER_CONNECTION_ERROR", + text: "CLIENT.ERROR_TUNNEL_" + errorName, + countdown: countdown, + actions: [ RECONNECT_ACTION ] + }); - // Show new status only if disconnected - if (status === "closed") { - - // Disconnect - $scope.id = null; + } + // Disconnected + else if (connectionState === ManagedClientState.ConnectionState.DISCONNECTED) { $scope.showStatus({ title: "CLIENT.DIALOG_HEADER_DISCONNECTED", - text: "CLIENT.TEXT_TUNNEL_STATUS_" + status.toUpperCase() + text: "CLIENT.TEXT_CLIENT_STATUS_" + connectionState.toUpperCase(), + actions: [ RECONNECT_ACTION ] }); } - }); - - // Show status dialog when tunnel errors occur - $scope.$on('guacTunnelError', function tunnelErrorListener(event, tunnel, status) { - - // Determine translation name of error - var errorName = (status in TUNNEL_ERRORS) ? status.toString(16).toUpperCase() : "DEFAULT"; - - // Determine whether the reconnect countdown applies - var countdown = (status in TUNNEL_AUTO_RECONNECT) ? RECONNECT_COUNTDOWN : null; - - // Override any existing status - $scope.showStatus(false); - - // Show error status - $scope.showStatus({ - className: "error", - title: "CLIENT.DIALOG_HEADER_CONNECTION_ERROR", - text: "CLIENT.ERROR_TUNNEL_" + errorName, - countdown: countdown, - actions: [ RECONNECT_ACTION ] - }); + // Hide status for all other states + else + $scope.showStatus(false); }); @@ -710,4 +706,12 @@ angular.module('client').controller('clientController', ['$scope', '$routeParams }); }); + // Clean up when view destroyed + $scope.$on('$destroy', function clientViewDestroyed() { + + // Hide any status dialog + $scope.showStatus(false); + + }); + }]); diff --git a/guacamole/src/main/webapp/app/client/types/ManagedClient.js b/guacamole/src/main/webapp/app/client/types/ManagedClient.js index ce2d92e5b..3510ae18a 100644 --- a/guacamole/src/main/webapp/app/client/types/ManagedClient.js +++ b/guacamole/src/main/webapp/app/client/types/ManagedClient.js @@ -288,7 +288,7 @@ angular.module('client').factory('ManagedClient', ['$rootScope', '$injector', // Connected case 3: ManagedClientState.setConnectionState(managedClient.clientState, - ManagedClientState.ConnectionState.DISCONNECTED); + ManagedClientState.ConnectionState.CONNECTED); break; // Connecting, disconnecting, and disconnected are all diff --git a/guacamole/src/main/webapp/translations/en_US.json b/guacamole/src/main/webapp/translations/en_US.json index fa30634a4..cb259dbd3 100644 --- a/guacamole/src/main/webapp/translations/en_US.json +++ b/guacamole/src/main/webapp/translations/en_US.json @@ -82,13 +82,13 @@ "SECTION_HEADER_DISPLAY" : "Display", "SECTION_HEADER_MOUSE_MODE" : "Mouse emulation mode", - "TEXT_ZOOM_AUTO_FIT" : "Automatically fit to browser window", - "TEXT_CLIENT_STATUS_IDLE" : "Idle.", - "TEXT_CLIENT_STATUS_CONNECTING" : "Connecting to Guacamole...", - "TEXT_CLIENT_STATUS_WAITING" : "Connected to Guacamole. Waiting for response...", - "TEXT_TUNNEL_STATUS_CLOSED" : "You have been disconnected. Reload the page to reconnect.", - "TEXT_RECONNECT_COUNTDOWN" : "Reconnecting in {REMAINING} {REMAINING, plural, one{second} other{seconds}}...", - "TEXT_FILE_TRANSFER_PROGRESS" : "{PROGRESS} {UNIT, select, b{B} kb{KB} mb{MB} gb{GB} other{}}", + "TEXT_ZOOM_AUTO_FIT" : "Automatically fit to browser window", + "TEXT_CLIENT_STATUS_IDLE" : "Idle.", + "TEXT_CLIENT_STATUS_CONNECTING" : "Connecting to Guacamole...", + "TEXT_CLIENT_STATUS_DISCONNECTED" : "You have been disconnected.", + "TEXT_CLIENT_STATUS_WAITING" : "Connected to Guacamole. Waiting for response...", + "TEXT_RECONNECT_COUNTDOWN" : "Reconnecting in {REMAINING} {REMAINING, plural, one{second} other{seconds}}...", + "TEXT_FILE_TRANSFER_PROGRESS" : "{PROGRESS} {UNIT, select, b{B} kb{KB} mb{MB} gb{GB} other{}}", "URL_OSK_LAYOUT" : "layouts/en-us-qwerty.xml" From f9c3e02f47b34ea558239430d07259e1a096ded4 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Sun, 28 Dec 2014 20:01:01 -0800 Subject: [PATCH 03/16] GUAC-963: Remove managed client when view is destroyed if client is no longer connected. --- .../client/controllers/clientController.js | 15 +++++++++ .../app/client/services/guacClientManager.js | 33 +++++++++++++++++-- 2 files changed, 46 insertions(+), 2 deletions(-) diff --git a/guacamole/src/main/webapp/app/client/controllers/clientController.js b/guacamole/src/main/webapp/app/client/controllers/clientController.js index 56d310b6d..25990a0b6 100644 --- a/guacamole/src/main/webapp/app/client/controllers/clientController.js +++ b/guacamole/src/main/webapp/app/client/controllers/clientController.js @@ -709,6 +709,21 @@ angular.module('client').controller('clientController', ['$scope', '$routeParams // Clean up when view destroyed $scope.$on('$destroy', function clientViewDestroyed() { + // Remove client from client manager if no longer connected + var managedClient = $scope.client; + if (managedClient) { + + // Get current connection state + var connectionState = managedClient.clientState.connectionState; + + // If disconnected, remove from management + if (connectionState === ManagedClientState.ConnectionState.DISCONNECTED + || connectionState === ManagedClientState.ConnectionState.TUNNEL_ERROR + || connectionState === ManagedClientState.ConnectionState.CLIENT_ERROR) + guacClientManager.removeManagedClient(managedClient.id); + + } + // Hide any status dialog $scope.showStatus(false); diff --git a/guacamole/src/main/webapp/app/client/services/guacClientManager.js b/guacamole/src/main/webapp/app/client/services/guacClientManager.js index 8e200e3ab..fc5482708 100644 --- a/guacamole/src/main/webapp/app/client/services/guacClientManager.js +++ b/guacamole/src/main/webapp/app/client/services/guacClientManager.js @@ -36,6 +36,36 @@ angular.module('client').factory('guacClientManager', ['ManagedClient', */ service.managedClients = {}; + /** + * Removes the existing ManagedClient associated with the connection having + * the given ID, if any. If no such a ManagedClient already exists, this + * function has no effect. + * + * @param {String} id + * The ID of the connection whose ManagedClient should be removed. + * + * @returns {Boolean} + * true if an existing client was removed, false otherwise. + */ + service.removeManagedClient = function replaceManagedClient(id) { + + // Remove client if it exists + if (id in service.managedClients) { + + // Disconnect and remove + service.managedClients[id].client.disconnect(); + delete service.managedClients[id]; + + // A client was removed + return true; + + } + + // No client was removed + return false; + + }; + /** * Creates a new ManagedClient associated with the connection having the * given ID. If such a ManagedClient already exists, it is disconnected and @@ -56,8 +86,7 @@ angular.module('client').factory('guacClientManager', ['ManagedClient', service.replaceManagedClient = function replaceManagedClient(id, connectionParameters) { // Disconnect any existing client - if (id in service.managedClients) - service.managedClients[id].client.disconnect(); + service.removeManagedClient(id); // Set new client return service.managedClients[id] = ManagedClient.getInstance(id, connectionParameters); From 175808503262772258637143068cddb40d766a57 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Sun, 28 Dec 2014 20:10:26 -0800 Subject: [PATCH 04/16] GUAC-963: Clean up guacViewport upon destruction. --- .../app/client/directives/guacViewport.js | 22 ++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/guacamole/src/main/webapp/app/client/directives/guacViewport.js b/guacamole/src/main/webapp/app/client/directives/guacViewport.js index 6d6d213ac..b75549c9d 100644 --- a/guacamole/src/main/webapp/app/client/directives/guacViewport.js +++ b/guacamole/src/main/webapp/app/client/directives/guacViewport.js @@ -28,11 +28,11 @@ angular.module('client').directive('guacViewport', [function guacViewport() { return { // Element only restrict: 'E', - scope: false, + scope: {}, transclude: true, templateUrl: 'app/client/templates/guacViewport.html', - controller: ['$window', '$document', '$element', - function guacViewportController($window, $document, $element) { + controller: ['$scope', '$window', '$document', '$element', + function guacViewportController($scope, $window, $document, $element) { /** * The fullscreen container element. @@ -55,8 +55,12 @@ angular.module('client').directive('guacViewport', [function guacViewport() { */ var currentAdjustedHeight = null; - // Fit container within visible region when window scrolls - $window.onscroll = function fitScrollArea() { + /** + * Resizes the container element inside the guacViewport such that + * it exactly fits within the visible area, even if the browser has + * been scrolled. + */ + var fitVisibleArea = function fitVisibleArea() { // Pull scroll properties var scrollLeft = document.body.scrollLeft; @@ -82,6 +86,14 @@ angular.module('client').directive('guacViewport', [function guacViewport() { }; + // Fit container within visible region when window scrolls + $window.addEventListener('scroll', fitVisibleArea); + + // Clean up event listener on destroy + $scope.$on('$destroy', function destroyViewport() { + $window.removeEventListener('scroll', fitVisibleArea); + }); + }] }; }]); \ No newline at end of file From 3b81525effccd52e342a9bc88f400822f688c8be Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Sun, 28 Dec 2014 22:19:50 -0800 Subject: [PATCH 05/16] GUAC-963: Use $evalAsync() for changes to $scope that may occur synchronously. --- .../src/main/webapp/app/client/types/ManagedClient.js | 4 ++-- .../src/main/webapp/app/element/directives/guacFocus.js | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/guacamole/src/main/webapp/app/client/types/ManagedClient.js b/guacamole/src/main/webapp/app/client/types/ManagedClient.js index 3510ae18a..81c30ae57 100644 --- a/guacamole/src/main/webapp/app/client/types/ManagedClient.js +++ b/guacamole/src/main/webapp/app/client/types/ManagedClient.js @@ -243,7 +243,7 @@ angular.module('client').factory('ManagedClient', ['$rootScope', '$injector', // Update connection state as tunnel state changes tunnel.onstatechange = function tunnelStateChanged(state) { - $rootScope.$apply(function updateTunnelState() { + $rootScope.$evalAsync(function updateTunnelState() { switch (state) { @@ -269,7 +269,7 @@ angular.module('client').factory('ManagedClient', ['$rootScope', '$injector', // Update connection state as client state changes client.onstatechange = function clientStateChanged(clientState) { - $rootScope.$apply(function updateClientState() { + $rootScope.$evalAsync(function updateClientState() { switch (clientState) { diff --git a/guacamole/src/main/webapp/app/element/directives/guacFocus.js b/guacamole/src/main/webapp/app/element/directives/guacFocus.js index 693770712..2331bc0e1 100644 --- a/guacamole/src/main/webapp/app/element/directives/guacFocus.js +++ b/guacamole/src/main/webapp/app/element/directives/guacFocus.js @@ -23,7 +23,7 @@ /** * A directive which allows elements to be manually focused / blurred. */ -angular.module('element').directive('guacFocus', ['$timeout', '$parse', function guacFocus($timeout, $parse) { +angular.module('element').directive('guacFocus', ['$parse', function guacFocus($parse) { return { restrict: 'A', @@ -47,7 +47,7 @@ angular.module('element').directive('guacFocus', ['$timeout', '$parse', function // Set/unset focus depending on value of guacFocus $scope.$watch(guacFocus, function updateFocus(value) { - $timeout(function updateFocusAsync() { + $scope.$evalAsync(function updateFocusAsync() { if (value) element.focus(); else @@ -57,14 +57,14 @@ angular.module('element').directive('guacFocus', ['$timeout', '$parse', function // Set focus flag when focus is received element.addEventListener('focus', function focusReceived() { - $scope.$apply(function setGuacFocus() { + $scope.$evalAsync(function setGuacFocusAsync() { guacFocus.assign($scope, true); }); }); // Unset focus flag when focus is lost element.addEventListener('blur', function focusLost() { - $scope.$apply(function unsetGuacFocus() { + $scope.$evalAsync(function unsetGuacFocusAsync() { guacFocus.assign($scope, false); }); }); From 9862934872e940b66cb2fd6e4c8ed200378e17f7 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Sun, 28 Dec 2014 23:16:55 -0800 Subject: [PATCH 06/16] GUAC-963: Manage display (cursor and size). --- .../app/client/directives/guacClient.js | 27 ++- .../webapp/app/client/types/ManagedClient.js | 25 +++ .../webapp/app/client/types/ManagedDisplay.js | 183 ++++++++++++++++++ 3 files changed, 219 insertions(+), 16 deletions(-) create mode 100644 guacamole/src/main/webapp/app/client/types/ManagedDisplay.js diff --git a/guacamole/src/main/webapp/app/client/directives/guacClient.js b/guacamole/src/main/webapp/app/client/directives/guacClient.js index 8cc3db40d..3abdc1944 100644 --- a/guacamole/src/main/webapp/app/client/directives/guacClient.js +++ b/guacamole/src/main/webapp/app/client/directives/guacClient.js @@ -258,7 +258,7 @@ angular.module('client').directive('guacClient', [function guacClient() { }); // Attach any given managed client - $scope.$watch('client', function(managedClient) { + $scope.$watch('client', function attachManagedClient(managedClient) { // Remove any existing display displayContainer.innerHTML = ""; @@ -274,26 +274,21 @@ angular.module('client').directive('guacClient', [function guacClient() { display = client.getDisplay(); display.scale($scope.client.clientProperties.scale); - // Update the scale of the display when the client display size changes. - display.onresize = function() { - $scope.$apply(updateDisplayScale); - }; - - // Use local cursor if possible, update localCursor flag - display.oncursor = function(canvas, x, y) { - localCursor = mouse.setCursor(canvas, x, y); - }; - // Add display element displayElement = display.getElement(); displayContainer.appendChild(displayElement); - // Do nothing when the display element is clicked on. - displayElement.onclick = function(e) { - e.preventDefault(); - return false; - }; + }); + // Update scale when display is resized + $scope.$watch('client.managedDisplay.size', function setDisplaySize() { + $scope.$evalAsync(updateDisplayScale); + }); + + // Keep local cursor up-to-date + $scope.$watch('client.managedDisplay.cursor', function setCursor(cursor) { + if (cursor) + localCursor = mouse.setCursor(cursor.canvas, cursor.x, cursor.y); }); /* diff --git a/guacamole/src/main/webapp/app/client/types/ManagedClient.js b/guacamole/src/main/webapp/app/client/types/ManagedClient.js index 81c30ae57..0818a0636 100644 --- a/guacamole/src/main/webapp/app/client/types/ManagedClient.js +++ b/guacamole/src/main/webapp/app/client/types/ManagedClient.js @@ -29,6 +29,7 @@ angular.module('client').factory('ManagedClient', ['$rootScope', '$injector', // Required types var ClientProperties = $injector.get('ClientProperties'); var ManagedClientState = $injector.get('ManagedClientState'); + var ManagedDisplay = $injector.get('ManagedDisplay'); // Required services var $window = $injector.get('$window'); @@ -74,6 +75,13 @@ angular.module('client').factory('ManagedClient', ['$rootScope', '$injector', */ this.tunnel = template.tunnel; + /** + * The display associated with the underlying Guacamole client. + * + * @type ManagedDisplay + */ + this.managedDisplay = template.managedDisplay; + /** * The name returned via the Guacamole protocol for this connection, if * any. @@ -89,6 +97,20 @@ angular.module('client').factory('ManagedClient', ['$rootScope', '$injector', */ this.clipboardData = template.clipboardData; + /** + * The current width of the Guacamole display, in pixels. + * + * @type Number + */ + this.displayWidth = template.displayWidth || 0; + + /** + * The current width of the Guacamole display, in pixels. + * + * @type Number + */ + this.displayHeight = template.displayHeight || 0; + /** * The current state of the Guacamole client (idle, connecting, * connected, terminated with error, etc.). @@ -353,6 +375,9 @@ angular.module('client').factory('ManagedClient', ['$rootScope', '$injector', }; + // Manage the client display + managedClient.managedDisplay = ManagedDisplay.getInstance(client.getDisplay()); + /* TODO: Restore file transfer again */ /* diff --git a/guacamole/src/main/webapp/app/client/types/ManagedDisplay.js b/guacamole/src/main/webapp/app/client/types/ManagedDisplay.js new file mode 100644 index 000000000..c70423f70 --- /dev/null +++ b/guacamole/src/main/webapp/app/client/types/ManagedDisplay.js @@ -0,0 +1,183 @@ +/* + * 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 ManagedDisplay class used by the guacClientManager service. + */ +angular.module('client').factory('ManagedDisplay', ['$rootScope', + function defineManagedDisplay($rootScope) { + + /** + * Object which serves as a surrogate interface, encapsulating a Guacamole + * display while it is active, allowing it to be detached and reattached + * from different client views. + * + * @constructor + * @param {ManagedDisplay|Object} [template={}] + * The object whose properties should be copied within the new + * ManagedDisplay. + */ + var ManagedDisplay = function ManagedDisplay(template) { + + // Use empty object by default + template = template || {}; + + /** + * The underlying Guacamole display. + * + * @type Guacamole.Display + */ + this.display = template.display; + + /** + * The current size of the Guacamole display. + * + * @type ManagedDisplay.Dimensions + */ + this.size = new ManagedDisplay.Dimensions(template.size); + + /** + * The current mouse cursor, if any. + * + * @type ManagedDisplay.Cursor + */ + this.cursor = template.cursor; + + }; + + /** + * Object which represents the size of the Guacamole display. + * + * @constructor + * @param {ManagedDisplay.Dimensions|Object} template + * The object whose properties should be copied within the new + * ManagedDisplay.Dimensions. + */ + ManagedDisplay.Dimensions = function Dimensions(template) { + + // Use empty object by default + template = template || {}; + + /** + * The current width of the Guacamole display, in pixels. + * + * @type Number + */ + this.width = template.width || 0; + + /** + * The current width of the Guacamole display, in pixels. + * + * @type Number + */ + this.height = template.height || 0; + + }; + + /** + * Object which represents a mouse cursor used by the Guacamole display. + * + * @constructor + * @param {ManagedDisplay.Cursor|Object} template + * The object whose properties should be copied within the new + * ManagedDisplay.Cursor. + */ + ManagedDisplay.Cursor = function Cursor(template) { + + // Use empty object by default + template = template || {}; + + /** + * The actual mouse cursor image. + * + * @type HTMLCanvasElement + */ + this.canvas = template.canvas; + + /** + * The X coordinate of the cursor hotspot. + * + * @type Number + */ + this.x = template.x; + + /** + * The Y coordinate of the cursor hotspot. + * + * @type Number + */ + this.y = template.y; + + }; + + /** + * Creates a new ManagedDisplay which represents the current state of the + * given Guacamole display. + * + * @param {Guacamole.Display} display + * The Guacamole display to represent. Changes to this display will + * affect this ManagedDisplay. + * + * @returns {ManagedDisplay} + * A new ManagedDisplay which represents the current state of the + * given Guacamole display. + */ + ManagedDisplay.getInstance = function getInstance(display) { + + var managedDisplay = new ManagedDisplay({ + display : display + }); + + // Store changes to display size + display.onresize = function() { + $rootScope.$apply(function updateClientSize() { + managedDisplay.size = new ManagedDisplay.Dimensions({ + width : display.getWidth(), + height : display.getHeight() + }); + }); + }; + + // Store changes to display cursor + display.oncursor = function(canvas, x, y) { + $rootScope.$apply(function updateClientCursor() { + managedDisplay.cursor = new ManagedDisplay.Cursor({ + canvas : canvas, + x : x, + y : y + }); + }); + }; + + // Do nothing when the display element is clicked on + display.getElement().onclick = function(e) { + e.preventDefault(); + return false; + }; + + return managedDisplay; + + }; + + return ManagedDisplay; + +}]); \ No newline at end of file From c71ef76bf5e0dd5a4b322b8787724331cebc0401 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Sun, 28 Dec 2014 23:42:08 -0800 Subject: [PATCH 07/16] GUAC-963: Clean up guacClient somewhat. --- .../app/client/directives/guacClient.js | 108 +++++++----------- 1 file changed, 43 insertions(+), 65 deletions(-) diff --git a/guacamole/src/main/webapp/app/client/directives/guacClient.js b/guacamole/src/main/webapp/app/client/directives/guacClient.js index 3abdc1944..4216b4ea2 100644 --- a/guacamole/src/main/webapp/app/client/directives/guacClient.js +++ b/guacamole/src/main/webapp/app/client/directives/guacClient.js @@ -208,55 +208,6 @@ angular.module('client').directive('guacClient', [function guacClient() { }; - /* - * MOUSE - */ - - // Watch for changes to mouse emulation mode - // Send all received mouse events to the client - mouse.onmousedown = - mouse.onmouseup = - mouse.onmousemove = function(mouseState) { - - if (!client || !display) - return; - - // Send mouse state, show cursor if necessary - display.showCursor(!localCursor); - sendScaledMouseState(mouseState); - - }; - - // Hide software cursor when mouse leaves display - mouse.onmouseout = function() { - if (!display) return; - display.showCursor(false); - }; - - /* - * CLIPBOARD - */ - - // Update active client if clipboard changes - $scope.$on('guacClipboard', function onClipboard(event, mimetype, data) { - if (client) - client.setClipboard(data); - }); - - /* - * SCROLLING - */ - - $scope.$watch('client.clientProperties.scrollLeft', function scrollLeftChanged(scrollLeft) { - main.scrollLeft = scrollLeft; - $scope.client.clientProperties.scrollLeft = main.scrollLeft; - }); - - $scope.$watch('client.clientProperties.scrollTop', function scrollTopChanged(scrollTop) { - main.scrollTop = scrollTop; - $scope.client.clientProperties.scrollTop = main.scrollTop; - }); - // Attach any given managed client $scope.$watch('client', function attachManagedClient(managedClient) { @@ -280,6 +231,18 @@ angular.module('client').directive('guacClient', [function guacClient() { }); + // Update actual view scrollLeft when scroll properties change + $scope.$watch('client.clientProperties.scrollLeft', function scrollLeftChanged(scrollLeft) { + main.scrollLeft = scrollLeft; + $scope.client.clientProperties.scrollLeft = main.scrollLeft; + }); + + // Update actual view scrollTop when scroll properties change + $scope.$watch('client.clientProperties.scrollTop', function scrollTopChanged(scrollTop) { + main.scrollTop = scrollTop; + $scope.client.clientProperties.scrollTop = main.scrollTop; + }); + // Update scale when display is resized $scope.$watch('client.managedDisplay.size', function setDisplaySize() { $scope.$evalAsync(updateDisplayScale); @@ -291,11 +254,7 @@ angular.module('client').directive('guacClient', [function guacClient() { localCursor = mouse.setCursor(cursor.canvas, cursor.x, cursor.y); }); - /* - * MOUSE EMULATION - */ - - // Watch for changes to mouse emulation mode + // Swap mouse emulation modes depending on absolute mode flag $scope.$watch('client.clientProperties.emulateAbsoluteMouse', function(emulateAbsoluteMouse) { if (!client || !display) return; @@ -340,10 +299,6 @@ angular.module('client').directive('guacClient', [function guacClient() { }); - /* - * DISPLAY SCALE / SIZE - */ - // Adjust scale if modified externally $scope.$watch('client.clientProperties.scale', function changeScale(scale) { @@ -392,12 +347,35 @@ angular.module('client').directive('guacClient', [function guacClient() { $scope.$apply(updateDisplayScale); }); - - /* - * KEYBOARD - */ - - // Listen for broadcasted keydown events and fire the appropriate listeners + + // Watch for changes to mouse emulation mode + // Send all received mouse events to the client + mouse.onmousedown = + mouse.onmouseup = + mouse.onmousemove = function(mouseState) { + + if (!client || !display) + return; + + // Send mouse state, show cursor if necessary + display.showCursor(!localCursor); + sendScaledMouseState(mouseState); + + }; + + // Hide software cursor when mouse leaves display + mouse.onmouseout = function() { + if (!display) return; + display.showCursor(false); + }; + + // Update remote clipboard if local clipboard changes + $scope.$on('guacClipboard', function onClipboard(event, mimetype, data) { + if (client) + client.setClipboard(data); + }); + + // 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) { client.sendKeyEvent(1, keysym); @@ -405,7 +383,7 @@ angular.module('client').directive('guacClient', [function guacClient() { } }); - // Listen for broadcasted keyup events and fire the appropriate listeners + // 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) { client.sendKeyEvent(0, keysym); From b197c7c63cb1c5497b79898ee5f37f67996912b4 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Mon, 29 Dec 2014 01:42:03 -0800 Subject: [PATCH 08/16] GUAC-963: List active connections within recent connections. --- .../app/client/directives/guacClient.js | 6 + .../app/client/directives/guacThumbnail.js | 199 ++++++++++++++++++ .../app/client/styles/thumbnail-display.css | 37 ++++ .../app/client/templates/guacThumbnail.html | 34 +++ .../webapp/app/client/types/ManagedDisplay.js | 6 - .../home/directives/guacRecentConnections.js | 65 +++++- .../src/main/webapp/app/home/homeModule.js | 2 +- .../home/templates/guacRecentConnections.html | 21 +- .../webapp/app/home/types/ActiveConnection.js | 55 +++++ .../main/webapp/app/index/styles/lists.css | 4 +- 10 files changed, 416 insertions(+), 13 deletions(-) create mode 100644 guacamole/src/main/webapp/app/client/directives/guacThumbnail.js create mode 100644 guacamole/src/main/webapp/app/client/styles/thumbnail-display.css create mode 100644 guacamole/src/main/webapp/app/client/templates/guacThumbnail.html create mode 100644 guacamole/src/main/webapp/app/home/types/ActiveConnection.js 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}}

+ + +
- +
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 { From 17f272689bb2389cc7e0c95dca25d503df9d5b98 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Mon, 29 Dec 2014 03:43:30 -0800 Subject: [PATCH 09/16] GUAC-963: Prevent interaction with non-interactive display, but do not disturb event propagation. Angular routing behaves oddly if click event propagation is altered. --- .../src/main/webapp/app/client/directives/guacThumbnail.js | 5 ----- .../src/main/webapp/app/client/styles/thumbnail-display.css | 1 + 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/guacamole/src/main/webapp/app/client/directives/guacThumbnail.js b/guacamole/src/main/webapp/app/client/directives/guacThumbnail.js index 06a6c3c4f..99956b769 100644 --- a/guacamole/src/main/webapp/app/client/directives/guacThumbnail.js +++ b/guacamole/src/main/webapp/app/client/directives/guacThumbnail.js @@ -189,11 +189,6 @@ angular.module('client').directive('guacThumbnail', [function guacThumbnail() { }); - // 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 index 405ece1d8..868959793 100644 --- a/guacamole/src/main/webapp/app/client/styles/thumbnail-display.css +++ b/guacamole/src/main/webapp/app/client/styles/thumbnail-display.css @@ -34,4 +34,5 @@ div.thumbnail-main { .thumbnail-main .display { position: absolute; + pointer-events: none; } \ No newline at end of file From 42f360a02b44df332af049a2dde6aecc36085dd5 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Mon, 29 Dec 2014 21:24:37 -0800 Subject: [PATCH 10/16] GUAC-963: Add back and disconnect buttons. --- .../app/client/controllers/clientController.js | 14 ++++++++++++++ .../src/main/webapp/app/client/styles/menu.css | 12 ++++++------ .../main/webapp/app/client/templates/client.html | 15 +++++++++++---- guacamole/src/main/webapp/translations/en_US.json | 8 +++++--- 4 files changed, 36 insertions(+), 13 deletions(-) diff --git a/guacamole/src/main/webapp/app/client/controllers/clientController.js b/guacamole/src/main/webapp/app/client/controllers/clientController.js index 25990a0b6..3330afeb3 100644 --- a/guacamole/src/main/webapp/app/client/controllers/clientController.js +++ b/guacamole/src/main/webapp/app/client/controllers/clientController.js @@ -497,6 +497,20 @@ angular.module('client').controller('clientController', ['$scope', '$routeParams return $scope.client.clientProperties.minZoom >= 1; }; + /** + * Immediately disconnects the currently-connected client, if any. + */ + $scope.disconnect = function disconnect() { + + // Disconnect if client is available + if ($scope.client) + $scope.client.client.disconnect(); + + // Hide menu + $scope.menuShown = false; + + }; + /** * Returns a progress object, as required by $scope.addNotification(), which * contains the given number of bytes as an appropriate combination of diff --git a/guacamole/src/main/webapp/app/client/styles/menu.css b/guacamole/src/main/webapp/app/client/styles/menu.css index b1d6130e5..26acfbbea 100644 --- a/guacamole/src/main/webapp/app/client/styles/menu.css +++ b/guacamole/src/main/webapp/app/client/styles/menu.css @@ -37,8 +37,12 @@ transition: left 0.125s, opacity 0.125s; } +#menu h3 { + margin: 1em; +} + #menu .content { - padding: 1em; + margin: 1em; } #menu .content > * { @@ -65,6 +69,7 @@ border-radius: 0.25em; white-space: pre; display: block; + font-size: 1em; } #menu #mouse-settings .choice { @@ -94,11 +99,6 @@ margin: 1em auto; } -#menu h2 { - padding: 0.25em 0.5em; - font-size: 1em; -} - #menu #keyboard-settings .figure { float: right; max-width: 30%; diff --git a/guacamole/src/main/webapp/app/client/templates/client.html b/guacamole/src/main/webapp/app/client/templates/client.html index 401970d00..7a794d22a 100644 --- a/guacamole/src/main/webapp/app/client/templates/client.html +++ b/guacamole/src/main/webapp/app/client/templates/client.html @@ -54,13 +54,20 @@