GUAC-919: Copy Angular changes from old GUAC-546 branch.

This commit is contained in:
James Muehlner
2014-11-03 12:51:17 -08:00
committed by Michael Jumper
parent ac2617b92a
commit 5c43ae4ff9
84 changed files with 16551 additions and 7476 deletions

View File

@@ -0,0 +1,26 @@
/*
* 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.
*/
/**
* The module for code used to connect to a connection or balancing group.
*/
angular.module('client', []);

View File

@@ -0,0 +1,132 @@
/*
* 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.
*/
/*
* In order to open the guacamole menu, we need to hit ctrl-alt-shift. There are
* several possible keysysms for each key.
*/
var SHIFT_KEYS = {0xFFE1 : true, 0xFFE2: true},
ALT_KEYS = {0xFFE9 : true, 0xFFEA : true, 0xFE03: true},
CTRL_KEYS = {0xFFE3 : true, 0xFFE4: true},
MENU_KEYS = angular.extend({}, SHIFT_KEYS, ALT_KEYS, CTRL_KEYS);
/**
* The controller for the page used to connect to a connection or balancing group.
*/
angular.module('home').controller('clientController', ['$scope', '$routeParams', 'localStorageUtility', '$injector',
function clientController($scope, $routeParams, localStorageUtility, $injector) {
// Get DAO for reading connections and groups
var connectionGroupDAO = $injector.get('connectionGroupDAO');
var connectionDAO = $injector.get('connectionDAO');
// Client settings and state
$scope.clientParameters = {scale: 1};
// Hide menu by default
$scope.menuShown = false;
$scope.menuHasBeenShown = false;
/*
* Parse the type, name, and id out of the url paramteres,
* as well as any extra parameters if set.
*/
$scope.type = $routeParams.type;
$scope.id = $routeParams.id;
$scope.connectionParameters = $routeParams.params || '';
// Keep title in sync with connection state
$scope.$watch('connectionName', function updateTitle() {
$scope.page.title = $scope.connectionName;
});
// Pull connection name from server
switch ($scope.type) {
// Connection
case 'c':
connectionDAO.getConnection($scope.id).success(function (connection) {
$scope.connectionName = connection.name;
});
break;
// Connection group
case 'g':
connectionGroupDAO.getConnectionGroup($scope.id).success(function (group) {
$scope.connectionName = group.name;
});
break;
}
var keysCurrentlyPressed = {};
/*
* Check to see if all currently pressed keys are in the set of menu keys.
*/
function checkMenuModeActive() {
for(var keysym in keysCurrentlyPressed) {
if(!MENU_KEYS[keysym]) {
return false;
}
}
return true;
}
$scope.$on('guacKeydown', function keydownListener(event, keysym, keyboard) {
keysCurrentlyPressed[keysym] = true;
});
// Listen for broadcasted keyup events and fire the appropriate listeners
$scope.$on('guacKeyup', function keyupListener(event, keysym, keyboard) {
/*
* If only menu keys are pressed, and we have one keysym from each group,
* and one of the keys is being released, show the menu.
*/
if(checkMenuModeActive()) {
var currentKeysPressedKeys = Object.keys(keysCurrentlyPressed);
// Check that there is a key pressed for each of the required key classes
if(!_.isEmpty(_.pick(SHIFT_KEYS, currentKeysPressedKeys)) &&
!_.isEmpty(_.pick(ALT_KEYS, currentKeysPressedKeys)) &&
!_.isEmpty(_.pick(CTRL_KEYS, currentKeysPressedKeys))
) {
// Toggle the menu
$scope.safeApply(function() {
$scope.menuShown = !$scope.menuShown;
// The menu has been shown at least once before
$scope.menuHasBeenShown = true;
});
// Reset the keys pressed
keysCurrentlyPressed = {};
}
}
delete keysCurrentlyPressed[keysym];
});
}]);

View File

