diff --git a/guacamole/src/main/webapp/app/client/controllers/clientController.js b/guacamole/src/main/webapp/app/client/controllers/clientController.js index fd445d1c7..e1b5c991b 100644 --- a/guacamole/src/main/webapp/app/client/controllers/clientController.js +++ b/guacamole/src/main/webapp/app/client/controllers/clientController.js @@ -35,6 +35,7 @@ angular.module('client').controller('clientController', ['$scope', '$routeParams // Required services var $location = $injector.get('$location'); var authenticationService = $injector.get('authenticationService'); + var clipboardService = $injector.get('clipboardService'); var guacClientManager = $injector.get('guacClientManager'); var guacNotification = $injector.get('guacNotification'); var preferenceService = $injector.get('preferenceService'); @@ -230,11 +231,6 @@ angular.module('client').controller('clientController', ['$scope', '$routeParams $scope.menu.shown = false; }; - // Update the model when clipboard data received from client - $scope.$on('guacClientClipboard', function clientClipboardListener(event, client, mimetype, clipboardData) { - $scope.clipboardData = clipboardData; - }); - /** * The client which should be attached to the client UI. * @@ -376,13 +372,18 @@ angular.module('client').controller('clientController', ['$scope', '$routeParams // Send clipboard data if menu is hidden if (!menuShown && menuShownPreviousState) - $scope.$broadcast('guacClipboard', 'text/plain', $scope.client.clipboardData); + $scope.$broadcast('guacClipboard', 'text/plain', $scope.client.clipboardData); // Disable client keyboard if the menu is shown $scope.client.clientProperties.keyboardEnabled = !menuShown; }); - + + // Update remote clipboard if local clipboard changes + $scope.$on('guacClipboard', function onClipboard(event, mimetype, data) { + $scope.client.clipboardData = data; + }); + $scope.$on('guacKeydown', function keydownListener(event, keysym, keyboard) { keysCurrentlyPressed[keysym] = true; @@ -526,6 +527,19 @@ angular.module('client').controller('clientController', ['$scope', '$routeParams }); } + // Hide status and sync local clipboard once connected + else if (connectionState === ManagedClientState.ConnectionState.CONNECTED) { + + // Sync with local clipboard + clipboardService.getLocalClipboard().then(function clipboardRead(data) { + $scope.$broadcast('guacClipboard', 'text/plain', data); + }); + + // Hide status notification + guacNotification.showStatus(false); + + } + // Hide status for all other states else guacNotification.showStatus(false); diff --git a/guacamole/src/main/webapp/app/client/directives/guacClient.js b/guacamole/src/main/webapp/app/client/directives/guacClient.js index 5ea960ff9..31f17fd03 100644 --- a/guacamole/src/main/webapp/app/client/directives/guacClient.js +++ b/guacamole/src/main/webapp/app/client/directives/guacClient.js @@ -413,7 +413,7 @@ angular.module('client').directive('guacClient', [function guacClient() { }; // Update remote clipboard if local clipboard changes - $scope.$on('guacClipboard', function onClipboard(event, mimetype, data) { + $scope.$watch('client.clipboardData', function clipboardChanged(data) { if (client) client.setClipboard(data); }); diff --git a/guacamole/src/main/webapp/app/client/services/clipboardService.js b/guacamole/src/main/webapp/app/client/services/clipboardService.js new file mode 100644 index 000000000..30a3d8bed --- /dev/null +++ b/guacamole/src/main/webapp/app/client/services/clipboardService.js @@ -0,0 +1,165 @@ +/* + * Copyright (C) 2016 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 accessing local clipboard data. + */ +angular.module('client').factory('clipboardService', ['$injector', + function clipboardService($injector) { + + // Get required services + var $q = $injector.get('$q'); + var $rootScope = $injector.get('$rootScope'); + + var service = {}; + + /** + * A div which is used to hide the clipboard textarea and remove it from + * document flow. + * + * @type Element + */ + var clipElement = document.createElement('div'); + + /** + * The textarea that will be used to hold the local clipboard contents. + * + * @type Element + */ + var clipboardContent = document.createElement('textarea'); + + /** + * The contents of the last clipboard event broadcast by this service when + * the clipboard contents changed. + * + * @type String + */ + var lastClipboardEvent = ''; + + // Ensure textarea is selectable but not visible + clipElement.appendChild(clipboardContent); + clipElement.style.position = 'absolute'; + clipElement.style.width = '1px'; + clipElement.style.height = '1px'; + clipElement.style.left = '-1px'; + clipElement.style.top = '-1px'; + clipElement.style.overflow = 'hidden'; + + // Add textarea to DOM + document.body.appendChild(clipElement); + + /** + * Sets the local clipboard, if possible, to the given text. + * + * @param {String} text + * The text to which the local clipboard should be set. + * + * @return {Promise} + * A promise that will resolve if setting the clipboard was successful, + * and will reject if it failed. + */ + service.setLocalClipboard = function setLocalClipboard(text) { + + var deferred = $q.defer(); + + // Copy the given value into the clipboard DOM element + clipboardContent.value = text; + clipboardContent.select(); + + // Attempt to copy data from clipboard element into local clipboard + if (document.execCommand('copy')) + deferred.resolve(); + else + deferred.reject(); + + // Unfocus the clipboard DOM event to avoid mobile keyboard opening + clipboardContent.blur(); + + return deferred.promise; + }; + + /** + * Get the current value of the local clipboard. + * + * @return {Promise} + * A promise that will resolve with the contents of the local clipboard + * if getting the clipboard was successful, and will reject if it + * failed. + */ + service.getLocalClipboard = function getLocalClipboard() { + + var deferred = $q.defer(); + + // Wait for the next event queue run before attempting to read + // clipboard data (in case the copy/cut has not yet completed) + window.setTimeout(function deferredClipboardRead() { + + // Clear and select the clipboard DOM element + clipboardContent.value = ''; + clipboardContent.focus(); + clipboardContent.select(); + + // Attempt paste local clipboard into clipboard DOM element + if (document.activeElement === clipboardContent && document.execCommand('paste')) + deferred.resolve(clipboardContent.value); + else + deferred.reject(); + + // Unfocus the clipboard DOM event to avoid mobile keyboard opening + clipboardContent.blur(); + + }, 100); + + return deferred.promise; + }; + + /** + * Checks whether the clipboard data has changed, firing a new + * "guacClipboard" event if it has. + */ + var checkClipboard = function checkClipboard() { + service.getLocalClipboard().then(function clipboardRead(data) { + + // Fire clipboard event if the data has changed + if (data !== lastClipboardEvent) { + $rootScope.$broadcast('guacClipboard', 'text/plain', data); + lastClipboardEvent = data; + } + + }); + }; + + // Attempt to read the clipboard if it may have changed + window.addEventListener('load', checkClipboard, true); + window.addEventListener('copy', checkClipboard, true); + window.addEventListener('cut', checkClipboard, true); + window.addEventListener('focus', function focusGained(e) { + + // Only recheck clipboard if it's the window itself that gained focus + if (e.target === window) + checkClipboard(); + + }, true); + + return service; + +}]); diff --git a/guacamole/src/main/webapp/app/client/types/ManagedClient.js b/guacamole/src/main/webapp/app/client/types/ManagedClient.js index a518ca88a..5187cb00c 100644 --- a/guacamole/src/main/webapp/app/client/types/ManagedClient.js +++ b/guacamole/src/main/webapp/app/client/types/ManagedClient.js @@ -38,8 +38,10 @@ angular.module('client').factory('ManagedClient', ['$rootScope', '$injector', // Required services var $document = $injector.get('$document'); var $q = $injector.get('$q'); + var $rootScope = $injector.get('$rootScope'); var $window = $injector.get('$window'); var authenticationService = $injector.get('authenticationService'); + var clipboardService = $injector.get('clipboardService'); var connectionGroupService = $injector.get('connectionGroupService'); var connectionService = $injector.get('connectionService'); var guacAudio = $injector.get('guacAudio'); @@ -403,6 +405,7 @@ angular.module('client').factory('ManagedClient', ['$rootScope', '$injector', reader.onend = function clipboard_text_end() { $rootScope.$apply(function updateClipboard() { managedClient.clipboardData = data; + clipboardService.setLocalClipboard(data); }); };