diff --git a/guacamole/src/main/frontend/src/app/client/controllers/clientController.js b/guacamole/src/main/frontend/src/app/client/controllers/clientController.js index 5dc0beaf0..25e46e4bb 100644 --- a/guacamole/src/main/frontend/src/app/client/controllers/clientController.js +++ b/guacamole/src/main/frontend/src/app/client/controllers/clientController.js @@ -207,6 +207,9 @@ angular.module('client').controller('clientController', ['$scope', '$routeParams if (_.isEmpty(_.intersection(previousClients, $scope.clientGroup.clients))) $scope.menu.shown = false; + // Update newly-attached clients with current contents of clipboard + clipboardService.resyncClipboard(); + }; /** @@ -341,15 +344,6 @@ angular.module('client').controller('clientController', ['$scope', '$routeParams */ var substituteKeysPressed = {}; - /** - * Map of all currently pressed keys (by keysym) to the clipboard contents - * received from the remote desktop while those keys were pressed. All keys - * not currently pressed will not have entries within this map. - * - * @type Object. - */ - var clipboardDataFromKey = {}; - /* * Check to see if all currently pressed keys are in the set of menu keys. */ @@ -486,11 +480,9 @@ angular.module('client').controller('clientController', ['$scope', '$routeParams // Update client state/behavior as visibility of the Guacamole menu changes $scope.$watch('menu.shown', function menuVisibilityChanged(menuShown, menuShownPreviousState) { - // Send clipboard and argument value data once menu is hidden - if (!menuShown && menuShownPreviousState) { - $scope.$broadcast('guacClipboard', $scope.client.clipboardData); + // Send any argument value data once menu is hidden + if (!menuShown && menuShownPreviousState) $scope.applyParameterChanges(); - } // Disable client keyboard if the menu is shown angular.forEach($scope.clientGroup.clients, function updateKeyboardEnabled(client) { @@ -521,19 +513,6 @@ angular.module('client').controller('clientController', ['$scope', '$routeParams iconService.setIcons(canvas); }); - // Watch clipboard for new data, associating it with any pressed keys - $scope.$watch('client.clipboardData', function clipboardChanged(data) { - - // Sync local clipboard as long as the menu is not open - if (!$scope.menu.shown) - clipboardService.setLocalClipboard(data)['catch'](angular.noop); - - // Associate new clipboard data with any currently-pressed key - for (var keysym in keysCurrentlyPressed) - clipboardDataFromKey[keysym] = data; - - }); - // Pull sharing profiles once the tunnel UUID is known $scope.$watch('focusedClient.tunnel.uuid', function retrieveSharingProfiles(uuid) { @@ -651,16 +630,9 @@ angular.module('client').controller('clientController', ['$scope', '$routeParams }); - // Update pressed keys as they are released, synchronizing the clipboard - // with any data that appears to have come from those key presses + // Update pressed keys as they are released $scope.$on('guacKeyup', function keyupListener(event, keysym, keyboard) { - // Sync local clipboard with any clipboard data received while this - // key was pressed (if any) as long as the menu is not open - var clipboardData = clipboardDataFromKey[keysym]; - if (clipboardData && !$scope.menu.shown) - clipboardService.setLocalClipboard(clipboardData)['catch'](angular.noop); - // Deal with substitute key presses if (substituteKeysPressed[keysym]) { event.preventDefault(); @@ -669,10 +641,8 @@ angular.module('client').controller('clientController', ['$scope', '$routeParams } // Mark key as released - else { - delete clipboardDataFromKey[keysym]; + else delete keysCurrentlyPressed[keysym]; - } }); diff --git a/guacamole/src/main/frontend/src/app/client/directives/guacClient.js b/guacamole/src/main/frontend/src/app/client/directives/guacClient.js index 6e47c26b2..8b160968f 100644 --- a/guacamole/src/main/frontend/src/app/client/directives/guacClient.js +++ b/guacamole/src/main/frontend/src/app/client/directives/guacClient.js @@ -413,10 +413,7 @@ angular.module('client').directive('guacClient', [function guacClient() { // Update remote clipboard if local clipboard changes $scope.$on('guacClipboard', function onClipboard(event, data) { - if (client) { - ManagedClient.setClipboard($scope.client, data); - $scope.client.clipboardData = data; - } + ManagedClient.setClipboard($scope.client, data); }); // Translate local keydown events to remote keydown events if keyboard is enabled diff --git a/guacamole/src/main/frontend/src/app/client/directives/guacClientNotification.js b/guacamole/src/main/frontend/src/app/client/directives/guacClientNotification.js index 9021b0b7b..719bfc45d 100644 --- a/guacamole/src/main/frontend/src/app/client/directives/guacClientNotification.js +++ b/guacamole/src/main/frontend/src/app/client/directives/guacClientNotification.js @@ -305,23 +305,6 @@ angular.module('client').directive('guacClientNotification', [function guacClien }); } - // Hide status and sync local clipboard once connected - else if (connectionState === ManagedClientState.ConnectionState.CONNECTED) { - - // TODO: Move clipboard sync elsewhere - - // Sync with local clipboard - /* - clipboardService.getLocalClipboard().then(function clipboardRead(data) { - $scope.$broadcast('guacClipboard', data); - }, angular.noop); - */ - - // Hide status notification - $scope.status = false; - - } - // Hide status for all other states else $scope.status = false; diff --git a/guacamole/src/main/frontend/src/app/client/templates/client.html b/guacamole/src/main/frontend/src/app/client/templates/client.html index 9d4f52209..1299afc11 100644 --- a/guacamole/src/main/frontend/src/app/client/templates/client.html +++ b/guacamole/src/main/frontend/src/app/client/templates/client.html @@ -100,7 +100,7 @@

{{'CLIENT.SECTION_HEADER_CLIPBOARD' | translate}}

{{'CLIENT.HELP_CLIPBOARD' | translate}}

- +
diff --git a/guacamole/src/main/frontend/src/app/client/types/ManagedClient.js b/guacamole/src/main/frontend/src/app/client/types/ManagedClient.js index 5da9f1828..6ae30403a 100644 --- a/guacamole/src/main/frontend/src/app/client/types/ManagedClient.js +++ b/guacamole/src/main/frontend/src/app/client/types/ManagedClient.js @@ -42,6 +42,7 @@ angular.module('client').factory('ManagedClient', ['$rootScope', '$injector', var $window = $injector.get('$window'); var activeConnectionService = $injector.get('activeConnectionService'); var authenticationService = $injector.get('authenticationService'); + var clipboardService = $injector.get('clipboardService'); var connectionGroupService = $injector.get('connectionGroupService'); var connectionService = $injector.get('connectionService'); var preferenceService = $injector.get('preferenceService'); @@ -146,16 +147,6 @@ angular.module('client').factory('ManagedClient', ['$rootScope', '$injector', */ this.thumbnail = template.thumbnail; - /** - * The current clipboard contents. - * - * @type ClipboardData - */ - this.clipboardData = template.clipboardData || new ClipboardData({ - type : 'text/plain', - data : '' - }); - /** * The current state of all parameters requested by the server via * "required" instructions, where each object key is the name of a @@ -448,9 +439,10 @@ angular.module('client').factory('ManagedClient', ['$rootScope', '$injector', ManagedClientState.setConnectionState(managedClient.clientState, ManagedClientState.ConnectionState.CONNECTED); - // Send any clipboard data already provided - if (managedClient.clipboardData) - ManagedClient.setClipboard(managedClient, managedClient.clipboardData); + // Sync current clipboard data + clipboardService.getClipboard().then((data) => { + ManagedClient.setClipboard(managedClient, data); + }, angular.noop); // Begin streaming audio input if possible requestAudioStream(client); @@ -545,12 +537,11 @@ angular.module('client').factory('ManagedClient', ['$rootScope', '$injector', // Set clipboard contents once stream is finished reader.onend = function textComplete() { - $rootScope.$apply(function updateClipboard() { - managedClient.clipboardData = new ClipboardData({ - type : mimetype, - data : data - }); - }); + clipboardService.setClipboard(new ClipboardData({ + source : managedClient.id, + type : mimetype, + data : data + }), managedClient)['catch'](angular.noop); }; } @@ -559,12 +550,11 @@ angular.module('client').factory('ManagedClient', ['$rootScope', '$injector', else { reader = new Guacamole.BlobReader(stream, mimetype); reader.onend = function blobComplete() { - $rootScope.$apply(function updateClipboard() { - managedClient.clipboardData = new ClipboardData({ - type : mimetype, - data : reader.getBlob() - }); - }); + clipboardService.setClipboard(new ClipboardData({ + source : managedClient.id, + type : mimetype, + data : reader.getBlob() + }), managedClient)['catch'](angular.noop); }; } @@ -693,7 +683,9 @@ angular.module('client').factory('ManagedClient', ['$rootScope', '$injector', /** * Sends the given clipboard data over the given Guacamole client, setting - * the contents of the remote clipboard to the data provided. + * the contents of the remote clipboard to the data provided. If the given + * clipboard data was originally received from that client, the data is + * ignored and this function has no effect. * * @param {ManagedClient} managedClient * The ManagedClient over which the given clipboard data is to be sent. @@ -703,6 +695,10 @@ angular.module('client').factory('ManagedClient', ['$rootScope', '$injector', */ ManagedClient.setClipboard = function setClipboard(managedClient, data) { + // Ignore clipboard data that was received from this connection + if (data.source === managedClient.id) + return; + var writer; // Create stream with proper mimetype diff --git a/guacamole/src/main/frontend/src/app/clipboard/directives/guacClipboard.js b/guacamole/src/main/frontend/src/app/clipboard/directives/guacClipboard.js index 70485d8c3..fedca828b 100644 --- a/guacamole/src/main/frontend/src/app/clipboard/directives/guacClipboard.js +++ b/guacamole/src/main/frontend/src/app/clipboard/directives/guacClipboard.js @@ -18,10 +18,10 @@ */ /** - * A directive provides an editor whose contents are exposed via a - * ClipboardData object via the "data" attribute. If this data should also be - * synced to the local clipboard, or sent via a connected Guacamole client - * using a "guacClipboard" event, it is up to external code to do so. + * A directive provides an editor for the clipboard content maintained by + * clipboardService. Changes to the clipboard by clipboardService will + * automatically be reflected in the editor, and changes in the editor will + * automatically be reflected in the clipboard by clipboardService. */ angular.module('clipboard').directive('guacClipboard', ['$injector', function guacClipboard($injector) { @@ -29,6 +29,9 @@ angular.module('clipboard').directive('guacClipboard', ['$injector', // Required types var ClipboardData = $injector.get('ClipboardData'); + // Required services + var clipboardService = $injector.get('clipboardService'); + /** * Configuration object for the guacClipboard directive. * @@ -40,20 +43,6 @@ angular.module('clipboard').directive('guacClipboard', ['$injector', templateUrl : 'app/clipboard/templates/guacClipboard.html' }; - // Scope properties exposed by the guacClipboard directive - config.scope = { - - /** - * The data to display within the field provided by this directive. This - * data will modified or replaced when the user manually alters the - * contents of the field. - * - * @type ClipboardData - */ - data : '=' - - }; - // guacClipboard directive controller config.controller = ['$scope', '$injector', '$element', function guacClipboardController($scope, $injector, $element) { @@ -75,12 +64,27 @@ angular.module('clipboard').directive('guacClipboard', ['$injector', var updateClipboardData = function updateClipboardData() { // Read contents of clipboard textarea - $scope.$evalAsync(function assignClipboardText() { - $scope.data = new ClipboardData({ - type : 'text/plain', - data : element.value - }); - }); + clipboardService.setClipboard(new ClipboardData({ + type : 'text/plain', + data : element.value + })); + + }; + + /** + * Updates the contents of the clipboard editor to the given data. + * + * @param {ClipboardData} data + * The ClipboardData to display within the clipboard editor for + * editing. + */ + var updateClipboardEditor = function updateClipboardEditor(data) { + + // If the clipboard data is a string, render it as text + if (typeof data.data === 'string') + element.value = data.data; + + // Ignore other data types for now }; @@ -89,17 +93,15 @@ angular.module('clipboard').directive('guacClipboard', ['$injector', element.addEventListener('input', updateClipboardData); element.addEventListener('change', updateClipboardData); - // Watch clipboard for new data, updating the clipboard textarea as - // necessary - $scope.$watch('data', function clipboardDataChanged(data) { + // Update remote clipboard if local clipboard changes + $scope.$on('guacClipboard', function clipboardChanged(event, data) { + updateClipboardEditor(data); + }); - // If the clipboard data is a string, render it as text - if (typeof data.data === 'string') - element.value = data.data; - - // Ignore other data types for now - - }); // end $scope.data watch + // Init clipboard editor with current clipboard contents + clipboardService.getClipboard().then((data) => { + updateClipboardEditor(data); + }, angular.noop); }]; diff --git a/guacamole/src/main/frontend/src/app/clipboard/services/clipboardService.js b/guacamole/src/main/frontend/src/app/clipboard/services/clipboardService.js index cae6c0b28..4ad9ee644 100644 --- a/guacamole/src/main/frontend/src/app/clipboard/services/clipboardService.js +++ b/guacamole/src/main/frontend/src/app/clipboard/services/clipboardService.js @@ -18,18 +18,32 @@ */ /** - * A service for accessing local clipboard data. + * A service for maintaining and accessing clipboard data. If possible, this + * service will leverage the local clipboard. If the local clipboard is not + * available, an internal in-memory clipboard will be used instead. */ angular.module('clipboard').factory('clipboardService', ['$injector', function clipboardService($injector) { // Get required services - var $q = $injector.get('$q'); - var $window = $injector.get('$window'); + var $q = $injector.get('$q'); + var $window = $injector.get('$window'); + var $rootScope = $injector.get('$rootScope'); + var sessionStorageFactory = $injector.get('sessionStorageFactory'); // Required types var ClipboardData = $injector.get('ClipboardData'); + /** + * Getter/setter which retrieves or sets the current stored clipboard + * contents. The stored clipboard contents are strictly internal to + * Guacamole, and may not reflect the local clipboard if local clipboard + * access is unavailable. + * + * @type Function + */ + var storedClipboardData = sessionStorageFactory.create(new ClipboardData()); + var service = {}; /** @@ -175,7 +189,7 @@ angular.module('clipboard').factory('clipboardService', ['$injector', * A promise that will resolve if setting the clipboard was successful, * and will reject if it failed. */ - service.setLocalClipboard = function setLocalClipboard(data) { + var setLocalClipboard = function setLocalClipboard(data) { var deferred = $q.defer(); @@ -423,7 +437,7 @@ angular.module('clipboard').factory('clipboardService', ['$injector', * if getting the clipboard was successful, and will reject if it * failed. */ - service.getLocalClipboard = function getLocalClipboard() { + var getLocalClipboard = function getLocalClipboard() { // If the clipboard is already being read, do not overlap the read // attempts; instead share the result across all requests @@ -548,6 +562,68 @@ angular.module('clipboard').factory('clipboardService', ['$injector', }; + /** + * Returns the current value of the internal clipboard shared across all + * active Guacamole connections running within the current browser tab. If + * access to the local clipboard is available, the internal clipboard is + * first synchronized with the current local clipboard contents. If access + * to the local clipboard is unavailable, only the internal clipboard will + * be used. + * + * @return {Promise.} + * A promise that will resolve with the contents of the internal + * clipboard, first retrieving those contents from the local clipboard + * if permission to do so has been granted. This promise is always + * resolved. + */ + service.getClipboard = function getClipboard() { + return getLocalClipboard().then((data) => storedClipboardData(data), () => storedClipboardData()); + }; + + /** + * Sets the content of the internal clipboard shared across all active + * Guacamole connections running within the current browser tab. If + * access to the local clipboard is available, the local clipboard is + * first set to the provided clipboard content. If access to the local + * clipboard is unavailable, only the internal clipboard will be used. A + * "guacClipboard" event will be broadcast with the assigned data once the + * operation has completed. + * + * @param {ClipboardData} data + * The data to assign to the clipboard. + * + * @return {Promise} + * A promise that will resolve after the clipboard content has been + * set. This promise is always resolved. + */ + service.setClipboard = function setClipboard(data) { + return setLocalClipboard(data).finally(() => { + + // Update internal clipboard and broadcast event notifying of + // updated contents + storedClipboardData(data); + $rootScope.$broadcast('guacClipboard', data); + + // Ensure promise is resolved (this function may be called from + // the promise rejection handler) + return data; + + }); + }; + + /** + * Resynchronizes the local and internal clipboards, setting the contents + * of the internal clipboard to that of the local clipboard (if local + * clipboard access is granted) and broadcasting a "guacClipboard" event + * with the current internal clipboard contents for consumption by external + * components like the "guacClient" directive. + */ + service.resyncClipboard = function resyncClipboard() { + service.getClipboard().then(function clipboardRead(data) { + return service.setClipboard(data); + }, angular.noop); + }; + return service; }]); diff --git a/guacamole/src/main/frontend/src/app/clipboard/types/ClipboardData.js b/guacamole/src/main/frontend/src/app/clipboard/types/ClipboardData.js index f9f573a56..34f699a29 100644 --- a/guacamole/src/main/frontend/src/app/clipboard/types/ClipboardData.js +++ b/guacamole/src/main/frontend/src/app/clipboard/types/ClipboardData.js @@ -36,6 +36,15 @@ angular.module('clipboard').factory('ClipboardData', [function defineClipboardDa // Use empty object by default template = template || {}; + /** + * The ID of the ManagedClient handling the remote desktop connection + * that originated this clipboard data, or null if the data originated + * from the clipboard editor or local clipboard. + * + * @type {string} + */ + this.source = template.source; + /** * The mimetype of the data currently stored within the clipboard. * diff --git a/guacamole/src/main/frontend/src/app/index/controllers/indexController.js b/guacamole/src/main/frontend/src/app/index/controllers/indexController.js index 8e6413250..1ed49b7a8 100644 --- a/guacamole/src/main/frontend/src/app/index/controllers/indexController.js +++ b/guacamole/src/main/frontend/src/app/index/controllers/indexController.js @@ -208,25 +208,15 @@ angular.module('index').controller('indexController', ['$scope', '$injector', keyboard.reset(); }); - /** - * Checks whether the clipboard data has changed, firing a new - * "guacClipboard" event if it has. - */ - var checkClipboard = function checkClipboard() { - clipboardService.getLocalClipboard().then(function clipboardRead(data) { - $scope.$broadcast('guacClipboard', data); - }, angular.noop); - }; - // Attempt to read the clipboard if it may have changed - $window.addEventListener('load', checkClipboard, true); - $window.addEventListener('copy', checkClipboard); - $window.addEventListener('cut', checkClipboard); + $window.addEventListener('load', clipboardService.resyncClipboard, true); + $window.addEventListener('copy', clipboardService.resyncClipboard); + $window.addEventListener('cut', clipboardService.resyncClipboard); $window.addEventListener('focus', function focusGained(e) { // Only recheck clipboard if it's the window itself that gained focus if (e.target === $window) - checkClipboard(); + clipboardService.resyncClipboard(); }, true);