Merge pull request #40 from glyptodon/managed-client

GUAC-963: Maintain connected clients in the background
This commit is contained in:
James Muehlner
2014-12-31 10:51:41 -08:00
25 changed files with 2088 additions and 937 deletions

View File

@@ -27,12 +27,12 @@ angular.module('client').controller('clientController', ['$scope', '$routeParams
function clientController($scope, $routeParams, $injector) { function clientController($scope, $routeParams, $injector) {
// Required types // Required types
var ClientProperties = $injector.get('ClientProperties'); var ManagedClientState = $injector.get('ManagedClientState');
var ScrollState = $injector.get('ScrollState'); var ScrollState = $injector.get('ScrollState');
// Required services // Required services
var connectionGroupService = $injector.get('connectionGroupService'); var $location = $injector.get('$location');
var connectionService = $injector.get('connectionService'); var guacClientManager = $injector.get('guacClientManager');
/** /**
* The minimum number of pixels a drag gesture must move to result in the * 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 * Action which returns the user to the home screen.
* showStatus. */
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 = { var RECONNECT_ACTION = {
name : "CLIENT.ACTION_RECONNECT", name : "CLIENT.ACTION_RECONNECT",
// Handle reconnect action callback : function reconnectCallback() {
callback : function reconnectCallback() { $scope.client = guacClientManager.replaceManagedClient(uniqueId, $routeParams.params);
$scope.id = uniqueId;
$scope.showStatus(false); $scope.showStatus(false);
} }
}; };
@@ -164,12 +173,6 @@ angular.module('client').controller('clientController', ['$scope', '$routeParams
remaining: 15 remaining: 15
}; };
// Client settings and state
$scope.clientProperties = new ClientProperties();
// Initialize clipboard data to an empty string
$scope.clipboardData = "";
// Hide menu by default // Hide menu by default
$scope.menuShown = false; $scope.menuShown = false;
@@ -198,27 +201,7 @@ angular.module('client').controller('clientController', ['$scope', '$routeParams
* as well as any extra parameters if set. * as well as any extra parameters if set.
*/ */
var uniqueId = $routeParams.type + '/' + $routeParams.id; var uniqueId = $routeParams.type + '/' + $routeParams.id;
$scope.id = uniqueId; $scope.client = guacClientManager.getManagedClient(uniqueId, $routeParams.params);
$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;
}
var keysCurrentlyPressed = {}; var keysCurrentlyPressed = {};
@@ -266,9 +249,9 @@ angular.module('client').controller('clientController', ['$scope', '$routeParams
} }
// Scroll display if absolute mouse is in use // Scroll display if absolute mouse is in use
else if ($scope.clientProperties.emulateAbsoluteMouse) { else if ($scope.client.clientProperties.emulateAbsoluteMouse) {
$scope.clientProperties.scrollLeft -= deltaX; $scope.client.clientProperties.scrollLeft -= deltaX;
$scope.clientProperties.scrollTop -= deltaY; $scope.client.clientProperties.scrollTop -= deltaY;
} }
return false; return false;
@@ -305,7 +288,7 @@ angular.module('client').controller('clientController', ['$scope', '$routeParams
$scope.clientPinch = function clientPinch(inProgress, startLength, currentLength, centerX, centerY) { $scope.clientPinch = function clientPinch(inProgress, startLength, currentLength, centerX, centerY) {
// Do not handle pinch gestures while relative mouse is in use // Do not handle pinch gestures while relative mouse is in use
if (!$scope.clientProperties.emulateAbsoluteMouse) if (!$scope.client.clientProperties.emulateAbsoluteMouse)
return false; return false;
// Stop gesture if not in progress // 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 // Set initial scale if gesture has just started
if (!initialScale) { if (!initialScale) {
initialScale = $scope.clientProperties.scale; initialScale = $scope.client.clientProperties.scale;
initialCenterX = (centerX + $scope.clientProperties.scrollLeft) / initialScale; initialCenterX = (centerX + $scope.client.clientProperties.scrollLeft) / initialScale;
initialCenterY = (centerY + $scope.clientProperties.scrollTop) / initialScale; initialCenterY = (centerY + $scope.client.clientProperties.scrollTop) / initialScale;
} }
// Determine new scale absolutely // Determine new scale absolutely
var currentScale = initialScale * currentLength / startLength; var currentScale = initialScale * currentLength / startLength;
// Fix scale within limits - scroll will be miscalculated otherwise // Fix scale within limits - scroll will be miscalculated otherwise
currentScale = Math.max(currentScale, $scope.clientProperties.minScale); currentScale = Math.max(currentScale, $scope.client.clientProperties.minScale);
currentScale = Math.min(currentScale, $scope.clientProperties.maxScale); currentScale = Math.min(currentScale, $scope.client.clientProperties.maxScale);
// Update scale based on pinch distance // Update scale based on pinch distance
$scope.autoFit = false; $scope.autoFit = false;
$scope.clientProperties.autoFit = false; $scope.client.clientProperties.autoFit = false;
$scope.clientProperties.scale = currentScale; $scope.client.clientProperties.scale = currentScale;
// Scroll display to keep original pinch location centered within current pinch // Scroll display to keep original pinch location centered within current pinch
$scope.clientProperties.scrollLeft = initialCenterX * currentScale - centerX; $scope.client.clientProperties.scrollLeft = initialCenterX * currentScale - centerX;
$scope.clientProperties.scrollTop = initialCenterY * currentScale - centerY; $scope.client.clientProperties.scrollTop = initialCenterY * currentScale - centerY;
return false; return false;
@@ -354,10 +337,10 @@ angular.module('client').controller('clientController', ['$scope', '$routeParams
// Send clipboard data if menu is hidden // Send clipboard data if menu is hidden
if (!menuShown && menuShownPreviousState) 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 // 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(); keyboard.reset();
// Toggle the menu // Toggle the menu
$scope.safeApply(function() { $scope.$apply(function() {
$scope.menuShown = !$scope.menuShown; $scope.menuShown = !$scope.menuShown;
}); });
} }
@@ -397,114 +380,129 @@ angular.module('client').controller('clientController', ['$scope', '$routeParams
delete keysCurrentlyPressed[keysym]; delete keysCurrentlyPressed[keysym];
}); });
// Show status dialog when client status changes // Update page title when client name is received
$scope.$on('guacClientStateChange', function clientStateChangeListener(event, client, status) { $scope.$watch('client.name', function clientNameChanged(name) {
$scope.page.title = name;
});
// Show new status if not yet connected // Show status dialog when connection status changes
if (status !== "connected") { $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({ $scope.showStatus({
title: "CLIENT.DIALOG_HEADER_CONNECTING", 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 else
$scope.showStatus(false); $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() { $scope.formattedScale = function formattedScale() {
return Math.round($scope.clientProperties.scale * 100); return Math.round($scope.client.clientProperties.scale * 100);
}; };
$scope.zoomIn = function zoomIn() { $scope.zoomIn = function zoomIn() {
$scope.autoFit = false; $scope.autoFit = false;
$scope.clientProperties.autoFit = false; $scope.client.clientProperties.autoFit = false;
$scope.clientProperties.scale += 0.1; $scope.client.clientProperties.scale += 0.1;
}; };
$scope.zoomOut = function zoomOut() { $scope.zoomOut = function zoomOut() {
$scope.clientProperties.autoFit = false; $scope.client.clientProperties.autoFit = false;
$scope.clientProperties.scale -= 0.1; $scope.client.clientProperties.scale -= 0.1;
}; };
$scope.autoFit = true; $scope.autoFit = true;
$scope.changeAutoFit = function changeAutoFit() { $scope.changeAutoFit = function changeAutoFit() {
if ($scope.autoFit && $scope.clientProperties.minScale) { if ($scope.autoFit && $scope.client.clientProperties.minScale) {
$scope.clientProperties.autoFit = true; $scope.client.clientProperties.autoFit = true;
} else { } else {
$scope.clientProperties.autoFit = false; $scope.client.clientProperties.autoFit = false;
$scope.clientProperties.scale = 1; $scope.client.clientProperties.scale = 1;
} }
}; };
$scope.autoFitDisabled = function() { $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 // Clean up when view destroyed
var downloadNotifications = {}; $scope.$on('$destroy', function clientViewDestroyed() {
// 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);
});
});
$scope.$on('guacClientFileDownloadProgress', function handleClientFileDownloadProgress(event, guacClient, streamIndex, mimetype, filename, length) { // Remove client from client manager if no longer connected
$scope.safeApply(function() { var managedClient = $scope.client;
if (managedClient) {
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() {
var notification = downloadNotifications[streamIndex]; // Get current connection state
var notificationID = downloadNotificationIDs[streamIndex]; var connectionState = managedClient.clientState.connectionState;
/**
* 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
}
];
}
}); // 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) { // Hide any status dialog
$scope.safeApply(function() { $scope.showStatus(false);
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() {
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";
}
});
}); });
}]); }]);

View File

@@ -32,46 +32,22 @@ angular.module('client').directive('guacClient', [function guacClient() {
scope: { 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', templateUrl: 'app/client/templates/guacClient.html',
controller: ['$scope', '$injector', '$element', function guacClientController($scope, $injector, $element) { controller: ['$scope', '$injector', '$element', function guacClientController($scope, $injector, $element) {
/* // Required types
* Safe $apply implementation from Alex Vanston: var ManagedClient = $injector.get('ManagedClient');
* https://coderwall.com/p/ngisma
*/ // Required services
$scope.safeApply = function(fn) { var $window = $injector.get('$window');
var phase = this.$root.$$phase;
if(phase === '$apply' || phase === '$digest') {
if(fn && (typeof(fn) === 'function')) {
fn();
}
} else {
this.$apply(fn);
}
};
/** /**
* Whether the local, hardware mouse cursor is in use. * 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 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 * Updates the scale of the attached Guacamole.Client based on current window
* size and "auto-fit" setting. * size and "auto-fit" setting.
@@ -163,60 +131,20 @@ angular.module('client').directive('guacClient', [function guacClient() {
if (!display) return; if (!display) return;
// Calculate scale to fit screen // Calculate scale to fit screen
$scope.clientProperties.minScale = Math.min( $scope.client.clientProperties.minScale = Math.min(
main.offsetWidth / Math.max(display.getWidth(), 1), main.offsetWidth / Math.max(display.getWidth(), 1),
main.offsetHeight / Math.max(display.getHeight(), 1) main.offsetHeight / Math.max(display.getHeight(), 1)
); );
// Calculate appropriate maximum zoom level // 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 // Clamp zoom level, maintain auto-fit
if (display.getScale() < $scope.clientProperties.minScale || $scope.clientProperties.autoFit) if (display.getScale() < $scope.client.clientProperties.minScale || $scope.client.clientProperties.autoFit)
$scope.clientProperties.scale = $scope.clientProperties.minScale; $scope.client.clientProperties.scale = $scope.client.clientProperties.minScale;
else if (display.getScale() > $scope.clientProperties.maxScale) else if (display.getScale() > $scope.client.clientProperties.maxScale)
$scope.clientProperties.scale = $scope.clientProperties.maxScale; $scope.client.clientProperties.scale = $scope.client.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;
}; };
@@ -283,158 +211,60 @@ angular.module('client').directive('guacClient', [function guacClient() {
}; };
/* // Attach any given managed client
* MOUSE $scope.$watch('client', function attachManagedClient(managedClient) {
*/
// Watch for changes to mouse emulation mode // Remove any existing display
// Send all received mouse events to the client displayContainer.innerHTML = "";
mouse.onmousedown =
mouse.onmouseup =
mouse.onmousemove = function(mouseState) {
if (!client || !display) // Only proceed if a client is given
if (!managedClient)
return; return;
// Send mouse state, show cursor if necessary // Get Guacamole client instance
display.showCursor(!localCursor); client = managedClient.client;
sendScaledMouseState(mouseState);
}; // Attach possibly new display
// 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
display = client.getDisplay(); 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);
};
// Use local cursor if possible, update localCursor flag
display.oncursor = function(canvas, x, y) {
localCursor = mouse.setCursor(canvas, x, y);
};
// Add display element // Add display element
displayElement = display.getElement(); displayElement = display.getElement();
displayContainer.innerHTML = "";
displayContainer.appendChild(displayElement); displayContainer.appendChild(displayElement);
// Do nothing when the display element is clicked on. // Do nothing when the display element is clicked on
displayElement.onclick = function(e) { display.getElement().onclick = function(e) {
e.preventDefault(); e.preventDefault();
return false; return false;
}; };
// Connect
client.connect(getConnectString());
}); });
// Clean up when client directive is destroyed // Update actual view scrollLeft when scroll properties change
$scope.$on('$destroy', function destroyClient() { $scope.$watch('client.clientProperties.scrollLeft', function scrollLeftChanged(scrollLeft) {
main.scrollLeft = scrollLeft;
// Update stored thumbnail of current connection $scope.client.clientProperties.scrollLeft = main.scrollLeft;
updateHistoryEntry($scope.id);
}); });
/* // Update actual view scrollTop when scroll properties change
* MOUSE EMULATION $scope.$watch('client.clientProperties.scrollTop', function scrollTopChanged(scrollTop) {
*/ main.scrollTop = scrollTop;
$scope.client.clientProperties.scrollTop = main.scrollTop;
// Watch for changes to mouse emulation mode });
$scope.$watch('clientProperties.emulateAbsoluteMouse', function(emulateAbsoluteMouse) {
// 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; if (!client || !display) return;
@@ -478,19 +308,15 @@ angular.module('client').directive('guacClient', [function guacClient() {
}); });
/*
* DISPLAY SCALE / SIZE
*/
// Adjust scale if modified externally // Adjust scale if modified externally
$scope.$watch('clientProperties.scale', function changeScale(scale) { $scope.$watch('client.clientProperties.scale', function changeScale(scale) {
// Fix scale within limits // Fix scale within limits
scale = Math.max(scale, $scope.clientProperties.minScale); scale = Math.max(scale, $scope.client.clientProperties.minScale);
scale = Math.min(scale, $scope.clientProperties.maxScale); scale = Math.min(scale, $scope.client.clientProperties.maxScale);
// If at minimum zoom level, hide scroll bars // If at minimum zoom level, hide scroll bars
if (scale === $scope.clientProperties.minScale) if (scale === $scope.client.clientProperties.minScale)
main.style.overflow = "hidden"; main.style.overflow = "hidden";
// If not at minimum zoom level, show scroll bars // If not at minimum zoom level, show scroll bars
@@ -501,15 +327,15 @@ angular.module('client').directive('guacClient', [function guacClient() {
if (display) if (display)
display.scale(scale); display.scale(scale);
if (scale !== $scope.clientProperties.scale) if (scale !== $scope.client.clientProperties.scale)
$scope.clientProperties.scale = scale; $scope.client.clientProperties.scale = scale;
}); });
// If autofit is set, the scale should be set to the minimum scale, filling the screen // 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) 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 // 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);
}); });
/* // Watch for changes to mouse emulation mode
* KEYBOARD // Send all received mouse events to the client
*/ mouse.onmousedown =
mouse.onmouseup =
// Listen for broadcasted keydown events and fire the appropriate listeners 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) { $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); client.sendKeyEvent(1, keysym);
event.preventDefault(); 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) { $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); client.sendKeyEvent(0, keysym);
event.preventDefault(); event.preventDefault();
} }
@@ -561,26 +410,6 @@ angular.module('client').directive('guacClient', [function guacClient() {
client.sendKeyEvent(0, keysym); 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.byteLength; i++)
data += String.fromCharCode(bytes[i]);
// Convert to base64
return $window.btoa(data);
}
/** /**
* Ignores the given event. * Ignores the given event.
* *
@@ -590,68 +419,6 @@ angular.module('client').directive('guacClient', [function guacClient() {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
} }
/**
* Uploads the given file to the server.
*
* @param {File} file The file to upload.
*/
function uploadFile(file) {
// Construct reader for file
var reader = new FileReader();
reader.onloadend = function() {
// Open file for writing
var stream = client.createFileStream(file.type, file.name);
var valid = true;
var bytes = new Uint8Array(reader.result);
var offset = 0;
// Add upload notification
$scope.$emit('guacClientFileUploadStart', client, stream.index, file.type, file.name, bytes.length);
// Invalidate stream on all errors
// Continue upload when acknowledged
stream.onack = function(status) {
// Handle errors
if (status.isError()) {
valid = false;
$scope.$emit('guacClientFileUploadError', client, stream.index, file.type, file.name, bytes.length, status.code);
}
// Abort upload if stream is invalid
if (!valid) return false;
// Encode packet as base64
var slice = bytes.subarray(offset, offset+4096);
var base64 = getBase64(slice);
// Write packet
stream.sendBlob(base64);
// Advance to next packet
offset += 4096;
// If at end, stop upload
if (offset >= 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 // Handle and ignore dragenter/dragover
displayContainer.addEventListener("dragenter", ignoreEvent, false); displayContainer.addEventListener("dragenter", ignoreEvent, false);
@@ -664,12 +431,13 @@ angular.module('client').directive('guacClient', [function guacClient() {
e.stopPropagation(); e.stopPropagation();
// Ignore file drops if no attached client // Ignore file drops if no attached client
if (!client) return; if (!$scope.client)
return;
// Upload each file // Upload each file
var files = e.dataTransfer.files; var files = e.dataTransfer.files;
for (var i=0; i<files.length; i++) for (var i=0; i<files.length; i++)
uploadFile(files[i]); ManagedClient.uploadFile($scope.client, files[i]);
}, false); }, false);

View File

@@ -0,0 +1,173 @@
/*
* 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 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
var 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() {
$scope.$apply(updateDisplayScale);
});
}]
};
}]);

View File

@@ -28,11 +28,11 @@ angular.module('client').directive('guacViewport', [function guacViewport() {
return { return {
// Element only // Element only
restrict: 'E', restrict: 'E',
scope: false, scope: {},
transclude: true, transclude: true,
templateUrl: 'app/client/templates/guacViewport.html', templateUrl: 'app/client/templates/guacViewport.html',
controller: ['$window', '$document', '$element', controller: ['$scope', '$window', '$document', '$element',
function guacViewportController($window, $document, $element) { function guacViewportController($scope, $window, $document, $element) {
/** /**
* The fullscreen container element. * The fullscreen container element.
@@ -55,8 +55,12 @@ angular.module('client').directive('guacViewport', [function guacViewport() {
*/ */
var currentAdjustedHeight = null; 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 // Pull scroll properties
var scrollLeft = document.body.scrollLeft; 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);
});
}] }]
}; };
}]); }]);