@@ -0,0 +1,670 @@
/*
* 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 the guacamole client.
*/
angular.module('client').directive('guacClient', [function guacClient() {
return {
// Element only
restrict: 'E',
replace: true,
scope: {
// Parameters for controlling client state
clientParameters : '=',
// Parameters for initially connecting
id : '=',
type : '=',
connectionName : '=',
connectionParameters : '='
},
templateUrl: 'app/client/templates/guacClient.html',
controller: ['$scope', '$injector', '$element', function guacClientController($scope, $injector, $element) {
var $window = $injector.get('$window'),
guacAudio = $injector.get('guacAudio'),
guacVideo = $injector.get('guacVideo'),
localStorageUtility = $injector.get('localStorageUtility');
var authToken = localStorageUtility.get('authToken'),
uniqueId = encodeURIComponent($scope.type + '/' + $scope.id);
// Get elements for DOM manipulation
$scope.main = $element[0];
// Settings and constants
$.extend(true, $scope, {
/**
* All error codes for which automatic reconnection is appropriate when a
* tunnel error occurs.
*/
"tunnel_auto_reconnect": {
0x0200: true,
0x0202: true,
0x0203: true,
0x0308: true
},
/**
* All error codes for which automatic reconnection is appropriate when a
* client error occurs.
*/
"client_auto_reconnect": {
0x0200: true,
0x0202: true,
0x0203: true,
0x0301: true,
0x0308: true
},
/* Constants */
"KEYBOARD_AUTO_RESIZE_INTERVAL" : 30, /* milliseconds */
"RECONNECT_PERIOD" : 15, /* seconds */
"TEXT_INPUT_PADDING" : 128, /* characters */
"TEXT_INPUT_PADDING_CODEPOINT" : 0x200B,
/* Settings for zoom */
"min_zoom" : 1,
"max_zoom" : 3,
/* Current connection parameters */
/* The user defined named for this connection */
"connectionName" : "Guacamole",
/* The attached client instance */
"attachedClient" : null,
/* Mouse emulation */
"emulate_absolute" : true,
"touch" : null,
"touch_screen" : null,
"touch_pad" : null,
/* Clipboard */
"remote_clipboard" : "",
"clipboard_integration_enabled" : undefined
});
/**
* Updates the scale of the attached Guacamole.Client based on current window
* size and "auto-fit" setting.
*/
$scope.updateDisplayScale = function() {
var guac = $scope.attachedClient;
if (!guac)
return;
// Determine whether display is currently fit to the screen
var auto_fit = (guac.getDisplay().getScale() === $scope.min_zoom);
// Calculate scale to fit screen
$scope.min_zoom = Math.min(
$scope.main.offsetWidth / Math.max(guac.getDisplay().getWidth(), 1),
$scope.main.offsetHeight / Math.max(guac.getDisplay().getHeight(), 1)
);
// Calculate appropriate maximum zoom level
$scope.max_zoom = Math.max($scope.min_zoom, 3);
// Clamp zoom level, maintain auto-fit
if (guac.getDisplay().getScale() < $scope.min_zoom || auto_fit)
$scope.setScale($scope.min_zoom);
else if (guac.getDisplay().getScale() > $scope.max_zoom)
$scope.setScale($scope.max_zoom);
};
/**
* Attaches a Guacamole.Client to the client UI, such that Guacamole events
* affect the UI, and local events affect the Guacamole.Client. If a client
* is already attached, it is replaced.
*
* @param {Guacamole.Client} guac The Guacamole.Client to attach to the UI.
*/
$scope.attach = function(guac) {
// If a client is already attached, ensure it is disconnected
if ($scope.attachedClient)
$scope.attachedClient.disconnect();
// Store attached client
$scope.attachedClient = guac;
// Get display element
var guac_display = guac.getDisplay().getElement();
/*
* Update the scale of the display when the client display size changes.
*/
guac.getDisplay().onresize = function() {
$scope.updateDisplayScale();
};
/*
* Update UI when the state of the Guacamole.Client changes.
*/
guac.onstatechange = function(clientState) {
switch (clientState) {
// Idle
case 0:
$scope.$emit('guacClientStatusChange', guac, "idle");
break;
// Connecting
case 1:
$scope.$emit('guacClientStatusChange', guac, "connecting");
break;
// Connected + waiting
case 2:
$scope.$emit('guacClientStatusChange', guac, "waiting");
break;
// Connected
case 3:
$scope.$emit('guacClientStatusChange', guac, null);
// Update server clipboard with current data
var clipboard = localStorageUtility.get("clipboard");
if (clipboard)
guac.setClipboard(clipboard);
break;
// Disconnecting / disconnected are handled by tunnel instead
case 4:
case 5:
break;
// Unknown status code
default:
$scope.$emit('guacClientError', guac, "unknown");
}
};
// Listen for clipboard events not sent by the client
$scope.$on('guacClipboard', function onClipboardChange(event, data) {
// Update server clipboard with current data
$scope.guac.setClipboard(data);
});
/*
* Emit a name change event
*/
guac.onname = function(name) {
$scope.connectionDisplayName = name;
$scope.$emit('name', guac, name);
};
/*
* Disconnect and emits an error when the client receives an error
*/
guac.onerror = function(status) {
// Disconnect, if connected
guac.disconnect();
$scope.$emit('guacClientError', guac, status.code, {operations: {reconnect: function reconnect () {
$scope.connect();
}}});
};
// Server copy handler
guac.onclipboard = function(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);
};
// Emit event when done
reader.onend = function clipboard_text_end() {
$scope.$emit('guacClientClipboard', guac, data);
};
};
/*
* Prompt to download file when file received.
*/
guac.onfile = function onfile(stream, mimetype, filename) {
// Begin file download
var guacFileStartEvent = $scope.$emit('guacFileStart', guac, 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('guacFileProgress', guac, stream.index, mimetype, filename);
stream.sendAck("Received", Guacamole.Status.Code.SUCCESS);
};
// When complete, prompt for download
blob_reader.onend = function onend() {
$scope.$emit('guacFileEnd', guac, stream.index, mimetype, filename);
};
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);
};
/*
* Do nothing when the display element is clicked on.
*/
guac_display.onclick = function(e) {
e.preventDefault();
return false;
};
/*
* Handle mouse and touch events relative to the display element.
*/
// Touchscreen
var touch_screen = new Guacamole.Mouse.Touchscreen(guac_display);
$scope.touch_screen = touch_screen;
// Touchpad
var touch_pad = new Guacamole.Mouse.Touchpad(guac_display);
$scope.touch_pad = touch_pad;
// Mouse
var mouse = new Guacamole.Mouse(guac_display);
mouse.onmousedown = mouse.onmouseup = mouse.onmousemove = function(mouseState) {
// Scale event by current scale
var scaledState = new Guacamole.Mouse.State(
mouseState.x / guac.getDisplay().getScale(),
mouseState.y / guac.getDisplay().getScale(),
mouseState.left,
mouseState.middle,
mouseState.right,
mouseState.up,
mouseState.down);
// Send mouse event
guac.sendMouseState(scaledState);
};
// Hide any existing status notifications
$scope.$emit('guacClientStatusChange', guac, null);
var $display = $element.find('.display');
// Remove old client from UI, if any
$display.html("");
// Add client to UI
guac.getDisplay().getElement().className = "software-cursor";
$display.append(guac.getDisplay().getElement());
};
// Watch for changes to mouse emulation mode
$scope.$watch('parameters.emulateAbsolute', function(emulateAbsolute) {
$scope.setMouseEmulationAbsolute(emulateAbsolute);
});
/**
* Sets the mouse emulation mode to absolute or relative.
*
* @param {Boolean} absolute Whether mouse emulation should use absolute
* (touchscreen) mode.
*/
$scope.setMouseEmulationAbsolute = function(absolute) {
function __handle_mouse_state(mouseState) {
// Get client - do nothing if not attached
var guac = $scope.attachedClient;
if (!guac) return;
// Determine mouse position within view
var guac_display = guac.getDisplay().getElement();
var mouse_view_x = mouseState.x + guac_display.offsetLeft - $scope.main.scrollLeft;
var mouse_view_y = mouseState.y + guac_display.offsetTop - $scope.main.scrollTop;
// Determine viewport dimensioins
var view_width = $scope.main.offsetWidth;
var view_height = $scope.main.offsetHeight;
// Determine scroll amounts based on mouse position relative to document
var scroll_amount_x;
if (mouse_view_x > view_width)
scroll_amount_x = mouse_view_x - view_width;
else if (mouse_view_x < 0)
scroll_amount_x = mouse_view_x;
else
scroll_amount_x = 0;
var scroll_amount_y;
if (mouse_view_y > view_height)
scroll_amount_y = mouse_view_y - view_height;
else if (mouse_view_y < 0)
scroll_amount_y = mouse_view_y;
else
scroll_amount_y = 0;
// Scroll (if necessary) to keep mouse on screen.
$scope.main.scrollLeft += scroll_amount_x;
$scope.main.scrollTop += scroll_amount_y;
// Scale event by current scale
var scaledState = new Guacamole.Mouse.State(
mouseState.x / guac.getDisplay().getScale(),
mouseState.y / guac.getDisplay().getScale(),
mouseState.left,
mouseState.middle,
mouseState.right,
mouseState.up,
mouseState.down);
// Send mouse event
guac.sendMouseState(scaledState);
};
var new_mode, old_mode;
$scope.emulate_absolute = absolute;
// Switch to touchscreen if absolute
if (absolute) {
new_mode = $scope.touch_screen;
old_mode = $scope.touch;
}
// Switch to touchpad if not absolute (relative)
else {
new_mode = $scope.touch_pad;
old_mode = $scope.touch;
}
// Perform switch
if (new_mode) {
if (old_mode) {
old_mode.onmousedown = old_mode.onmouseup = old_mode.onmousemove = null;
new_mode.currentState.x = old_mode.currentState.x;
new_mode.currentState.y = old_mode.currentState.y;
}
new_mode.onmousedown = new_mode.onmouseup = new_mode.onmousemove = __handle_mouse_state;
$scope.touch = new_mode;
}
};
/**
* Connects to the current Guacamole connection, attaching a new Guacamole
* client to the user interface. If a Guacamole client is already attached,
* it is replaced.
*/
$scope.connect = function connect() {
// If WebSocket available, try to use it.
if ($window.WebSocket)
$scope.tunnel = new Guacamole.ChainedTunnel(
new Guacamole.WebSocketTunnel("websocket-tunnel"),
new Guacamole.HTTPTunnel("tunnel")
);
// If no WebSocket, then use HTTP.
else
$scope.tunnel = new Guacamole.HTTPTunnel("tunnel");
// Instantiate client
$scope.guac = new Guacamole.Client($scope.tunnel);
// Tie UI to client
$scope.attach($scope.guac);
// 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;
// Scale width/height to be at least 600x600
if (optimal_width < 600 || optimal_height < 600) {
var scale = Math.max(600 / optimal_width, 600 / optimal_height);
optimal_width = optimal_width * scale;
optimal_height = optimal_height * scale;
}
// Get entire query string, and pass to connect().
// Normally, only the "id" parameter is required, but
// all parameters should be preserved and passed on for
// the sake of authentication.
var connectString =
"id=" + uniqueId + ($scope.connectionParameters ? '&' + $scope.connectionParameters : '')
+ "&authToken="+ authToken
+ "&width=" + Math.floor(optimal_width)
+ "&height=" + Math.floor(optimal_height)
+ "&dpi=" + Math.floor(optimal_dpi);
// 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);
});
// Show connection errors from tunnel
$scope.tunnel.onerror = function onerror(status) {
//FIXME: Needs to auto reconnect - should that be here, or in the error handler further up?
$scope.$emit('guacTunnelError', $scope.guac, status.code);
};
// Notify of disconnections (if not already notified of something else)
$scope.tunnel.onstatechange = function onstatechange(state) {
if (state === Guacamole.Tunnel.State.CLOSED) {
$scope.$emit('guacTunnelError', $scope.guac, "disconnected", state);
}
};
// Connect
$scope.guac.connect(connectString);
};
/**
* Sets the current display scale to the given value, where 1 is 100% (1:1
* pixel ratio). Out-of-range values will be clamped in-range.
*
* @param {Number} scale The new scale to apply
*/
$scope.setScale = function setScale(scale) {
scale = Math.max(scale, $scope.min_zoom);
scale = Math.min(scale, $scope.max_zoom);
if ($scope.attachedClient)
$scope.attachedClient.getDisplay().scale(scale);
return scale;
};
// Adjust scale if modified externally
$scope.$watch('clientParameters.scale', function changeScale(scale) {
$scope.setScale(scale);
checkScale();
});
// Verify that the scale is within acceptable bounds, and adjust if needed
function checkScale() {
// If at minimum zoom level, auto fit is ON
if ($scope.scale === $scope.min_zoom) {
$scope.main.style.overflow = "hidden";
$scope.autoFitEnabled = true;
}
// If at minimum zoom level, auto fit is OFF
else {
$scope.main.style.overflow = "auto";
$scope.autoFitEnabled = false;
}
}
var show_keyboard_gesture_possible = true;
// Handle Keyboard events
function __send_key(pressed, keysym) {
$scope.attachedClient.sendKeyEvent(pressed, keysym);
return false;
}
$scope.keydown = function keydown (keysym, keyboard) {
// Only handle key events if client is attached
var guac = $scope.attachedClient;
if (!guac) return true;
// Handle Ctrl-shortcuts specifically
if (keyboard.modifiers.ctrl && !keyboard.modifiers.alt && !keyboard.modifiers.shift) {
// Allow event through if Ctrl+C or Ctrl+X
if (keyboard.pressed[0x63] || keyboard.pressed[0x78]) {
__send_key(1, keysym);
return true;
}
// If Ctrl+V, wait until after paste event (next event loop)
if (keyboard.pressed[0x76]) {
window.setTimeout(function after_paste() {
__send_key(1, keysym);
}, 10);
return true;
}
}
// If key is NOT one of the expected keys, gesture not possible
if (keysym !== 0xFFE3 && keysym !== 0xFFE9 && keysym !== 0xFFE1)
show_keyboard_gesture_possible = false;
// Send key event
return __send_key(1, keysym);
};
$scope.keyup = function keyup(keysym, keyboard) {
// Only handle key events if client is attached
var guac = $scope.attachedClient;
if (!guac) return true;
// If lifting up on shift, toggle menu visibility if rest of gesture
// conditions satisfied
if (show_keyboard_gesture_possible && keysym === 0xFFE1
&& keyboard.pressed[0xFFE3] && keyboard.pressed[0xFFE9]) {
__send_key(0, 0xFFE1);
__send_key(0, 0xFFE9);
__send_key(0, 0xFFE3);
// Emit an event to show the menu
$scope.$emit('guacClientMenu', true);
}
// Detect if no keys are pressed
var reset_gesture = true;
for (var pressed in keyboard.pressed) {
reset_gesture = false;
break;
}
// Reset gesture state if possible
if (reset_gesture)
show_keyboard_gesture_possible = true;
// Send key event
return __send_key(0, keysym);
};
// Listen for broadcasted keydown events and fire the appropriate listeners
$scope.$on('guacKeydown', function keydownListener(event, keysym, keyboard) {
var preventDefault = $scope.keydown(keysym, keyboard);
if(preventDefault) {
event.preventDefault();
}
});
// Listen for broadcasted keyup events and fire the appropriate listeners
$scope.$on('guacKeyup', function keyupListener(event, keysym, keyboard) {
var preventDefault = $scope.keyup(keysym, keyboard);
if(preventDefault) {
event.preventDefault();
}
});
// Connect!
$scope.connect();
}]
};
}]);

