diff --git a/guacamole/src/main/webapp/app/client/controllers/clientController.js b/guacamole/src/main/webapp/app/client/controllers/clientController.js index 63411b4e6..63a35bdc8 100644 --- a/guacamole/src/main/webapp/app/client/controllers/clientController.js +++ b/guacamole/src/main/webapp/app/client/controllers/clientController.js @@ -27,12 +27,12 @@ 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'); - var connectionService = $injector.get('connectionService'); + var $location = $injector.get('$location'); + var guacClientManager = $injector.get('guacClientManager'); /** * The minimum number of pixels a drag gesture must move to result in the @@ -142,14 +142,23 @@ angular.module('client').controller('clientController', ['$scope', '$routeParams }; /** - * The reconnect action to be provided along with the object sent to - * showStatus. + * Action which returns the user to the home screen. + */ + var NAVIGATE_BACK_ACTION = { + name : "CLIENT.ACTION_NAVIGATE_BACK", + className : "back button", + callback : function navigateBackCallback() { + $location.path('/'); + } + }; + + /** + * Action which replaces the current client with a newly-connected client. */ var RECONNECT_ACTION = { - name : "CLIENT.ACTION_RECONNECT", - // Handle reconnect action - callback : function reconnectCallback() { - $scope.id = uniqueId; + name : "CLIENT.ACTION_RECONNECT", + callback : function reconnectCallback() { + $scope.client = guacClientManager.replaceManagedClient(uniqueId, $routeParams.params); $scope.showStatus(false); } }; @@ -164,12 +173,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,27 +201,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 || ''; - - // Pull connection name from server - switch ($routeParams.type) { - - // Connection - case 'c': - connectionService.getConnection($routeParams.id).success(function (connection) { - $scope.connectionName = $scope.page.title = connection.name; - }); - break; - - // Connection group - case 'g': - connectionGroupService.getConnectionGroup($routeParams.id).success(function (group) { - $scope.connectionName = $scope.page.title = group.name; - }); - break; - - } + $scope.client = guacClientManager.getManagedClient(uniqueId, $routeParams.params); var keysCurrentlyPressed = {}; @@ -266,9 +249,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 +288,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 +299,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; @@ -354,10 +337,10 @@ angular.module('client').controller('clientController', ['$scope', '$routeParams // Send clipboard data if menu is hidden if (!menuShown && menuShownPreviousState) - $scope.$broadcast('guacClipboard', 'text/plain', $scope.clipboardData); + $scope.$broadcast('guacClipboard', 'text/plain', $scope.client.clipboardData); // Disable client keyboard if the menu is shown - $scope.clientProperties.keyboardEnabled = !menuShown; + $scope.client.clientProperties.keyboardEnabled = !menuShown; }); @@ -385,7 +368,7 @@ angular.module('client').controller('clientController', ['$scope', '$routeParams keyboard.reset(); // Toggle the menu - $scope.safeApply(function() { + $scope.$apply(function() { $scope.menuShown = !$scope.menuShown; }); } @@ -397,114 +380,129 @@ 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) { + // Update page title when client name is received + $scope.$watch('client.name', function clientNameChanged(name) { + $scope.page.title = name; + }); - // Show new status if not yet connected - if (status !== "connected") { + // Show status dialog when connection status changes + $scope.$watch('client.clientState.connectionState', function clientStateChanged(connectionState) { + + // 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 + // 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"; + + // Determine whether the reconnect countdown applies + var countdown = (status in CLIENT_AUTO_RECONNECT) ? RECONNECT_COUNTDOWN : null; + + // Show error status + $scope.showStatus({ + className: "error", + title: "CLIENT.DIALOG_HEADER_CONNECTION_ERROR", + text: "CLIENT.ERROR_CLIENT_" + errorName, + countdown: countdown, + actions: [ NAVIGATE_BACK_ACTION, RECONNECT_ACTION ] + }); + + } + + // Tunnel error + else if (connectionState === ManagedClientState.ConnectionState.TUNNEL_ERROR) { + + // 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 error status + $scope.showStatus({ + className: "error", + title: "CLIENT.DIALOG_HEADER_CONNECTION_ERROR", + text: "CLIENT.ERROR_TUNNEL_" + errorName, + countdown: countdown, + actions: [ NAVIGATE_BACK_ACTION, RECONNECT_ACTION ] + }); + + } + + // Disconnected + else if (connectionState === ManagedClientState.ConnectionState.DISCONNECTED) { + $scope.showStatus({ + title: "CLIENT.DIALOG_HEADER_DISCONNECTED", + text: "CLIENT.TEXT_CLIENT_STATUS_" + connectionState.toUpperCase(), + actions: [ NAVIGATE_BACK_ACTION, RECONNECT_ACTION ] + }); + } + + // Hide status for all other states else $scope.showStatus(false); }); - // Show status dialog when client errors occur - $scope.$on('guacClientError', function clientErrorListener(event, client, status) { - - // Determine translation name of error - var errorName = (status in CLIENT_ERRORS) ? status.toString(16).toUpperCase() : "DEFAULT"; - - // Determine whether the reconnect countdown applies - var countdown = (status in CLIENT_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_CLIENT_" + errorName, - countdown: countdown, - actions: [ RECONNECT_ACTION ] - }); - - }); - - // Show status dialog when tunnel status changes - $scope.$on('guacTunnelStateChange', function tunnelStateChangeListener(event, tunnel, status) { - - // Show new status only if disconnected - if (status === "closed") { - - // Disconnect - $scope.id = null; - - $scope.showStatus({ - title: "CLIENT.DIALOG_HEADER_DISCONNECTED", - text: "CLIENT.TEXT_TUNNEL_STATUS_" + status.toUpperCase() - }); - } - - }); - - // 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 ] - }); - - }); - $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; + }; + + /** + * 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; + }; /** @@ -561,159 +559,27 @@ angular.module('client').controller('clientController', ['$scope', '$routeParams }; - // Mapping of download stream index to notification object - var downloadNotifications = {}; - - // Mapping of download stream index to notification ID - var downloadNotificationIDs = {}; - - $scope.$on('guacClientFileDownloadStart', function handleClientFileDownloadStart(event, guacClient, streamIndex, mimetype, filename) { - $scope.safeApply(function() { - - var notification = { - className : 'download', - title : 'CLIENT.DIALOG_TITLE_FILE_TRANSFER', - text : filename - }; - - downloadNotifications[streamIndex] = notification; - downloadNotificationIDs[streamIndex] = $scope.addNotification(notification); - - }); - }); + // Clean up when view destroyed + $scope.$on('$destroy', function clientViewDestroyed() { - $scope.$on('guacClientFileDownloadProgress', function handleClientFileDownloadProgress(event, guacClient, streamIndex, mimetype, filename, length) { - $scope.safeApply(function() { - - var notification = downloadNotifications[streamIndex]; - if (notification) - notification.progress = getFileProgress('CLIENT.TEXT_FILE_TRANSFER_PROGRESS', length); - - }); - }); - - $scope.$on('guacClientFileDownloadEnd', function handleClientFileDownloadEnd(event, guacClient, streamIndex, mimetype, filename, blob) { - $scope.safeApply(function() { + // Remove client from client manager if no longer connected + var managedClient = $scope.client; + if (managedClient) { - var notification = downloadNotifications[streamIndex]; - var notificationID = downloadNotificationIDs[streamIndex]; - - /** - * Saves the current file. - */ - var saveFile = function saveFile() { - saveAs(blob, filename); - $scope.removeNotification(notificationID); - delete downloadNotifications[streamIndex]; - delete downloadNotificationIDs[streamIndex]; - }; - - // Add download action and remove progress indicator - if (notificationID && notification) { - delete notification.progress; - notification.actions = [ - { - name : 'CLIENT.ACTION_SAVE_FILE', - callback : saveFile - } - ]; - } + // 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); - // Mapping of upload stream index to notification object - var uploadNotifications = {}; - - // Mapping of upload stream index to notification ID - var uploadNotificationIDs = {}; - - $scope.$on('guacClientFileUploadStart', function handleClientFileUploadStart(event, guacClient, streamIndex, mimetype, filename, length) { - $scope.safeApply(function() { - - var notification = { - className : 'upload', - title : 'CLIENT.DIALOG_TITLE_FILE_TRANSFER', - text : filename - }; - - uploadNotifications[streamIndex] = notification; - uploadNotificationIDs[streamIndex] = $scope.addNotification(notification); - - }); - }); + } - $scope.$on('guacClientFileUploadProgress', function handleClientFileUploadProgress(event, guacClient, streamIndex, mimetype, filename, length, offset) { - $scope.safeApply(function() { - - var notification = uploadNotifications[streamIndex]; - if (notification) - notification.progress = getFileProgress('CLIENT.TEXT_FILE_TRANSFER_PROGRESS', offset, length); - - }); - }); - - $scope.$on('guacClientFileUploadEnd', function handleClientFileUploadEnd(event, guacClient, streamIndex, mimetype, filename, length) { - $scope.safeApply(function() { + // Hide any status dialog + $scope.showStatus(false); - var notification = uploadNotifications[streamIndex]; - var notificationID = uploadNotificationIDs[streamIndex]; - - /** - * Close the notification. - */ - var closeNotification = function closeNotification() { - $scope.removeNotification(notificationID); - delete uploadNotifications[streamIndex]; - delete uploadNotificationIDs[streamIndex]; - }; - - // Show that the file has uploaded successfully - if (notificationID && notification) { - delete notification.progress; - notification.actions = [ - { - name : 'CLIENT.ACTION_ACKNOWLEDGE', - callback : closeNotification - } - ]; - } - - }); - }); - - $scope.$on('guacClientFileUploadError', function handleClientFileUploadError(event, guacClient, streamIndex, mimetype, fileName, length, status) { - $scope.safeApply(function() { - - var notification = uploadNotifications[streamIndex]; - var notificationID = uploadNotificationIDs[streamIndex]; - - // Determine translation name of error - var errorName = (status in UPLOAD_ERRORS) ? status.toString(16).toUpperCase() : "DEFAULT"; - - /** - * Close the notification. - */ - var closeNotification = function closeNotification() { - $scope.removeNotification(notificationID); - delete uploadNotifications[streamIndex]; - delete uploadNotificationIDs[streamIndex]; - }; - - // Show that the file upload has failed - if (notificationID && notification) { - delete notification.progress; - notification.actions = [ - { - name : 'CLIENT.ACTION_ACKNOWLEDGE', - callback : closeNotification - } - ]; - notification.text = "CLIENT.ERROR_UPLOAD_" + errorName; - notification.className = "upload error"; - } - - }); }); }]); diff --git a/guacamole/src/main/webapp/app/client/directives/guacClient.js b/guacamole/src/main/webapp/app/client/directives/guacClient.js index 1ef3bcdec..77bd7f164 100644 --- a/guacamole/src/main/webapp/app/client/directives/guacClient.js +++ b/guacamole/src/main/webapp/app/client/directives/guacClient.js @@ -32,46 +32,22 @@ 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 types + var ManagedClient = $injector.get('ManagedClient'); + + // Required services + var $window = $injector.get('$window'); + /** * Whether the local, hardware mouse cursor is in use. * @@ -146,14 +122,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 +131,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; }; @@ -283,158 +211,60 @@ angular.module('client').directive('guacClient', [function guacClient() { }; - /* - * MOUSE - */ + // Attach any given managed client + $scope.$watch('client', function attachManagedClient(managedClient) { - // Watch for changes to mouse emulation mode - // Send all received mouse events to the client - mouse.onmousedown = - mouse.onmouseup = - mouse.onmousemove = function(mouseState) { + // Remove any existing display + displayContainer.innerHTML = ""; - if (!client || !display) + // Only proceed if a client is given + if (!managedClient) return; - // Send mouse state, show cursor if necessary - display.showCursor(!localCursor); - sendScaledMouseState(mouseState); + // Get Guacamole client instance + client = managedClient.client; - }; - - // 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('clientProperties.scrollLeft', function scrollLeftChanged(scrollLeft) { - main.scrollLeft = scrollLeft; - $scope.clientProperties.scrollLeft = main.scrollLeft; - }); - - $scope.$watch('clientProperties.scrollTop', function scrollTopChanged(scrollTop) { - main.scrollTop = scrollTop; - $scope.clientProperties.scrollTop = main.scrollTop; - }); - - /* - * CONNECT / RECONNECT - */ - - /** - * 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) { - - // 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) - return; - - // Get new client instance - var tunnel = guacTunnelFactory.getInstance($scope); - client = guacClientFactory.getInstance($scope, tunnel); - - // Init display + // Attach possibly new display display = client.getDisplay(); - display.scale($scope.clientProperties.scale); - - // Update the scale of the display when the client display size changes. - display.onresize = function() { - $scope.safeApply(updateDisplayScale); - }; - - // Use local cursor if possible, update localCursor flag - display.oncursor = function(canvas, x, y) { - localCursor = mouse.setCursor(canvas, x, y); - }; + display.scale($scope.client.clientProperties.scale); // Add display element displayElement = display.getElement(); - displayContainer.innerHTML = ""; displayContainer.appendChild(displayElement); - // Do nothing when the display element is clicked on. - displayElement.onclick = function(e) { + // Do nothing when the display element is clicked on + display.getElement().onclick = function(e) { e.preventDefault(); 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); - + // 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; }); - /* - * MOUSE EMULATION - */ - - // Watch for changes to mouse emulation mode - $scope.$watch('clientProperties.emulateAbsoluteMouse', function(emulateAbsoluteMouse) { + // 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); + }); + + // 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); + }); + + // Swap mouse emulation modes depending on absolute mode flag + $scope.$watch('client.clientProperties.emulateAbsoluteMouse', function(emulateAbsoluteMouse) { if (!client || !display) return; @@ -478,19 +308,15 @@ angular.module('client').directive('guacClient', [function guacClient() { }); - /* - * DISPLAY SCALE / SIZE - */ - // 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 +327,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,25 +353,48 @@ angular.module('client').directive('guacClient', [function guacClient() { } - $scope.safeApply(updateDisplayScale); + $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.clientProperties.keyboardEnabled && !event.defaultPrevented) { + if ($scope.client.clientProperties.keyboardEnabled && !event.defaultPrevented) { client.sendKeyEvent(1, keysym); event.preventDefault(); } }); - // 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.clientProperties.keyboardEnabled && !event.defaultPrevented) { + if ($scope.client.clientProperties.keyboardEnabled && !event.defaultPrevented) { client.sendKeyEvent(0, keysym); event.preventDefault(); } @@ -561,26 +410,6 @@ angular.module('client').directive('guacClient', [function guacClient() { client.sendKeyEvent(0, keysym); }); - /** - * Converts the given bytes to a base64-encoded string. - * - * @param {Uint8Array} bytes A Uint8Array which contains the data to be - * encoded as base64. - * @return {String} The base64-encoded string. - */ - function getBase64(bytes) { - - var data = ""; - - // Produce binary string from bytes in buffer - for (var i=0; i= bytes.length) { - stream.sendEnd(); - $scope.$emit('guacClientFileUploadProgress', client, stream.index, file.type, file.name, bytes.length, bytes.length); - $scope.$emit('guacClientFileUploadEnd', client, stream.index, file.type, file.name, bytes.length); - } - - // Otherwise, update progress - else - $scope.$emit('guacClientFileUploadProgress', client, stream.index, file.type, file.name, bytes.length, offset); - - }; - - }; - reader.readAsArrayBuffer(file); - - } // Handle and ignore dragenter/dragover displayContainer.addEventListener("dragenter", ignoreEvent, false); @@ -664,12 +431,13 @@ angular.module('client').directive('guacClient', [function guacClient() { e.stopPropagation(); // Ignore file drops if no attached client - if (!client) return; + if (!$scope.client) + return; // Upload each file var files = e.dataTransfer.files; for (var i=0; i + */ + 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 + * 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 + service.removeManagedClient(id); + + // 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/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/styles/thumbnail-display.css b/guacamole/src/main/webapp/app/client/styles/thumbnail-display.css new file mode 100644 index 000000000..868959793 --- /dev/null +++ b/guacamole/src/main/webapp/app/client/styles/thumbnail-display.css @@ -0,0 +1,38 @@ +/* + * 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; + pointer-events: none; +} \ No newline at end of file diff --git a/guacamole/src/main/webapp/app/client/templates/client.html b/guacamole/src/main/webapp/app/client/templates/client.html index 879a53cec..8a071ed53 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 @@
- +
@@ -57,13 +54,20 @@