View File

@@ -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;
}]);

View File

@@ -0,0 +1,126 @@
/*
* 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.<String, 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
* 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;
}]);

View File

@@ -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;
}]);

View File

@@ -37,8 +37,12 @@
transition: left 0.125s, opacity 0.125s; transition: left 0.125s, opacity 0.125s;
} }
#menu h3 {
margin: 1em;
}
#menu .content { #menu .content {
padding: 1em; margin: 1em;
} }
#menu .content > * { #menu .content > * {
@@ -65,6 +69,7 @@
border-radius: 0.25em; border-radius: 0.25em;
white-space: pre; white-space: pre;
display: block; display: block;
font-size: 1em;
} }
#menu #mouse-settings .choice { #menu #mouse-settings .choice {
@@ -94,11 +99,6 @@
margin: 1em auto; margin: 1em auto;
} }
#menu h2 {
padding: 0.25em 0.5em;
font-size: 1em;
}
#menu #keyboard-settings .figure { #menu #keyboard-settings .figure {
float: right; float: right;
max-width: 30%; max-width: 30%;

View File

@@ -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;
}

View File

@@ -28,10 +28,7 @@
<div class="client-body" guac-touch-drag="clientDrag" guac-touch-pinch="clientPinch"> <div class="client-body" guac-touch-drag="clientDrag" guac-touch-pinch="clientPinch">
<!-- Client --> <!-- Client -->
<guac-client <guac-client client="client"/></guac-client>
client-properties="clientProperties"
id="id"
connection-parameters="connectionParameters"/></guac-client>
</div> </div>
@@ -57,13 +54,20 @@
<!-- Menu --> <!-- Menu -->
<div ng-class="{open: menuShown}" id="menu" guac-touch-drag="menuDrag" guac-scroll="menuScrollState"> <div ng-class="{open: menuShown}" id="menu" guac-touch-drag="menuDrag" guac-scroll="menuScrollState">
<h2>{{'CLIENT.SECTION_HEADER_CLIPBOARD' | translate}}</h2>
<div class="logout-panel">
<a class="back button" href="#/">{{'CLIENT.ACTION_NAVIGATE_BACK' | translate}}</a>
<a class="disconnect danger button" ng-click="disconnect()">{{'CLIENT.ACTION_DISCONNECT' | translate}}</a>
</div>
<h2>{{client.name}}</h2>
<h3>{{'CLIENT.SECTION_HEADER_CLIPBOARD' | translate}}</h3>
<div class="content" id="clipboard-settings"> <div class="content" id="clipboard-settings">
<p class="description">{{'CLIENT.HELP_CLIPBOARD' | translate}}</p> <p class="description">{{'CLIENT.HELP_CLIPBOARD' | translate}}</p>
<textarea ng-model="clipboardData" rows="10" cols="40" id="clipboard"></textarea> <textarea ng-model="client.clipboardData" rows="10" cols="40" id="clipboard"></textarea>
</div> </div>
<h2>{{'CLIENT.SECTION_HEADER_INPUT_METHOD' | translate}}</h2> <h3>{{'CLIENT.SECTION_HEADER_INPUT_METHOD' | translate}}</h3>
<div class="content" id="keyboard-settings"> <div class="content" id="keyboard-settings">
<!-- No IME --> <!-- No IME -->
@@ -87,13 +91,13 @@
</div> </div>
<h2>{{'CLIENT.SECTION_HEADER_MOUSE_MODE' | translate}}</h2> <h3>{{'CLIENT.SECTION_HEADER_MOUSE_MODE' | translate}}</h3>
<div class="content" id="mouse-settings"> <div class="content" id="mouse-settings">
<p class="description">{{'CLIENT.HELP_MOUSE_MODE' | translate}}</p> <p class="description">{{'CLIENT.HELP_MOUSE_MODE' | translate}}</p>
<!-- Touchscreen --> <!-- Touchscreen -->
<div class="choice"> <div class="choice">
<input name="mouse-mode" ng-change="closeMenu()" ng-model="clientProperties.emulateAbsoluteMouse" type="radio" ng-value="true" checked="checked" id="absolute"/> <input name="mouse-mode" ng-change="closeMenu()" ng-model="client.clientProperties.emulateAbsoluteMouse" type="radio" ng-value="true" checked="checked" id="absolute"/>
<div class="figure"> <div class="figure">
<label for="absolute"><img src="images/settings/touchscreen.png" alt="{{'CLIENT.NAME_MOUSE_MODE_ABSOLUTE' | translate}}"/></label> <label for="absolute"><img src="images/settings/touchscreen.png" alt="{{'CLIENT.NAME_MOUSE_MODE_ABSOLUTE' | translate}}"/></label>
<p class="caption"><label for="absolute">{{'CLIENT.HELP_MOUSE_MODE_ABSOLUTE' | translate}}</label></p> <p class="caption"><label for="absolute">{{'CLIENT.HELP_MOUSE_MODE_ABSOLUTE' | translate}}</label></p>
@@ -102,7 +106,7 @@
<!-- Touchpad --> <!-- Touchpad -->
<div class="choice"> <div class="choice">
<input name="mouse-mode" ng-change="closeMenu()" ng-model="clientProperties.emulateAbsoluteMouse" type="radio" ng-value="false" id="relative"/> <input name="mouse-mode" ng-change="closeMenu()" ng-model="client.clientProperties.emulateAbsoluteMouse" type="radio" ng-value="false" id="relative"/>
<div class="figure"> <div class="figure">
<label for="relative"><img src="images/settings/touchpad.png" alt="{{'CLIENT.NAME_MOUSE_MODE_RELATIVE' | translate}}"/></label> <label for="relative"><img src="images/settings/touchpad.png" alt="{{'CLIENT.NAME_MOUSE_MODE_RELATIVE' | translate}}"/></label>
<p class="caption"><label for="relative">{{'CLIENT.HELP_MOUSE_MODE_RELATIVE' | translate}}</label></p> <p class="caption"><label for="relative">{{'CLIENT.HELP_MOUSE_MODE_RELATIVE' | translate}}</label></p>
@@ -111,7 +115,7 @@
</div> </div>
<h2>{{'CLIENT.SECTION_HEADER_DISPLAY' | translate}}</h2> <h3>{{'CLIENT.SECTION_HEADER_DISPLAY' | translate}}</h3>
<div class="content"> <div class="content">
<div id="zoom-settings"> <div id="zoom-settings">
<div ng-click="zoomOut()" id="zoom-out"><img src="images/settings/zoom-out.png" alt="-"/></div> <div ng-click="zoomOut()" id="zoom-out"><img src="images/settings/zoom-out.png" alt="-"/></div>

View File

@@ -0,0 +1,34 @@
<div class="thumbnail-main">
<!--
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.
-->
<!-- Resize sensor -->
<iframe class="resize-sensor" src="app/client/templates/blank.html"></iframe>
<!-- Display -->
<div class="display">
</div>
<!-- Dummy background thumbnail -->
<img alt="" ng-src="{{thumbnail}}"/>
</div>

View File

@@ -0,0 +1,432 @@
/*
* 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');
var ManagedDisplay = $injector.get('ManagedDisplay');
var ManagedFileDownload = $injector.get('ManagedFileDownload');
var ManagedFileUpload = $injector.get('ManagedFileUpload');
// Required services
var $window = $injector.get('$window');
var $document = $injector.get('$document');
var authenticationService = $injector.get('authenticationService');
var connectionGroupService = $injector.get('connectionGroupService');
var connectionService = $injector.get('connectionService');
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 display associated with the underlying Guacamole client.
*
* @type ManagedDisplay
*/
this.managedDisplay = template.managedDisplay;
/**
* The name returned associated with the connection or connection
* group in use.
*
* @type String
*/
this.name = template.name;
/**
* The current clipboard contents.
*
* @type String
*/
this.clipboardData = template.clipboardData;
/**
* All downloaded files. As files are downloaded, their progress can be
* observed through the elements of this array. It is intended that
* this array be manipulated externally as needed.
*
* @type ManagedFileDownload[]
*/
this.downloads = template.downloads || [];
/**
* All uploaded files. As files are uploaded, their progress can be
* observed through the elements of this array. It is intended that
* this array be manipulated externally as needed.
*
* @type ManagedFileUpload[]
*/
this.uploads = template.uploads || [];
/**
* 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.$evalAsync(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.$evalAsync(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.CONNECTED);
break;
// Connecting, disconnecting, and disconnected are all
// either ignored or handled by tunnel state
case 1: // Connecting
case 4: // Disconnecting
case 5: // Disconnected
break;
}
});
};
// 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;
});
};
};
// Handle any received files
client.onfile = function clientFileReceived(stream, mimetype, filename) {
$rootScope.$apply(function startDownload() {
managedClient.downloads.push(ManagedFileDownload.getInstance(stream, mimetype, filename));
});
};
// Manage the client display
managedClient.managedDisplay = ManagedDisplay.getInstance(client.getDisplay());
// Connect the Guacamole client
client.connect(getConnectString(id, connectionParameters));
// Determine type of connection
var typePrefix = id.substring(0, 2);
// If using a connection, pull connection name
if (typePrefix === 'c/') {
connectionService.getConnection(id.substring(2))
.success(function connectionRetrieved(connection) {
managedClient.name = connection.name;
});
}
// If using a connection group, pull connection name
else if (typePrefix === 'g/') {
connectionGroupService.getConnectionGroup(id.substring(2))
.success(function connectionGroupRetrieved(group) {
managedClient.name = group.name;
});
}
return managedClient;
};
/**
* Uploads the given file to the server through the given Guacamole client.
* The file transfer can be monitored through the corresponding entry in
* the uploads array of the given managedClient.
*
* @param {ManagedClient} managedClient
* The ManagedClient through which the file is to be uploaded.
*
* @param {File} file
* The file to upload.
*/
ManagedClient.uploadFile = function uploadFile(managedClient, file) {
managedClient.uploads.push(ManagedFileUpload.getInstance(managedClient.client, file));
};
return ManagedClient;
}]);