View File

@@ -0,0 +1,79 @@
/*
* 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 checking browser audio support.
*/
angular.module('client').factory('guacAudio', [function guacAudio() {
/**
* Object describing the UI's level of audio support.
*/
return new (function() {
var codecs = [
'audio/ogg; codecs="vorbis"',
'audio/mp4; codecs="mp4a.40.5"',
'audio/mpeg; codecs="mp3"',
'audio/webm; codecs="vorbis"',
'audio/wav; codecs=1'
];
var probably_supported = [];
var maybe_supported = [];
/**
* Array of all supported audio mimetypes, ordered by liklihood of
* working.
*/
this.supported = [];
// Build array of supported audio formats
codecs.forEach(function(mimetype) {
var audio = new Audio();
var support_level = audio.canPlayType(mimetype);
// Trim semicolon and trailer
var semicolon = mimetype.indexOf(";");
if (semicolon != -1)
mimetype = mimetype.substring(0, semicolon);
// Partition by probably/maybe
if (support_level == "probably")
probably_supported.push(mimetype);
else if (support_level == "maybe")
maybe_supported.push(mimetype);
});
// Add probably supported types first
Array.prototype.push.apply(
this.supported, probably_supported);
// Prioritize "maybe" supported types second
Array.prototype.push.apply(
this.supported, maybe_supported);
})();
}]);

View File

@@ -0,0 +1,77 @@
/*
* 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 checking browser video support.
*/
angular.module('client').factory('guacVideo', [function guacVideo() {
/**
* Object describing the UI's level of video support.
*/
return new (function() {
var codecs = [
'video/ogg; codecs="theora, vorbis"',
'video/mp4; codecs="avc1.4D401E, mp4a.40.5"',
'video/webm; codecs="vp8.0, vorbis"'
];
var probably_supported = [];
var maybe_supported = [];
/**
* Array of all supported video mimetypes, ordered by liklihood of
* working.
*/
this.supported = [];
// Build array of supported audio formats
codecs.forEach(function(mimetype) {
var video = document.createElement("video");
var support_level = video.canPlayType(mimetype);
// Trim semicolon and trailer
var semicolon = mimetype.indexOf(";");
if (semicolon != -1)
mimetype = mimetype.substring(0, semicolon);
// Partition by probably/maybe
if (support_level == "probably")
probably_supported.push(mimetype);
else if (support_level == "maybe")
maybe_supported.push(mimetype);
});
// Add probably supported types first
Array.prototype.push.apply(
this.supported, probably_supported);
// Prioritize "maybe" supported types second
Array.prototype.push.apply(
this.supported, maybe_supported);
})();
}]);