View File

@@ -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;
}]);

View File

@@ -0,0 +1,177 @@
/*
* 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 setClientSize() {
$rootScope.$apply(function updateClientSize() {
managedDisplay.size = new ManagedDisplay.Dimensions({
width : display.getWidth(),
height : display.getHeight()
});
});
};
// Store changes to display cursor
display.oncursor = function setClientCursor(canvas, x, y) {
$rootScope.$apply(function updateClientCursor() {
managedDisplay.cursor = new ManagedDisplay.Cursor({
canvas : canvas,
x : x,
y : y
});
});
};
return managedDisplay;
};
return ManagedDisplay;
}]);

View File

@@ -0,0 +1,153 @@
/*
* 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 ManagedFileDownload class used by the guacClientManager service.
*/
angular.module('client').factory('ManagedFileDownload', ['$rootScope', '$injector',
function defineManagedFileDownload($rootScope, $injector) {
// Required types
var ManagedFileTransferState = $injector.get('ManagedFileTransferState');
/**
* Object which serves as a surrogate interface, encapsulating a Guacamole
* file download while it is active, allowing it to be detached and
* reattached from different client views.
*
* @constructor
* @param {ManagedFileDownload|Object} [template={}]
* The object whose properties should be copied within the new
* ManagedFileDownload.
*/
var ManagedFileDownload = function ManagedFileDownload(template) {
// Use empty object by default
template = template || {};
/**
* The current state of the file transfer stream.
*
* @type ManagedFileTransferState
*/
this.transferState = template.transferState || new ManagedFileTransferState();
/**
* The mimetype of the file being transferred.
*
* @type String
*/
this.mimetype = template.mimetype;
/**
* The filename of the file being transferred.
*
* @type String
*/
this.filename = template.filename;
/**
* The number of bytes transferred so far.
*
* @type Number
*/
this.progress = template.progress;
/**
* A blob containing the complete downloaded file. This is available
* only after the download has finished.
*
* @type Blob
*/
this.blob = template.blob;
};
/**
* Creates a new ManagedFileDownload which downloads the contents of the
* given stream as a file having the given mimetype and filename.
*
* @param {Guacamole.InputStream} stream
* The stream whose contents should be downloaded as a file.
*
* @param {String} mimetype
* The mimetype of the stream contents.
*
* @param {String} filename
* The filename of the file being received over the steram.
*
* @return {ManagedFileDownload}
* A new ManagedFileDownload object which can be used to track the
* progress of the download.
*/
ManagedFileDownload.getInstance = function getInstance(stream, mimetype, filename) {
// Init new file download object
var managedFileDownload = new ManagedFileDownload({
mimetype : mimetype,
filename : filename,
progress : 0,
transferState : new ManagedFileTransferState({
streamState : ManagedFileTransferState.StreamState.OPEN
})
});
// Begin file download
var blob_reader = new Guacamole.BlobReader(stream, mimetype);
// Update progress as data is received
blob_reader.onprogress = function onprogress() {
// Update progress
$rootScope.$apply(function downloadStreamProgress() {
managedFileDownload.progress = blob_reader.getLength();
});
// Signal server that data was received
stream.sendAck("Received", Guacamole.Status.Code.SUCCESS);
};
// Save blob and close stream when complete
blob_reader.onend = function onend() {
$rootScope.$apply(function downloadStreamEnd() {
// Save blob
managedFileDownload.blob = blob_reader.getBlob();
// Mark stream as closed
ManagedFileTransferState.setStreamState(managedFileDownload.transferState,
ManagedFileTransferState.StreamState.CLOSED);
});
};
// Signal server that data is ready to be received
stream.sendAck("Ready", Guacamole.Status.Code.SUCCESS);
return managedFileDownload;
};
return ManagedFileDownload;
}]);