View File

@@ -0,0 +1,482 @@
/*
* 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.
*/
body {
background: black;
font-family: FreeSans, Helvetica, Arial, sans-serif;
padding: 0;
margin: 0;
}
img {
border: none;
}
.software-cursor {
cursor: url('images/mouse/blank.gif'),url('images/mouse/blank.cur'),default;
overflow: hidden;
cursor: none;
}
.guac-error .software-cursor {
cursor: default;
}
* {
-webkit-tap-highlight-color: rgba(0,0,0,0);
}
.event-target {
position: fixed;
opacity: 0;
}
/* Dialogs */
div.dialogOuter {
display: table;
height: 100%;
width: 100%;
position: fixed;
left: 0;
top: 0;
background: rgba(0, 0, 0, 0.75);
}
div.dialogMiddle {
width: 100%;
text-align: center;
display: table-cell;
vertical-align: middle;
}
button {
border-style: solid;
border-width: 1px;
padding: 0.25em;
padding-right: 1em;
padding-left: 1em;
}
button:active {
padding-top: 0.35em;
padding-left: 1.1em;
padding-bottom: 0.15em;
padding-right: 0.9em;
}
button#reconnect {
display: none;
}
.guac-error button#reconnect {
display: inline;
background: #200;
border-color: #822;
color: #944;
}
.guac-error button#reconnect:hover {
background: #822;
border-color: #B33;
color: black;
}
div.dialog p {
margin: 0;
}
div.displayOuter {
height: 100%;
width: 100%;
position: absolute;
left: 0;
top: 0;
display: table;
}
div.displayMiddle {
width: 100%;
display: table-cell;
vertical-align: middle;
text-align: center;
}
div.display * {
position: relative;
}
div.display > * {
margin-left: auto;
margin-right: auto;
}
div.magnifier-background {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
z-index: 1;
overflow: hidden;
}
div.magnifier {
position: absolute;
left: 0;
top: 0;
box-shadow: 2px 2px 10px rgba(0, 0, 0, 0.75);
width: 50%;
height: 50%;
overflow: hidden;
}
.pan-overlay,
.type-overlay {
position: fixed;
left: 0;
top: 0;
width: 100%;
height: 100%;
z-index: 1;
}
.pan-overlay .indicator {
position: fixed;
background-size: 32px 32px;
-moz-background-size: 32px 32px;
-webkit-background-size: 32px 32px;
-khtml-background-size: 32px 32px;
background-position: center;
background-repeat: no-repeat;
opacity: 0.8;
}
.pan-overlay .indicator.up {
top: 0;
left: 0;
right: 0;
height: 32px;
background-image: url('images/arrows/arrows-u.png');
}
.pan-overlay .indicator.down {
bottom: 0;
left: 0;
right: 0;
height: 32px;
background-image: url('images/arrows/arrows-d.png');
}
.pan-overlay .indicator.left {
top: 0;
bottom: 0;
left: 0;
width: 32px;
background-image: url('images/arrows/arrows-l.png');
}
.pan-overlay .indicator.right {
top: 0;
bottom: 0;
right: 0;
width: 32px;
background-image: url('images/arrows/arrows-r.png');
}
/* Viewport Clone */
div#viewportClone {
display: table;
height: 100%;
width: 100%;
position: fixed;
left: 0;
top: 0;
visibility: hidden;
}
@keyframes show-dialog {
0% {transform: scale(0.75); }
100% {transform: scale(1); }
}
@-webkit-keyframes show-dialog {
0% {-webkit-transform: scale(0.75); }
100% {-webkit-transform: scale(1); }
}
.dialog {
animation-name: show-dialog;
animation-timing-function: linear;
animation-duration: 0.125s;
-webkit-animation-name: show-dialog;
-webkit-animation-timing-function: linear;
-webkit-animation-duration: 0.125s;
max-width: 75%;
max-height: none;
width: 4in;
-moz-border-radius: 0.2em;
-webkit-border-radius: 0.2em;
-khtml-border-radius: 0.2em;
border-radius: 0.2em;
padding: 0.5em;
text-align: left;
}
.guac-error .dialog {
background: #FDD;
border: 1px solid #964040;
}
.dialog .title {
font-size: 1.1em;
font-weight: bold;
border-bottom: 1px solid black;
margin-bottom: 0.5em;
}
.dialog .status {
padding: 0.5em;
font-size: 0.8em;
}
p.hint {
border: 0.25em solid rgba(255, 255, 255, 0.25);
background: black;
opacity: 0.75;
color: white;
max-width: 10em;
padding: 1em;
margin: 1em;
position: absolute;
left: 0;
top: 0;
box-shadow: 0.25em 0.25em 0.25em rgba(0, 0, 0, 0.75);
}
#notificationArea {
position: fixed;
right: 0.5em;
bottom: 0.5em;
max-width: 25%;
min-width: 10em;
}
.notification {
font-size: 0.7em;
text-align: center;
border: 1px solid rgba(0, 0, 0, 0.75);
-moz-border-radius: 0.2em;
-webkit-border-radius: 0.2em;
-khtml-border-radius: 0.2em;
border-radius: 0.2em;
background: white;
color: black;
padding: 0.5em;
margin: 1em;
overflow: hidden;
box-shadow: 0.1em 0.1em 0.2em rgba(0, 0, 0, 0.25);
}
.notification div {
display: inline-block;
text-align: left;
}
.notification .title-bar {
display: block;
white-space: nowrap;
font-weight: bold;
border-bottom: 1px solid black;
padding-bottom: 0.5em;
margin-bottom: 0.5em;
}
.notification .title-bar * {
vertical-align: middle;
}
.notification .close {
background: url('images/action-icons/guac-close.png');
background-size: 10px 10px;
-moz-background-size: 10px 10px;
-webkit-background-size: 10px 10px;
-khtml-background-size: 10px 10px;
width: 10px;
height: 10px;
float: right;
cursor: pointer;
}
@keyframes progress {
from {background-position: 0px 0px;}
to {background-position: 64px 0px;}
}
@-webkit-keyframes progress {
from {background-position: 0px 0px;}
to {background-position: 64px 0px;}
}
.notification .caption,
.download.notification .caption {
width: 100%;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.upload.notification .status,
.download.notification .status {
color: red;
font-size: 1em;
padding: 1em;
}
.download.notification .progress,
.upload.notification .progress,
.download.notification .download {
margin-top: 1em;
margin-left: 0.75em;
padding: 0.25em;
min-width: 5em;
border: 1px solid gray;
-moz-border-radius: 0.2em;
-webkit-border-radius: 0.2em;
-khtml-border-radius: 0.2em;
border-radius: 0.2em;
text-align: center;
float: right;
position: relative;
}
.upload.notification .progress {
float: none;
width: 80%;
margin-left: auto;
margin-right: auto;
}
.download.notification .progress div,
.upload.notification .progress div {
position: relative;
}
.download.notification .progress .bar,
.upload.notification .progress .bar {
background: #A3D655;
position: absolute;
top: 0;
left: 0;
height: 100%;
width: 0;
box-shadow: inset 1px 1px 0 rgba(255, 255, 255, 0.5),
inset -1px -1px 0 rgba( 0, 0, 0, 0.1),
1px 1px 0 gray;
}
.upload.notification .progress,
.download.notification .progress {
background: #C2C2C2 url('images/progress.png');
background-size: 16px 16px;
-moz-background-size: 16px 16px;
-webkit-background-size: 16px 16px;
-khtml-background-size: 16px 16px;
animation-name: progress;
animation-duration: 2s;
animation-timing-function: linear;
animation-iteration-count: infinite;
-webkit-animation-name: progress;
-webkit-animation-duration: 2s;
-webkit-animation-timing-function: linear;
-webkit-animation-iteration-count: infinite;
}
.download.notification .download {
background: rgb(16, 87, 153);
cursor: pointer;
}
#preload {
visibility: hidden;
position: absolute;
left: 0;
right: 0;
width: 0;
height: 0;
overflow: hidden;
}