View File

@@ -0,0 +1,136 @@
/*
* 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 ManagedFileTransferState class used by the guacClientManager
* service.
*/
angular.module('client').factory('ManagedFileTransferState', [function defineManagedFileTransferState() {
/**
* Object which represents the state of a Guacamole stream, including any
* error conditions.
*
* @constructor
* @param {ManagedFileTransferState|Object} [template={}]
* The object whose properties should be copied within the new
* ManagedFileTransferState.
*/
var ManagedFileTransferState = function ManagedFileTransferState(template) {
// Use empty object by default
template = template || {};
/**
* The current stream state. Valid values are described by
* ManagedFileTransferState.StreamState.
*
* @type String
* @default ManagedFileTransferState.StreamState.IDLE
*/
this.streamState = template.streamState || ManagedFileTransferState.StreamState.IDLE;
/**
* The status code of the current error condition, if streamState
* is ERROR. For all other streamState 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 stream state strings. Each state string is associated with a
* specific state of a Guacamole stream.
*/
ManagedFileTransferState.StreamState = {
/**
* The stream has not yet been opened.
*
* @type String
*/
IDLE : "IDLE",
/**
* The stream has been successfully established. Data can be sent or
* received.
*
* @type String
*/
OPEN : "OPEN",
/**
* The stream has terminated successfully. No errors are indicated.
*
* @type String
*/
CLOSED : "CLOSED",
/**
* The stream has terminated due to an error. The associated error code
* is stored in statusCode.
*
* @type String
*/
ERROR : "ERROR"
};
/**
* Sets the current transfer state and, if given, the associated status
* code. If an error is already represented, this function has no effect.
*
* @param {ManagedFileTransferState} transferState
* The ManagedFileTransferState to update.
*
* @param {String} streamState
* The stream state to assign to the given ManagedFileTransferState, as
* listed within ManagedFileTransferState.StreamState.
*
* @param {Number} [statusCode]
* The status code to assign to the given ManagedFileTransferState, if
* any, as listed within Guacamole.Status.Code. If no status code is
* specified, the status code of the ManagedFileTransferState is not
* touched.
*/
ManagedFileTransferState.setStreamState = function setStreamState(transferState, streamState, statusCode) {
// Do not set state after an error is registered
if (transferState.streamState === ManagedFileTransferState.StreamState.ERROR)
return;
// Update stream state
transferState.streamState = streamState;
// Set status code, if given
if (statusCode)
transferState.statusCode = statusCode;
};
return ManagedFileTransferState;
}]);

View File

@@ -0,0 +1,218 @@
/*
* 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 ManagedFileUpload class used by the guacClientManager service.
*/
angular.module('client').factory('ManagedFileUpload', ['$rootScope', '$injector',
function defineManagedFileUpload($rootScope, $injector) {
// Required types
var ManagedFileTransferState = $injector.get('ManagedFileTransferState');
// Required services
var $window = $injector.get('$window');
/**
* The maximum number of bytes to include in each blob for the Guacamole
* file stream. Note that this, along with instruction opcode and protocol-
* related overhead, must not exceed the 8192 byte maximum imposed by the
* Guacamole protocol.
*
* @type Number
*/
var STREAM_BLOB_SIZE = 4096;
/**
* Object which serves as a surrogate interface, encapsulating a Guacamole
* file upload while it is active, allowing it to be detached and
* reattached from different client views.
*
* @constructor
* @param {ManagedFileUpload|Object} [template={}]
* The object whose properties should be copied within the new
* ManagedFileUpload.
*/
var ManagedFileUpload = function ManagedFileUpload(template) {
// Use empty object by default
template = template || {};
/**
* The current state of the file transfer stream.
*
* @type ManagedFileTransferState
*/
this.transferState = template.transferState || new ManagedFileTransferState();
/**
* The mimetype of the file being transferred.
*
* @type String
*/
this.mimetype = template.mimetype;
/**
* The filename of the file being transferred.
*
* @type String
*/
this.filename = template.filename;
/**
* The number of bytes transferred so far.
*
* @type Number
*/
this.progress = template.progress;
/**
* The total number of bytes in the file.
*
* @type Number
*/
this.length = template.length;
};
/**
* 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.
*/
var getBase64 = function getBase64(bytes) {
var data = "";
// Produce binary string from bytes in buffer
for (var i=0; i<bytes.byteLength; i++)
data += String.fromCharCode(bytes[i]);
// Convert to base64
return $window.btoa(data);
};
/**
* Creates a new ManagedFileUpload which uploads the given file to the
* server through the given Guacamole client.
*
* @param {Guacamole.Client} client
* The Guacamole client through which the file is to be uploaded.
*
* @param {File} file
* The file to upload.
*
* @return {ManagedFileUpload}
* A new ManagedFileUpload object which can be used to track the
* progress of the upload.
*/
ManagedFileUpload.getInstance = function getInstance(client, file) {
var managedFileUpload = new ManagedFileUpload();
// Construct reader for file
var reader = new FileReader();
reader.onloadend = function fileContentsLoaded() {
// Open file for writing
var stream = client.createFileStream(file.type, file.name);
var valid = true;
var bytes = new Uint8Array(reader.result);
var offset = 0;
$rootScope.$apply(function uploadStreamOpen() {
// Init managed upload
managedFileUpload.filename = file.name;
managedFileUpload.mimetype = file.type;
managedFileUpload.progress = 0;
managedFileUpload.length = bytes.length;
// Notify that stream is open
ManagedFileTransferState.setStreamState(managedFileUpload.transferState,
ManagedFileTransferState.StreamState.OPEN);
});
// Invalidate stream on all errors
// Continue upload when acknowledged
stream.onack = function ackReceived(status) {
// Handle errors
if (status.isError()) {
valid = false;
$rootScope.$apply(function uploadStreamError() {
ManagedFileTransferState.setStreamState(managedFileUpload.transferState,
ManagedFileTransferState.StreamState.ERROR,
status.code);
});
}
// Abort upload if stream is invalid
if (!valid)
return false;
// Encode packet as base64
var slice = bytes.subarray(offset, offset + STREAM_BLOB_SIZE);
var base64 = getBase64(slice);
// Write packet
stream.sendBlob(base64);
// Advance to next packet
offset += STREAM_BLOB_SIZE;
$rootScope.$apply(function uploadStreamProgress() {
// If at end, stop upload
if (offset >= bytes.length) {
stream.sendEnd();
managedFileUpload.progress = bytes.length;
// Upload complete
ManagedFileTransferState.setStreamState(managedFileUpload.transferState,
ManagedFileTransferState.StreamState.CLOSED);
}
// Otherwise, update progress
else
managedFileUpload.progress = offset;
});
}; // end ack handler
};
reader.readAsArrayBuffer(file);
return managedFileUpload;
};
return ManagedFileUpload;
}]);

View File

@@ -23,7 +23,7 @@
/** /**
* A directive which allows elements to be manually focused / blurred. * 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 { return {
restrict: 'A', restrict: 'A',
@@ -47,7 +47,7 @@ angular.module('element').directive('guacFocus', ['$timeout', '$parse', function
// Set/unset focus depending on value of guacFocus // Set/unset focus depending on value of guacFocus
$scope.$watch(guacFocus, function updateFocus(value) { $scope.$watch(guacFocus, function updateFocus(value) {
$timeout(function updateFocusAsync() { $scope.$evalAsync(function updateFocusAsync() {
if (value) if (value)
element.focus(); element.focus();
else else
@@ -57,14 +57,14 @@ angular.module('element').directive('guacFocus', ['$timeout', '$parse', function
// Set focus flag when focus is received // Set focus flag when focus is received
element.addEventListener('focus', function focusReceived() { element.addEventListener('focus', function focusReceived() {
$scope.$apply(function setGuacFocus() { $scope.$evalAsync(function setGuacFocusAsync() {
guacFocus.assign($scope, true); guacFocus.assign($scope, true);
}); });
}); });
// Unset focus flag when focus is lost // Unset focus flag when focus is lost
element.addEventListener('blur', function focusLost() { element.addEventListener('blur', function focusLost() {
$scope.$apply(function unsetGuacFocus() { $scope.$evalAsync(function unsetGuacFocusAsync() {
guacFocus.assign($scope, false); guacFocus.assign($scope, false);
}); });
}); });

View File

@@ -42,9 +42,50 @@ angular.module('home').directive('guacRecentConnections', [function guacRecentCo
}, },
templateUrl: 'app/home/templates/guacRecentConnections.html', templateUrl: 'app/home/templates/guacRecentConnections.html',
controller: ['$scope', '$injector', 'guacHistory', 'RecentConnection', controller: ['$scope', '$injector', function guacRecentConnectionsController($scope, $injector) {
function guacRecentConnectionsController($scope, $injector, guacHistory, RecentConnection) {
// 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.<String, Connection|ConnectionGroup>
*/
var visibleObjects = {}; var visibleObjects = {};
/** /**
@@ -87,6 +128,8 @@ angular.module('home').directive('guacRecentConnections', [function guacRecentCo
// Update visible objects when root group is set // Update visible objects when root group is set
$scope.$watch("rootGroup", function setRootGroup(rootGroup) { $scope.$watch("rootGroup", function setRootGroup(rootGroup) {
// Clear connection arrays
$scope.activeConnections = [];
$scope.recentConnections = []; $scope.recentConnections = [];
// Produce collection of visible objects // Produce collection of visible objects
@@ -94,11 +137,27 @@ angular.module('home').directive('guacRecentConnections', [function guacRecentCo
if (rootGroup) if (rootGroup)
addVisibleConnectionGroup(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 // Add any recent connections that are visible
guacHistory.recentConnections.forEach(function addRecentConnection(historyEntry) { guacHistory.recentConnections.forEach(function addRecentConnection(historyEntry) {
// Add recent connections for history entries with associated visible objects // 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]; var object = visibleObjects[historyEntry.id];
$scope.recentConnections.push(new RecentConnection(object.name, historyEntry)); $scope.recentConnections.push(new RecentConnection(object.name, historyEntry));

View File

@@ -20,4 +20,4 @@
* THE SOFTWARE. * THE SOFTWARE.
*/ */
angular.module('home', ['history', 'groupList', 'rest']); angular.module('home', ['client', 'history', 'groupList', 'rest']);

View File

@@ -22,11 +22,28 @@
--> -->
<!-- Text displayed if no recent connections exist --> <!-- Text displayed if no recent connections exist -->
<p class="no-recent" ng-hide="recentConnections.length">{{'HOME.INFO_NO_RECENT_CONNECTIONS' | translate}}</p> <p class="no-recent" ng-hide="hasRecentConnections()">{{'HOME.INFO_NO_RECENT_CONNECTIONS' | translate}}</p>
<!-- All active connections -->
<div ng-repeat="activeConnection in activeConnections" class="connection">
<a href="#/client/{{activeConnection.client.id}}">
<!-- Connection thumbnail -->
<div class="thumbnail">
<guac-thumbnail client="activeConnection.client"/></guac-thumbnail>
</div>
<!-- Connection name -->
<div class="caption">
<span class="name">{{activeConnection.name}}</span>
</div>
</a>
</div>
<!-- All recent connections --> <!-- All recent connections -->
<div ng-repeat="recentConnection in recentConnections" class="connection"> <div ng-repeat="recentConnection in recentConnections" class="connection">
<a href="#/client/{{recentConnection.entry.id}}/{{recentConnection.name}}"> <a href="#/client/{{recentConnection.entry.id}}">
<!-- Connection thumbnail --> <!-- Connection thumbnail -->
<div class="thumbnail"> <div class="thumbnail">

View File

@@ -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;
}]);