View File

@@ -0,0 +1,120 @@
<!--
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.
-->
<div id="clientContainer">
<!-- Client -->
<guac-client
client-parameters="clientParameters"
type="type"
id="id"
connection-name="connectionName"
connection-parameters="connectionParameters"
></guac-client>
<!-- Text input target -->
<div id="text-input"><div id="text-input-field"><div id="sent-history"></div><textarea rows="1" id="target"></textarea></div><div id="text-input-buttons"><button class="key" data-keysym="0xFFE3" data-sticky="true">{{'client.ctrl' | translate}}</button><button class="key" data-keysym="0xFFE9" data-sticky="true">{{'client.alt' | translate}}</button><button class="key" data-keysym="0xFF1B">{{'client.esc' | translate}}</button><button class="key" data-keysym="0xFF09">{{'client.tab' | translate}}</button></div></div>
<!-- Dimensional clone of viewport -->
<div id="viewportClone"/>
<!-- Notification area -->
<div id="notificationArea"/>
<!-- Menu -->
<div ng-class="{closed: menuHasBeenShown && !menuShown, open: menuShown}" id="menu">
<h2 id="menu-title">Guacamole ${project.version}</h2>
<h3>{{'client.clipboard' | translate}}</h3>
<div class="content" id="clipboard-settings">
<p class="description"></p>
<textarea rows="10" cols="40" id="clipboard">{{'client.copiedText' | translate}}</textarea>
</div>
<h3></h3>
<div class="content" id="keyboard-settings">
<!-- No IME -->
<div class="choice">
<label><input name="input-method" type="radio" value="ime-none" checked="checked" id="ime-none"/> {{'client.none' | translate}}</label>
<p class="caption"><label for="ime-none">{{'client.noneDesc' | translate}}</label></p>
</div>
<!-- Text input -->
<div class="choice">
<div class="figure"><label for="ime-text"><img src="images/settings/tablet-keys.png" alt=""/></label></div>
<label><input name="input-method" type="radio" value="ime-text" id="ime-text"/> {{'client.textInput' | translate}}</label>
<p class="caption"><label for="ime-text">{{'client.textInputDesc' | translate}} </label></p>
</div>
<!-- Guac OSK -->
<div class="choice">
<label><input name="input-method" type="radio" value="ime-osk" id="ime-osk"/> {{'client.osk' | translate}}</label>
<p class="caption"><label for="ime-osk">{{'client.oskDesc' | translate}}</label></p>
</div>
</div>
<h3>{{'client.mouseMode' | translate}}</h3>
<div class="content" id="mouse-settings">
<p class="description">{{'client.mouseModeDesc' | translate}}</p>
<!-- Touchscreen -->
<div class="choice">
<input name="mouse-mode" type="radio" value="absolute" checked="checked" id="absolute"/>
<div class="figure">
<label for="absolute"><img src="images/settings/touchscreen.png" alt="{{'client.touchscreen' | translate}}"/></label>
<p class="caption"><label for="absolute">{{'client.touchscreenDesc' | translate}}</label></p>
</div>
</div>
<!-- Touchpad -->
<div class="choice">
<input name="mouse-mode" type="radio" value="relative" id="relative"/>
<div class="figure">
<label for="relative"><img src="images/settings/touchpad.png" alt="{{'client.touchpad' | translate}}"/></label>
<p class="caption"><label for="relative">{{'client.touchpadDesc' | translate}}</label></p>
</div>
</div>
</div>
<h3>{{'client.display' | translate}}</h3>
<div class="content">
<div id="zoom-settings">
<div ng-click="zoomOut()" id="zoom-out"><img src="images/settings/zoom-out.png" alt="-"/></div>
<div id="zoom-state">{{formattedScale()}}</div>
<div ng-click="zoomIn()" id="zoom-in"><img src="images/settings/zoom-in.png" alt="+"/></div>
</div>
<div><label><input ng-model="autoFitEnabled" ng-change="autoFit(autoFitEnabled)" type="checkbox" id="auto-fit" checked="checked"/> {{'client.autoFit' | translate}}</label></div>
</div>
</div>
<!-- Images which should be preloaded -->
<div id="preload">
<img src="images/action-icons/guac-close.png"/>
<img src="images/progress.png"/>
</div>
<ng-include src="app/client/template/clientError.html"/>
</div>

View File

@@ -0,0 +1,35 @@
<!--
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.
-->
<div ng-show="errorPresent" class="dialogOuter guac-error">
<div class="dialogMiddle">
<div class="dialog">
<p class="title">{{'client.error.connectionErrorTitle' | translate}}</p>
<p class="status">{{errorStatus}}</p>
<div class="reconnect">
<button ng-click="reconnect()">{{'client.error.reconnect' | translate}}</button>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,32 @@
<div class="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.
-->
<!-- Display -->
<div class="displayOuter">
<div class="displayMiddle">
<div class="display">
</div>
</div>
</div>
</div>