View File

@@ -26,32 +26,17 @@
angular.module('index').controller('indexController', ['$scope', '$injector', angular.module('index').controller('indexController', ['$scope', '$injector',
function indexController($scope, $injector) { function indexController($scope, $injector) {
// Get class dependencies // Required types
var PermissionSet = $injector.get("PermissionSet"); var PermissionSet = $injector.get("PermissionSet");
// Get services // Required services
var permissionService = $injector.get("permissionService"), var $document = $injector.get("$document");
authenticationService = $injector.get("authenticationService"), var $location = $injector.get("$location");
$q = $injector.get("$q"), var $q = $injector.get("$q");
$document = $injector.get("$document"), var $window = $injector.get("$window");
$window = $injector.get("$window"), var authenticationService = $injector.get("authenticationService");
$location = $injector.get("$location"); 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 * The current status notification, or false if no status is currently
* shown. * shown.

View File

@@ -55,10 +55,12 @@
margin: 0.5em; margin: 0.5em;
} }
.connection .thumbnail img { .connection .thumbnail > * {
border: 1px solid black; border: 1px solid black;
background: black;
box-shadow: 1px 1px 5px black; box-shadow: 1px 1px 5px black;
max-width: 75%; max-width: 75%;
display: inline-block;
} }
div.recent-connections .connection .thumbnail { div.recent-connections .connection .thumbnail {

View File

@@ -17,9 +17,11 @@
"CLIENT" : { "CLIENT" : {
"ACTION_RECONNECT" : "Reconnect", "ACTION_RECONNECT" : "Reconnect",
"ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE",
"ACTION_SAVE_FILE" : "@:APP.ACTION_SAVE", "ACTION_DISCONNECT" : "Disconnect",
"ACTION_NAVIGATE_BACK" : "@:APP.ACTION_NAVIGATE_BACK",
"ACTION_SAVE_FILE" : "@:APP.ACTION_SAVE",
"DIALOG_HEADER_CONNECTING" : "Connecting", "DIALOG_HEADER_CONNECTING" : "Connecting",
"DIALOG_HEADER_CONNECTION_ERROR" : "Connection Error", "DIALOG_HEADER_CONNECTION_ERROR" : "Connection Error",
@@ -82,13 +84,13 @@
"SECTION_HEADER_DISPLAY" : "Display", "SECTION_HEADER_DISPLAY" : "Display",
"SECTION_HEADER_MOUSE_MODE" : "Mouse emulation mode", "SECTION_HEADER_MOUSE_MODE" : "Mouse emulation mode",
"TEXT_ZOOM_AUTO_FIT" : "Automatically fit to browser window", "TEXT_ZOOM_AUTO_FIT" : "Automatically fit to browser window",
"TEXT_CLIENT_STATUS_IDLE" : "Idle.", "TEXT_CLIENT_STATUS_IDLE" : "Idle.",
"TEXT_CLIENT_STATUS_CONNECTING" : "Connecting to Guacamole...", "TEXT_CLIENT_STATUS_CONNECTING" : "Connecting to Guacamole...",
"TEXT_CLIENT_STATUS_WAITING" : "Connected to Guacamole. Waiting for response...", "TEXT_CLIENT_STATUS_DISCONNECTED" : "You have been disconnected.",
"TEXT_TUNNEL_STATUS_CLOSED" : "You have been disconnected. Reload the page to reconnect.", "TEXT_CLIENT_STATUS_WAITING" : "Connected to Guacamole. Waiting for response...",
"TEXT_RECONNECT_COUNTDOWN" : "Reconnecting in {REMAINING} {REMAINING, plural, one{second} other{seconds}}...", "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_FILE_TRANSFER_PROGRESS" : "{PROGRESS} {UNIT, select, b{B} kb{KB} mb{MB} gb{GB} other{}}",
"URL_OSK_LAYOUT" : "layouts/en-us-qwerty.xml" "URL_OSK_LAYOUT" : "layouts/en-us-qwerty.xml"