diff --git a/src/sunstone/public/app/tabs/vms-tab/dialogs/guac.js b/src/sunstone/public/app/tabs/vms-tab/dialogs/guac.js
index 04d2ca96cb..15ef897b2a 100644
--- a/src/sunstone/public/app/tabs/vms-tab/dialogs/guac.js
+++ b/src/sunstone/public/app/tabs/vms-tab/dialogs/guac.js
@@ -15,30 +15,20 @@
/* -------------------------------------------------------------------------- */
define(function(require) {
- /*
- DEPENDENCIES
- */
var BaseDialog = require('utils/dialogs/dialog');
- var TemplateHTML = require('hbs!./guac/html');
+ var GuacController = require('utils/guacamole/controller');
+ var Locale = require("utils/locale");
+ var Notifier = require("utils/notifier");
var Sunstone = require('sunstone');
- var GClient = require('utils/gclient');
- var Files = require('utils/files');
- /*
- CONSTANTS
- */
+ var TemplateHTML = require('hbs!./guac/html');
var DIALOG_ID = require('./guac/dialogId');
var TAB_ID = require('../tabId')
- /*
- CONSTRUCTOR
- */
-
function Dialog() {
this.dialogId = DIALOG_ID;
- this.gClient = new GClient();
BaseDialog.call(this);
};
@@ -54,47 +44,47 @@ define(function(require) {
return Dialog;
- /*
- FUNCTION DEFINITIONS
- */
+ /* FUNCTION DEFINITIONS */
function _html() {
- return TemplateHTML({
- 'dialogId': this.dialogId
- });
+ return TemplateHTML({ 'dialogId': this.dialogId });
}
function _setup(context) {
- var that = this;
-
$("#open_in_a_new_window_gclient", context).on("click", function() {
var dialog = Sunstone.getDialog(DIALOG_ID);
dialog.hide();
});
- $("#takeScreenshot_gclient", context).on("click", function() {
- var canvas = that.gClient.snapshot();
- Files.downloadImage('screenshot', canvas)
- });
-
return false;
}
function _onShow() {
- this.gClient.connect(this.element);
- this.gClient.mouse(true);
- this.gClient.keyboard(true);
+ var token = this.element.token;
+ var info = this.element.info;
+
+ if (!token) {
+ Notifier.notifyError(
+ Locale.tr("The OpenNebula service for remote console is not running, please contact your administrator.")
+ );
+
+ return null;
+ }
+
+ this.controller = new GuacController();
+ this.controller.setInformation(info);
+ this.controller.setConnection(token);
+
return false;
}
function _onClose() {
- this.gClient.disconnect();
- this.gClient.mouse(false);
- this.gClient.keyboard(false);
+ this.controller.disconnect();
+
return false;
}
function _setElement(element) {
- this.element = element
+ this.element = element;
}
});
diff --git a/src/sunstone/public/app/tabs/vms-tab/dialogs/guac/html.hbs b/src/sunstone/public/app/tabs/vms-tab/dialogs/guac/html.hbs
index ea496b491e..31dfd272d6 100644
--- a/src/sunstone/public/app/tabs/vms-tab/dialogs/guac/html.hbs
+++ b/src/sunstone/public/app/tabs/vms-tab/dialogs/guac/html.hbs
@@ -17,24 +17,47 @@
diff --git a/src/sunstone/public/app/utils/gclient.js b/src/sunstone/public/app/utils/guacamole/client2.js
similarity index 50%
rename from src/sunstone/public/app/utils/gclient.js
rename to src/sunstone/public/app/utils/guacamole/client2.js
index a4b0e1801a..b8b96aa23e 100644
--- a/src/sunstone/public/app/utils/gclient.js
+++ b/src/sunstone/public/app/utils/guacamole/client2.js
@@ -24,15 +24,36 @@ define(function(require) {
var Locale = require("utils/locale");
var UtilsConnection = require("utils/info-connection/utils");
+ /*
+ CONSTANTS
+ */
+
+ var KEYS = {
+ CTRL: 0xFF03,
+ ALT: 0xFFE9,
+ DELETE: 0xFFFF,
+ };
+
+ var client = null;
+
+ /**
+ * Whether the local, hardware mouse cursor is in use.
+ * @type Boolean
+ */
+ var localCursor = false;
+
/**
* CONSTRUCTOR
*/
+
function GClient() {
this._display = null;
this._client = null;
- this._mouse = null;
this._keyboard = null;
this._clientErrorText = Locale.tr("The OpenNebula service for remote console is not running, please contact your administrator.")
+
+ this._mouse = null;
+ this._touchScreen = null;
return this;
};
@@ -41,8 +62,11 @@ define(function(require) {
"connect": connect,
"mouse": mouse,
"keyboard": keyboard,
- "snapshot": getCanvas,
"disconnect": disconnect,
+
+ /* ----- FUNCTIONS INTERACTION ----- */
+ snapshot: getCanvas,
+ sendCtrlAltDelete: sendCtrlAltDelete
};
return GClient;
@@ -62,7 +86,9 @@ define(function(require) {
Notifier.notifyError(this._clientErrorText)
return null;
}
- else setLoading(true);
+ else {
+ setLoading(true);
+ }
var endpoint = Config.publicFireedgeEndpoint.split("//");
var fireedge_protocol = endpoint[0];
@@ -74,30 +100,40 @@ define(function(require) {
var wsprotocol = (fireedge_protocol == 'https:') ? 'wss:' : 'ws:';
var tunnel = new Guacamole.WebSocketTunnel(wsprotocol + '//' + host + ':' + port + '/fireedge/guacamole')
- var guac = this._client = new Guacamole.Client(tunnel);
+ console.log('-<<<<<< pepe')
+ client = new Guacamole.Client(tunnel);
+ var display = client.getDisplay();
+
+ config.guac = client;
+
+ //var guacLayout = new Guacamole.OnScreenKeyboard.Layout()
+
+ //var keyboard = new Guacamole.OnScreenKeyboard(guacLayout);
+ //$("#guacamole-keyboard").html(keyboard.getElement());
+ //if (keyboard) keyboard.resize($("#guacamole-keyboard").offsetWidth);
var info_decode = UtilsConnection.decodeInfoConnection(response.info);
- UtilsConnection.printInfoConnection($('.guac_info'), info_decode)
+ UtilsConnection.printInfoConnection($('.guac_info'), info_decode);
// Client display
- this._display = $("#guacamole-display");
- this._display.html(this._client.getDisplay().getElement());
+ this._displayContainer = document.getElementById('guacamole-display');
+ this._displayContainer.appendChild(display.getElement());
// client error handler
- guac.onerror = function() {
- Notifier.notifyError(that._clientErrorText)
+ client.onerror = function() {
+ Notifier.notifyError(clientErrorText)
};
- // websoket error handler
+ // websocket error handler
tunnel.onerror = function() {
disconnect();
- Notifier.notifyError(that._clientErrorText)
+ Notifier.notifyError(clientErrorText)
setStatus("Guacamole tunnel ERROR");
setLoading(false);
};
- guac.onstatechange = function(state) {
+ client.onstatechange = function(state) {
switch (state) {
case 0:
setStatus('Client IDLE');
@@ -112,11 +148,14 @@ define(function(require) {
setLoading(true);
break;
case 3:
+ $("#sendCtrlAltDelButton_gclient").on("click", function() {
+ sendCtrlAltDelete();
+ });
setStatus('Client CONNECTED');
setLoading(false);
setTimeout(function() {
rescale(that);
- guac.getDisplay().showCursor(false);
+ display.showCursor(false);
}, 100);
break;
case 4:
@@ -137,13 +176,15 @@ define(function(require) {
// Connect
var params = [
'token=' + response.token,
- 'width=' + this._display.width()
+ 'width=' + this._displayContainer.offsetWidth
];
try {
- guac.connect(params.join('&'));
+ var con = client.connect(params.join('&'));
+ console.log('... connect', con)
} catch (error) {
console.log(error)
+ client = null;
}
// Disconnect on close
@@ -163,60 +204,133 @@ define(function(require) {
*/
function mouse(enable = true) {
var that = this;
- if (!this._client) return;
+ var display = client.getDisplay();
+
+ if (!client || !display) return;
if (enable) {
- this._mouse = new Guacamole.Mouse(this._client.getDisplay().getElement());
-
- // apply sendState function
- this._mouse.onmousedown =
- this._mouse.onmouseup = function(mouseState) {
- that._client.sendMouseState(mouseState);
+ var displayElement = display.getElement();
+
+ var mouse = this._mouse = new Guacamole.Mouse(displayElement);
+
+ // Ensure focus is regained via mouseup/mousedown before forwarding event
+ mouse.onmouseup =
+ mouse.onmousedown = function(mouseState) {
+ document.body.focus();
+ handleMouseState(mouseState, that);
};
- this._mouse.onmousemove = function(mouseState) {
- mouseState.y = mouseState.y / that._client.getDisplay().getScale();
- mouseState.x = mouseState.x / that._client.getDisplay().getScale();
- that._client.sendMouseState(mouseState);
+ // Forward mousemove events
+ mouse.onmousemove = function(mouseState) {
+ mouseState.y = mouseState.y / display.getScale();
+ mouseState.x = mouseState.x / display.getScale();
+ handleMouseState(mouseState, that);
+ }
+
+ // Hide software cursor when mouse leaves display
+ mouse.onmouseout = function() {
+ if (!display) return;
+ display.showCursor(false);
};
+
+ display.oncursor = function setClientCursor(canvas, x, y) {
+ localCursor = mouse.setCursor(canvas, x, y);
+ }
+ } else {
+ this._mouse.onmouseup =
+ this._mouse.onmousedown =
+ this._mouse.onmousemove =
+ this._mouse.onmouseout =
+ this._mouse = null;
}
}
+ /**
+ * Handles a mouse event originating from the user's actual mouse.
+ * This differs from handleEmulatedMouseState() in that the
+ * software mouse cursor must be shown only if the user's browser
+ * does not support explicitly setting the hardware mouse cursor.
+ *
+ * @param {Guacamole.Mouse.State} mouseState
+ * The current state of the user's hardware mouse.
+ */
+ function handleMouseState(mouseState, thisContext) {
+ var display = client.getDisplay();
+
+ // Do not attempt to handle mouse state changes if the client
+ // or display are not yet available
+ if (!client || !display)
+ return;
+
+ // Send mouse state, show cursor if necessary
+ display.showCursor(!localCursor);
+ client.sendMouseState(mouseState, true);
+ };
+
+
/*
GuacamoleWrapper.keyboard
- handles keyboard interaction
*/
function keyboard(enable = true) {
var that = this;
- if (!this._client) return;
+ var displayContainer = this._displayContainer;
+
+ if (!client || !displayContainer) return;
if (enable) {
- this._keyboard = new Guacamole.Keyboard(document);
- this._keyboard.onkeydown = function(keysym) {
- that._client.sendKeyEvent(1, keysym);
+ var keyboard = this._keyboard = new Guacamole.Keyboard(document);
+ var sink = new Guacamole.InputSink();
+
+ document.body.appendChild(sink.getElement());
+ keyboard.listenTo(sink.getElement());
+
+ keyboard.onkeydown = function(keysym) {
+ console.log(keysym)
+ client.sendKeyEvent(1, keysym);
}
- this._keyboard.onkeyup = function(keysym) {
- that._client.sendKeyEvent(0, keysym);
+
+ keyboard.onkeyup = function(keysym) {
+ client.sendKeyEvent(0, keysym);
}
} else {
- this._keyboard.onkeydown = null;
- this._keyboard.onkeyup = null;
+ this._keyboard.onkeydown =
+ this._keyboard.onkeyup =
this._keyboard = null;
}
}
+ /*
+ GuacamoleWrapper.sendCtrlAltDelete
+ */
+ function sendCtrlAltDelete() {
+ if (!client) return;
+
+ client.sendKeyEvent(1, KEYS.CTRL);
+ client.sendKeyEvent(1, KEYS.ALT);
+ client.sendKeyEvent(1, KEYS.DELETE);
+ client.sendKeyEvent(0, KEYS.DELETE);
+ client.sendKeyEvent(0, KEYS.ALT);
+ client.sendKeyEvent(0, KEYS.CTRL);
+ }
+
/*
GuacamoleWrapper.getCanvas
- shortcut for returning default guac layer (active tunnel viewport)
*/
function getCanvas() {
- return (this._client) ? this._client.getDisplay().getDefaultLayer().getCanvas() : false;
+ return (client) ? client.getDisplay().getDefaultLayer().getCanvas() : false;
}
function disconnect() {
- if (this._client) {
- this._client.disconnect();
+ if (client) {
+ client.disconnect();
+ client = null;
+
+ console.log('->>> disconnect')
+
this._scale = 1;
+ this._displayContainer.innerHTML = ""
$(window).off('resize');
setStatus()
}
@@ -227,29 +341,30 @@ define(function(require) {
* size and "auto-fit" setting.
*/
function rescale(thisContext) {
- var gclientDisplay = thisContext._client.getDisplay();
+ var displayContainer = thisContext._displayContainer;
+ var display = client.getDisplay();
- //Get screen resolution.
- var origHeigth = Math.max(gclientDisplay.getHeight(), 1);
- var origWidth = Math.max(gclientDisplay.getWidth(), 1);
+ // Get screen resolution.
+ var origHeight = Math.max(display.getHeight(), 1);
+ var origWidth = Math.max(display.getWidth(), 1);
- var htmlWidth = thisContext._display.width();
- var htmlHeigth = thisContext._display.height();
+ var htmlWidth = displayContainer.offsetWidth;
+ var htmlHeight = displayContainer.offsetHeight;
- var xscale = htmlWidth / origWidth;
- var yscale = htmlHeigth / origHeigth;
+ var xScale = htmlWidth / origWidth;
+ var yScale = htmlHeight / origHeight;
- // This is done to handle both X and Y axis slacing
- var scale = Math.min(yscale, xscale);
+ // This is done to handle both X and Y axis
+ var scale = Math.min(yScale, xScale);
// Limit to 1
scale = Math.min(scale, 1);
if (scale !== 0) {
- gclientDisplay.scale(scale);
+ display.scale(scale);
- // Set minimum height container display
- thisContext._display.css('min-height', gclientDisplay.getHeight());
+ // Set minimum height to display container
+ displayContainer.style['min-height'] = display.getHeight() + "px";
}
}
});
diff --git a/src/sunstone/public/app/utils/guacamole/controller.js b/src/sunstone/public/app/utils/guacamole/controller.js
new file mode 100644
index 0000000000..0cc69bfd72
--- /dev/null
+++ b/src/sunstone/public/app/utils/guacamole/controller.js
@@ -0,0 +1,191 @@
+/* -------------------------------------------------------------------------- */
+/* Copyright 2002-2021, OpenNebula Project, OpenNebula Systems */
+/* */
+/* Licensed under the Apache License, Version 2.0 (the "License"); you may */
+/* not use this file except in compliance with the License. You may obtain */
+/* a copy of the License at */
+/* */
+/* http://www.apache.org/licenses/LICENSE-2.0 */
+/* */
+/* Unless required by applicable law or agreed to in writing, software */
+/* distributed under the License is distributed on an "AS IS" BASIS, */
+/* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. */
+/* See the License for the specific language governing permissions and */
+/* limitations under the License. */
+/* -------------------------------------------------------------------------- */
+
+define(function(require) {
+
+ require("guacamole-common-js")
+ var ManagedClient = require("utils/guacamole/types/client");
+ var ManagedClientState = require("utils/guacamole/types/client-state");
+ var Utils = require("utils/guacamole/utils");
+ var UtilsConnection = require("utils/info-connection/utils");
+
+ var GuacButtons = require("utils/guacamole/directives/guacButtons");
+ var GuacClipboard = require("utils/guacamole/directives/guacClipboard");
+ var GuacKeyboard = require("utils/guacamole/directives/guacKeyboard");
+ var GuacMouse = require("utils/guacamole/directives/guacMouse");
+ var GuacOsk = require("utils/guacamole/directives/guacOsk");
+
+ function GuacController() {
+ var $guac = {};
+ var $scope = {};
+ var $elements = {
+ main: document.getElementById('guacamole-main'),
+ displayContainer: document.getElementById('guacamole-display'),
+ osk: document.getElementById('osk'),
+ closeOskButton: document.getElementById('osk-close'),
+
+ /* Buttons */
+ sendCtrlAltDelButton: document.getElementById('sendCtrlAltDelButton_gclient'),
+ mouseButton: document.getElementById('mouseButton_gclient'),
+ screenshotButton: document.getElementById('takeScreenshot_gclient'),
+ oskButton: document.getElementById('oskButton_gclient'),
+ };
+
+ var throttleResizeFunction = Utils.throttle(containerResized, 250);
+ window.addEventListener('resize', throttleResizeFunction);
+
+ this.disconnect = function() {
+ if ($guac.client) $guac.client.disconnect();
+ if ($guac.keyboard) GuacKeyboard.destroy();
+ if ($guac.mouse) GuacMouse.destroy();
+ if ($guac.osk) GuacOsk.destroy();
+
+ GuacButtons.destroy();
+ GuacClipboard.destroy();
+ window.removeEventListener('resize', throttleResizeFunction);
+ $('#guacamole-state').text('');
+
+ $guac = {};
+ $scope = {};
+ }
+
+ this.setConnection = function(token) {
+ var managedClient = ManagedClient.getInstance(token, undefined, $elements.displayContainer)
+
+ new GuacKeyboard($guac, $scope, $elements);
+ new GuacMouse($guac, $scope, $elements);
+ new GuacOsk($guac, $scope, $elements);
+ new GuacButtons($guac, $scope, $elements);
+ new GuacClipboard($guac, $scope, $elements);
+
+ // Remove any existing display
+ $elements.displayContainer.innerHTML = "";
+
+ // Only proceed if a client is given
+ if (!managedClient) return;
+ $scope.client = managedClient;
+
+ // Get Guacamole client instance
+ $guac.client = managedClient.client;
+
+ // Attach possibly new display
+ $guac.display = $guac.client.getDisplay();
+ $guac.display.scale($scope.client.clientProperties.scale);
+
+ // Add display element
+ $scope.displayElement = $guac.display.getElement();
+ $elements.displayContainer.appendChild($scope.displayElement);
+
+ // Do nothing when the display element is clicked on
+ $guac.display.getElement().onclick = function(event) {
+ event.preventDefault();
+ return false;
+ };
+
+ // Size of newly-attached client
+ containerResized();
+
+ Utils.observe($scope.client.managedDisplay, 'size', (function() {
+ updateDisplayScale();
+ }).bind(this));
+
+ Utils.observe($scope.client.managedDisplay, 'cursor', (function(cursor) {
+ if (cursor && $scope.localCursor) {
+ $scope.localCursor = $guac.mouse.setCursor(cursor.canvas, cursor.x, cursor.y);
+ }
+ }).bind(this));
+
+ Utils.observe($scope, 'disableCursor', (function(disabled) {
+ $elements.mouseButton.disabled = !!disabled;
+ }).bind(this));
+
+ Utils.observe($scope.client.clientState, 'connectionState', (function(connectionState) {
+ var isLoading = connectionState === ManagedClientState.ConnectionState.WAITING;
+
+ $('#guacamole-loading')[isLoading ? 'fadeIn' : 'fadeOut']('fast');
+ $('#guacamole-state').text(connectionState).animate();
+ }).bind(this));
+
+ Utils.observe($scope.client.clientProperties, 'scale', (function(scale) {
+ scale = Math.max(scale, $scope.client.clientProperties.minScale);
+ scale = Math.min(scale, $scope.client.clientProperties.maxScale);
+
+ // Apply scale if client attached
+ if ($guac.display && scale !== 0) {
+ $guac.display.scale(scale);
+ $elements.displayContainer.style['min-height'] = $guac.display.getHeight() + "px";
+ }
+
+ if (scale !== $scope.client.clientProperties.scale) {
+ $scope.client.clientProperties.scale = scale;
+ }
+ }).bind(this));
+ };
+
+ this.setInformation = function(information) {
+ var info_decode = UtilsConnection.decodeInfoConnection(information);
+ UtilsConnection.printInfoConnection($('.guacamole_info'), info_decode);
+ }
+
+ function containerResized() {
+ // Send new display size, if changed
+ if ($guac.client && $guac.display) {
+ var pixelDensity = window.devicePixelRatio || 1;
+ var width = $elements.main.offsetWidth * pixelDensity;
+ var height = $elements.main.offsetHeight * pixelDensity;
+
+ if ($guac.display.getWidth() !== width || $guac.display.getHeight() !== height) {
+ $guac.client.sendSize(width, height);
+ }
+
+ if ($guac.osk) {
+ var MAX_OSK_WIDTH = 1000;
+
+ $guac.osk.resize(Math.min(MAX_OSK_WIDTH, width));
+ }
+ }
+
+ updateDisplayScale();
+ };
+
+ function updateDisplayScale() {
+ if (!$guac.display) return;
+
+ // Calculate scale to fit screen
+ $scope.client.clientProperties.minScale = Math.min(
+ $elements.main.offsetWidth / Math.max($guac.display.getWidth(), 1),
+ $elements.main.offsetHeight / Math.max($guac.display.getHeight(), 1)
+ );
+
+ // Calculate appropriate maximum zoom level
+ $scope.client.clientProperties.maxScale = Math.max($scope.client.clientProperties.minScale, 3);
+
+ // Clamp zoom level, maintain auto-fit
+ if (
+ $guac.display.getScale() < $scope.client.clientProperties.minScale ||
+ $scope.client.clientProperties.autoFit
+ ) {
+ $scope.client.clientProperties.scale = $scope.client.clientProperties.minScale;
+ }
+ else if ($guac.display.getScale() > $scope.client.clientProperties.maxScale) {
+ $scope.client.clientProperties.scale = $scope.client.clientProperties.maxScale;
+ }
+ };
+ }
+
+ return GuacController;
+
+});
\ No newline at end of file
diff --git a/src/sunstone/public/app/utils/guacamole/directives/guacButtons.js b/src/sunstone/public/app/utils/guacamole/directives/guacButtons.js
new file mode 100644
index 0000000000..38ce4e5016
--- /dev/null
+++ b/src/sunstone/public/app/utils/guacamole/directives/guacButtons.js
@@ -0,0 +1,73 @@
+/* -------------------------------------------------------------------------- */
+/* Copyright 2002-2021, OpenNebula Project, OpenNebula Systems */
+/* */
+/* Licensed under the Apache License, Version 2.0 (the "License"); you may */
+/* not use this file except in compliance with the License. You may obtain */
+/* a copy of the License at */
+/* */
+/* http://www.apache.org/licenses/LICENSE-2.0 */
+/* */
+/* Unless required by applicable law or agreed to in writing, software */
+/* distributed under the License is distributed on an "AS IS" BASIS, */
+/* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. */
+/* See the License for the specific language governing permissions and */
+/* limitations under the License. */
+/* -------------------------------------------------------------------------- */
+
+define(function(require) {
+
+ var Files = require('utils/files');
+
+ function GuacButtons($guac, $scope, $elements) {
+ $elements.screenshotButton.onclick = function() {
+ if (!$guac.client) return;
+
+ var canvas = $guac.client.getDisplay().getDefaultLayer().getCanvas();
+ Files.downloadImage('screenshot', canvas)
+ };
+
+ $elements.sendCtrlAltDelButton.onclick = function() {
+ if (!$guac.client || !$guac.osk) return;
+
+ var ctrlKey = $guac.osk.keys['LCtrl'][0].keysym;
+ var altKey = $guac.osk.keys['LAlt'][0].keysym;
+ var delKey = $guac.osk.keys['Del'][0].keysym;
+
+ $guac.client.sendKeyEvent(1, ctrlKey);
+ $guac.client.sendKeyEvent(1, altKey);
+ $guac.client.sendKeyEvent(1, delKey);
+ $guac.client.sendKeyEvent(0, delKey);
+ $guac.client.sendKeyEvent(0, altKey);
+ $guac.client.sendKeyEvent(0, ctrlKey);
+ };
+
+ $elements.oskButton.onclick =
+ $elements.closeOskButton.onclick = function() {
+ if (!$guac.client) return;
+
+ $('#osk-container').fadeToggle('fast');
+ };
+
+ $elements.mouseButton.onclick = function() {
+ // toggle disabled
+ this.classList.toggle('disabled');
+
+ $scope.localCursor = $elements.mouseButton.classList.contains('disabled');
+ };
+
+ GuacButtons.destroy = function() {
+ // reset default state
+ $('#osk-container').hide();
+ $elements.mouseButton.classList.remove('disabled');
+
+ $elements.sendCtrlAltDelButton =
+ $elements.screenshotButton.onclick =
+ $elements.mouseButton.onclick =
+ $elements.oskButton.onclick =
+ $elements.closeOskButton.onclick = null;
+ };
+ }
+
+ return GuacButtons;
+
+});
\ No newline at end of file
diff --git a/src/sunstone/public/app/utils/guacamole/directives/guacClipboard.js b/src/sunstone/public/app/utils/guacamole/directives/guacClipboard.js
new file mode 100644
index 0000000000..81ae82d632
--- /dev/null
+++ b/src/sunstone/public/app/utils/guacamole/directives/guacClipboard.js
@@ -0,0 +1,72 @@
+/* -------------------------------------------------------------------------- */
+/* Copyright 2002-2021, OpenNebula Project, OpenNebula Systems */
+/* */
+/* Licensed under the Apache License, Version 2.0 (the "License"); you may */
+/* not use this file except in compliance with the License. You may obtain */
+/* a copy of the License at */
+/* */
+/* http://www.apache.org/licenses/LICENSE-2.0 */
+/* */
+/* Unless required by applicable law or agreed to in writing, software */
+/* distributed under the License is distributed on an "AS IS" BASIS, */
+/* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. */
+/* See the License for the specific language governing permissions and */
+/* limitations under the License. */
+/* -------------------------------------------------------------------------- */
+
+define(function(require) {
+
+ var ClipboardData = require("utils/guacamole/types/clipboard-data");
+ var ManagedClient = require("utils/guacamole/types/client");
+
+ function GuacClipboard($guac, $scope, $elements) {
+ window.addEventListener('load', checkClipboard, true);
+ window.addEventListener('copy', checkClipboard);
+ window.addEventListener('cut', checkClipboard);
+ window.addEventListener('focus', focusGained, true);
+
+ function focusGained(event) {
+ // Only recheck clipboard if it's the window itself that gained focus
+ if (event.target === window) checkClipboard();
+ }
+
+ function checkClipboard() {
+ $.when(getLocalClipboard()).done(function(data) {
+ if ($guac.client) {
+ ManagedClient.setClipboard($scope.client, data);
+ $scope.client.clipboardData = data;
+ }
+ });
+ };
+
+ function getLocalClipboard() {
+ var deferred = $.Deferred();
+
+ try {
+ // Attempt to read the clipboard using the Asynchronous Clipboard
+ // API, if it's available
+ if (navigator.clipboard && navigator.clipboard.readText) {
+ navigator.clipboard.readText().then(function(text) {
+ deferred.resolve(
+ new ClipboardData({ type: 'text/plain', data: text })
+ );
+ }, deferred.reject);
+
+ return deferred.promise();
+ }
+ }
+ // Ignore any hard failures to use Asynchronous Clipboard API
+ catch (ignore) { console.error(ignore) }
+ }
+
+ GuacClipboard.destroy = function() {
+ window.removeEventListener('load', checkClipboard, true);
+ window.removeEventListener('copy', checkClipboard);
+ window.removeEventListener('cut', checkClipboard);
+ window.removeEventListener('focus', focusGained, true);
+ };
+ }
+
+ return GuacClipboard;
+
+});
\ No newline at end of file
diff --git a/src/sunstone/public/app/utils/guacamole/directives/guacKeyboard.js b/src/sunstone/public/app/utils/guacamole/directives/guacKeyboard.js
new file mode 100644
index 0000000000..3c37dff64a
--- /dev/null
+++ b/src/sunstone/public/app/utils/guacamole/directives/guacKeyboard.js
@@ -0,0 +1,53 @@
+/* -------------------------------------------------------------------------- */
+/* Copyright 2002-2021, OpenNebula Project, OpenNebula Systems */
+/* */
+/* Licensed under the Apache License, Version 2.0 (the "License"); you may */
+/* not use this file except in compliance with the License. You may obtain */
+/* a copy of the License at */
+/* */
+/* http://www.apache.org/licenses/LICENSE-2.0 */
+/* */
+/* Unless required by applicable law or agreed to in writing, software */
+/* distributed under the License is distributed on an "AS IS" BASIS, */
+/* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. */
+/* See the License for the specific language governing permissions and */
+/* limitations under the License. */
+/* -------------------------------------------------------------------------- */
+
+define(function(require) {
+
+ require("guacamole-common-js")
+
+ function GuacKeyboard($guac, $scope, $elements) {
+ // Add default destination for input events
+ var sink = $guac.sink = new Guacamole.InputSink();
+ $elements.displayContainer.appendChild(sink.getElement());
+
+ // Create event listeners at the global level
+ var keyboard = $guac.keyboard = new Guacamole.Keyboard(document);
+ keyboard.listenTo(sink.getElement());
+
+ keyboard.onkeydown = function(keysym) {
+ $guac.client.sendKeyEvent(1, keysym);
+ };
+
+ keyboard.onkeyup = function(keysym) {
+ $guac.client.sendKeyEvent(0, keysym);
+ };
+
+ // Release all keys when window loses focus
+ window.addEventListener('blur', keyboard.reset);
+
+ GuacKeyboard.destroy = function() {
+ window.removeEventListener('blur', keyboard.reset);
+
+ $guac.sink =
+ $guac.keyboard =
+ $guac.keyboard.onkeydown =
+ $guac.keyboard.onkeyup = null;
+ }
+ }
+
+ return GuacKeyboard;
+
+});
\ No newline at end of file
diff --git a/src/sunstone/public/app/utils/guacamole/directives/guacMouse.js b/src/sunstone/public/app/utils/guacamole/directives/guacMouse.js
new file mode 100644
index 0000000000..ee3c24fad2
--- /dev/null
+++ b/src/sunstone/public/app/utils/guacamole/directives/guacMouse.js
@@ -0,0 +1,65 @@
+/* -------------------------------------------------------------------------- */
+/* Copyright 2002-2021, OpenNebula Project, OpenNebula Systems */
+/* */
+/* Licensed under the Apache License, Version 2.0 (the "License"); you may */
+/* not use this file except in compliance with the License. You may obtain */
+/* a copy of the License at */
+/* */
+/* http://www.apache.org/licenses/LICENSE-2.0 */
+/* */
+/* Unless required by applicable law or agreed to in writing, software */
+/* distributed under the License is distributed on an "AS IS" BASIS, */
+/* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. */
+/* See the License for the specific language governing permissions and */
+/* limitations under the License. */
+/* -------------------------------------------------------------------------- */
+
+define(function(require) {
+
+ require("guacamole-common-js")
+
+ function GuacMouse($guac, $scope, $elements) {
+ var mouse = $guac.mouse = new Guacamole.Mouse($elements.displayContainer);
+
+ // Ensure focus is regained via mousedown before forwarding event
+ mouse.onmouseup =
+ mouse.onmousedown = function(mouseState) {
+ $elements.displayContainer.focus();
+ handleMouseState(mouseState);
+ };
+
+ // Forward mousemove events untouched
+ mouse.onmousemove = function(mouseState) {
+ mouseState.y = mouseState.y / $guac.display.getScale();
+ mouseState.x = mouseState.x / $guac.display.getScale();
+ handleMouseState(mouseState);
+ }
+
+ // Hide software cursor when mouse leaves display
+ mouse.onmouseout = function() {
+ if (!$guac.display) return;
+
+ $guac.display.showCursor(false);
+ };
+
+ function handleMouseState(mouseState) {
+ // Do not attempt to handle mouse state changes if the client
+ // or display are not yet available
+ if (!$guac.client || !$guac.display) return;
+
+ // Send mouse state, show cursor if necessary
+ $guac.display.showCursor(!$scope.localCursor);
+ $guac.client.sendMouseState(mouseState);
+ };
+
+ GuacMouse.destroy = function() {
+ $guac.mouse =
+ $guac.mouse.onmouseup =
+ $guac.mouse.onmousedown =
+ $guac.mouse.onmouseout = null;
+ };
+ }
+
+ return GuacMouse;
+
+});
\ No newline at end of file
diff --git a/src/sunstone/public/app/utils/guacamole/directives/guacOsk.js b/src/sunstone/public/app/utils/guacamole/directives/guacOsk.js
new file mode 100644
index 0000000000..17d9814dab
--- /dev/null
+++ b/src/sunstone/public/app/utils/guacamole/directives/guacOsk.js
@@ -0,0 +1,76 @@
+/* -------------------------------------------------------------------------- */
+/* Copyright 2002-2021, OpenNebula Project, OpenNebula Systems */
+/* */
+/* Licensed under the Apache License, Version 2.0 (the "License"); you may */
+/* not use this file except in compliance with the License. You may obtain */
+/* a copy of the License at */
+/* */
+/* http://www.apache.org/licenses/LICENSE-2.0 */
+/* */
+/* Unless required by applicable law or agreed to in writing, software */
+/* distributed under the License is distributed on an "AS IS" BASIS, */
+/* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. */
+/* See the License for the specific language governing permissions and */
+/* limitations under the License. */
+/* -------------------------------------------------------------------------- */
+
+define(function(require) {
+
+ require("guacamole-common-js")
+ var enUsQwerty = require('utils/guacamole/layouts/en-us-qwerty');
+ var esEsQwerty = require('utils/guacamole/layouts/es-es-qwerty');
+
+ var DEFAULT_LAYOUT = enUsQwerty.language;
+
+ function GuacOsk($guac, $scope, $elements) {
+ loadLayouts();
+ changeLayout(DEFAULT_LAYOUT);
+
+ $('#osk-container').draggable();
+
+ function loadLayouts() {
+ $('#osk-qwerty').empty();
+
+ var enUsLayout = new Option(enUsQwerty.language, enUsQwerty.language);
+ $('#osk-qwerty').append(enUsLayout);
+
+ var esEsLayout = new Option(esEsQwerty.language, esEsQwerty.language);
+ $('#osk-qwerty').append(esEsLayout);
+
+ $('#osk-qwerty').off().on('change', function() {
+ changeLayout(this.value);
+ })
+ };
+
+ function changeLayout(newLayout) {
+ var layout = newLayout === enUsQwerty.language ? enUsQwerty : esEsQwerty;
+
+ var osk = $guac.osk = new Guacamole.OnScreenKeyboard(layout);
+
+ $elements.osk.innerHTML = "";
+ $elements.osk.appendChild(osk.getElement());
+
+ osk.onkeydown = function(keysym) {
+ $guac.client.sendKeyEvent(1, keysym);
+ };
+
+ osk.onkeyup = function(keysym) {
+ $guac.client.sendKeyEvent(0, keysym);
+ };
+
+ var pixelDensity = window.devicePixelRatio || 1;
+ var width = $elements.main.offsetWidth * pixelDensity;
+ var MAX_OSK_WIDTH = 1000;
+ $guac.osk.resize(Math.min(MAX_OSK_WIDTH, width));
+ }
+
+ GuacOsk.destroy = function() {
+ $guac.osk =
+ $guac.osk.onkeydown =
+ $guac.osk.onkeyup = null;
+ };
+ }
+
+ return GuacOsk;
+
+});
\ No newline at end of file
diff --git a/src/sunstone/public/app/utils/guacamole/layouts/en-us-qwerty.js b/src/sunstone/public/app/utils/guacamole/layouts/en-us-qwerty.js
new file mode 100644
index 0000000000..a714fcfcc8
--- /dev/null
+++ b/src/sunstone/public/app/utils/guacamole/layouts/en-us-qwerty.js
@@ -0,0 +1,1164 @@
+define(function() {
+ return {
+ "language": "en_US",
+ "type": "qwerty",
+ "width": 22,
+ "keys": {
+ "0": [
+ {
+ "title": "0",
+ "requires": []
+ },
+ {
+ "title": ")",
+ "requires": [
+ "shift"
+ ]
+ }
+ ],
+ "1": [
+ {
+ "title": "1",
+ "requires": []
+ },
+ {
+ "title": "!",
+ "requires": [
+ "shift"
+ ]
+ }
+ ],
+ "2": [
+ {
+ "title": "2",
+ "requires": []
+ },
+ {
+ "title": "@",
+ "requires": [
+ "shift"
+ ]
+ }
+ ],
+ "3": [
+ {
+ "title": "3",
+ "requires": []
+ },
+ {
+ "title": "#",
+ "requires": [
+ "shift"
+ ]
+ }
+ ],
+ "4": [
+ {
+ "title": "4",
+ "requires": []
+ },
+ {
+ "title": "$",
+ "requires": [
+ "shift"
+ ]
+ }
+ ],
+ "5": [
+ {
+ "title": "5",
+ "requires": []
+ },
+ {
+ "title": "%",
+ "requires": [
+ "shift"
+ ]
+ }
+ ],
+ "6": [
+ {
+ "title": "6",
+ "requires": []
+ },
+ {
+ "title": "^",
+ "requires": [
+ "shift"
+ ]
+ }
+ ],
+ "7": [
+ {
+ "title": "7",
+ "requires": []
+ },
+ {
+ "title": "&",
+ "requires": [
+ "shift"
+ ]
+ }
+ ],
+ "8": [
+ {
+ "title": "8",
+ "requires": []
+ },
+ {
+ "title": "*",
+ "requires": [
+ "shift"
+ ]
+ }
+ ],
+ "9": [
+ {
+ "title": "9",
+ "requires": []
+ },
+ {
+ "title": "(",
+ "requires": [
+ "shift"
+ ]
+ }
+ ],
+ "Back": 65288,
+ "Tab": 65289,
+ "Enter": 65293,
+ "Esc": 65307,
+ "Home": 65360,
+ "PgUp": 65365,
+ "PgDn": 65366,
+ "End": 65367,
+ "Ins": 65379,
+ "F1": 65470,
+ "F2": 65471,
+ "F3": 65472,
+ "F4": 65473,
+ "F5": 65474,
+ "F6": 65475,
+ "F7": 65476,
+ "F8": 65477,
+ "F9": 65478,
+ "F10": 65479,
+ "F11": 65480,
+ "F12": 65481,
+ "Del": 65535,
+ "Space": " ",
+ "Left": [
+ {
+ "title": "←",
+ "keysym": 65361
+ }
+ ],
+ "Up": [
+ {
+ "title": "↑",
+ "keysym": 65362
+ }
+ ],
+ "Right": [
+ {
+ "title": "→",
+ "keysym": 65363
+ }
+ ],
+ "Down": [
+ {
+ "title": "↓",
+ "keysym": 65364
+ }
+ ],
+ "Menu": [
+ {
+ "title": "Menu",
+ "keysym": 65383
+ }
+ ],
+ "LShift": [
+ {
+ "title": "Shift",
+ "modifier": "shift",
+ "keysym": 65505
+ }
+ ],
+ "RShift": [
+ {
+ "title": "Shift",
+ "modifier": "shift",
+ "keysym": 65506
+ }
+ ],
+ "LCtrl": [
+ {
+ "title": "Ctrl",
+ "modifier": "control",
+ "keysym": 65507
+ }
+ ],
+ "RCtrl": [
+ {
+ "title": "Ctrl",
+ "modifier": "control",
+ "keysym": 65508
+ }
+ ],
+ "Caps": [
+ {
+ "title": "Caps",
+ "modifier": "caps",
+ "keysym": 65509
+ }
+ ],
+ "LAlt": [
+ {
+ "title": "Alt",
+ "modifier": "alt",
+ "keysym": 65513
+ }
+ ],
+ "RAlt": [
+ {
+ "title": "Alt",
+ "modifier": "alt",
+ "keysym": 65514
+ }
+ ],
+ "Super": [
+ {
+ "title": "Super",
+ "modifier": "super",
+ "keysym": 65515
+ }
+ ],
+ "`": [
+ {
+ "title": "`",
+ "requires": []
+ },
+ {
+ "title": "~",
+ "requires": [
+ "shift"
+ ]
+ }
+ ],
+ "-": [
+ {
+ "title": "-",
+ "requires": []
+ },
+ {
+ "title": "_",
+ "requires": [
+ "shift"
+ ]
+ }
+ ],
+ "=": [
+ {
+ "title": "=",
+ "requires": []
+ },
+ {
+ "title": "+",
+ "requires": [
+ "shift"
+ ]
+ }
+ ],
+ ",": [
+ {
+ "title": ",",
+ "requires": []
+ },
+ {
+ "title": "<",
+ "requires": [
+ "shift"
+ ]
+ }
+ ],
+ ".": [
+ {
+ "title": ".",
+ "requires": []
+ },
+ {
+ "title": ">",
+ "requires": [
+ "shift"
+ ]
+ }
+ ],
+ "/": [
+ {
+ "title": "/",
+ "requires": []
+ },
+ {
+ "title": "?",
+ "requires": [
+ "shift"
+ ]
+ }
+ ],
+ "[": [
+ {
+ "title": "[",
+ "requires": []
+ },
+ {
+ "title": "{",
+ "requires": [
+ "shift"
+ ]
+ }
+ ],
+ "]": [
+ {
+ "title": "]",
+ "requires": []
+ },
+ {
+ "title": "}",
+ "requires": [
+ "shift"
+ ]
+ }
+ ],
+ "\\": [
+ {
+ "title": "\\",
+ "requires": []
+ },
+ {
+ "title": "|",
+ "requires": [
+ "shift"
+ ]
+ }
+ ],
+ ";": [
+ {
+ "title": ";",
+ "requires": []
+ },
+ {
+ "title": ":",
+ "requires": [
+ "shift"
+ ]
+ }
+ ],
+ "'": [
+ {
+ "title": "'",
+ "requires": []
+ },
+ {
+ "title": "\"",
+ "requires": [
+ "shift"
+ ]
+ }
+ ],
+ "q": [
+ {
+ "title": "q",
+ "requires": []
+ },
+ {
+ "title": "Q",
+ "requires": [
+ "caps"
+ ]
+ },
+ {
+ "title": "Q",
+ "requires": [
+ "shift"
+ ]
+ },
+ {
+ "title": "q",
+ "requires": [
+ "caps",
+ "shift"
+ ]
+ }
+ ],
+ "w": [
+ {
+ "title": "w",
+ "requires": []
+ },
+ {
+ "title": "W",
+ "requires": [
+ "caps"
+ ]
+ },
+ {
+ "title": "W",
+ "requires": [
+ "shift"
+ ]
+ },
+ {
+ "title": "w",
+ "requires": [
+ "caps",
+ "shift"
+ ]
+ }
+ ],
+ "e": [
+ {
+ "title": "e",
+ "requires": []
+ },
+ {
+ "title": "E",
+ "requires": [
+ "caps"
+ ]
+ },
+ {
+ "title": "E",
+ "requires": [
+ "shift"
+ ]
+ },
+ {
+ "title": "e",
+ "requires": [
+ "caps",
+ "shift"
+ ]
+ }
+ ],
+ "r": [
+ {
+ "title": "r",
+ "requires": []
+ },
+ {
+ "title": "R",
+ "requires": [
+ "caps"
+ ]
+ },
+ {
+ "title": "R",
+ "requires": [
+ "shift"
+ ]
+ },
+ {
+ "title": "r",
+ "requires": [
+ "caps",
+ "shift"
+ ]
+ }
+ ],
+ "t": [
+ {
+ "title": "t",
+ "requires": []
+ },
+ {
+ "title": "T",
+ "requires": [
+ "caps"
+ ]
+ },
+ {
+ "title": "T",
+ "requires": [
+ "shift"
+ ]
+ },
+ {
+ "title": "t",
+ "requires": [
+ "caps",
+ "shift"
+ ]
+ }
+ ],
+ "y": [
+ {
+ "title": "y",
+ "requires": []
+ },
+ {
+ "title": "Y",
+ "requires": [
+ "caps"
+ ]
+ },
+ {
+ "title": "Y",
+ "requires": [
+ "shift"
+ ]
+ },
+ {
+ "title": "y",
+ "requires": [
+ "caps",
+ "shift"
+ ]
+ }
+ ],
+ "u": [
+ {
+ "title": "u",
+ "requires": []
+ },
+ {
+ "title": "U",
+ "requires": [
+ "caps"
+ ]
+ },
+ {
+ "title": "U",
+ "requires": [
+ "shift"
+ ]
+ },
+ {
+ "title": "u",
+ "requires": [
+ "caps",
+ "shift"
+ ]
+ }
+ ],
+ "i": [
+ {
+ "title": "i",
+ "requires": []
+ },
+ {
+ "title": "I",
+ "requires": [
+ "caps"
+ ]
+ },
+ {
+ "title": "I",
+ "requires": [
+ "shift"
+ ]
+ },
+ {
+ "title": "i",
+ "requires": [
+ "caps",
+ "shift"
+ ]
+ }
+ ],
+ "o": [
+ {
+ "title": "o",
+ "requires": []
+ },
+ {
+ "title": "O",
+ "requires": [
+ "caps"
+ ]
+ },
+ {
+ "title": "O",
+ "requires": [
+ "shift"
+ ]
+ },
+ {
+ "title": "o",
+ "requires": [
+ "caps",
+ "shift"
+ ]
+ }
+ ],
+ "p": [
+ {
+ "title": "p",
+ "requires": []
+ },
+ {
+ "title": "P",
+ "requires": [
+ "caps"
+ ]
+ },
+ {
+ "title": "P",
+ "requires": [
+ "shift"
+ ]
+ },
+ {
+ "title": "p",
+ "requires": [
+ "caps",
+ "shift"
+ ]
+ }
+ ],
+ "a": [
+ {
+ "title": "a",
+ "requires": []
+ },
+ {
+ "title": "A",
+ "requires": [
+ "caps"
+ ]
+ },
+ {
+ "title": "A",
+ "requires": [
+ "shift"
+ ]
+ },
+ {
+ "title": "a",
+ "requires": [
+ "caps",
+ "shift"
+ ]
+ }
+ ],
+ "s": [
+ {
+ "title": "s",
+ "requires": []
+ },
+ {
+ "title": "S",
+ "requires": [
+ "caps"
+ ]
+ },
+ {
+ "title": "S",
+ "requires": [
+ "shift"
+ ]
+ },
+ {
+ "title": "s",
+ "requires": [
+ "caps",
+ "shift"
+ ]
+ }
+ ],
+ "d": [
+ {
+ "title": "d",
+ "requires": []
+ },
+ {
+ "title": "D",
+ "requires": [
+ "caps"
+ ]
+ },
+ {
+ "title": "D",
+ "requires": [
+ "shift"
+ ]
+ },
+ {
+ "title": "d",
+ "requires": [
+ "caps",
+ "shift"
+ ]
+ }
+ ],
+ "f": [
+ {
+ "title": "f",
+ "requires": []
+ },
+ {
+ "title": "F",
+ "requires": [
+ "caps"
+ ]
+ },
+ {
+ "title": "F",
+ "requires": [
+ "shift"
+ ]
+ },
+ {
+ "title": "f",
+ "requires": [
+ "caps",
+ "shift"
+ ]
+ }
+ ],
+ "g": [
+ {
+ "title": "g",
+ "requires": []
+ },
+ {
+ "title": "G",
+ "requires": [
+ "caps"
+ ]
+ },
+ {
+ "title": "G",
+ "requires": [
+ "shift"
+ ]
+ },
+ {
+ "title": "g",
+ "requires": [
+ "caps",
+ "shift"
+ ]
+ }
+ ],
+ "h": [
+ {
+ "title": "h",
+ "requires": []
+ },
+ {
+ "title": "H",
+ "requires": [
+ "caps"
+ ]
+ },
+ {
+ "title": "H",
+ "requires": [
+ "shift"
+ ]
+ },
+ {
+ "title": "h",
+ "requires": [
+ "caps",
+ "shift"
+ ]
+ }
+ ],
+ "j": [
+ {
+ "title": "j",
+ "requires": []
+ },
+ {
+ "title": "J",
+ "requires": [
+ "caps"
+ ]
+ },
+ {
+ "title": "J",
+ "requires": [
+ "shift"
+ ]
+ },
+ {
+ "title": "j",
+ "requires": [
+ "caps",
+ "shift"
+ ]
+ }
+ ],
+ "k": [
+ {
+ "title": "k",
+ "requires": []
+ },
+ {
+ "title": "K",
+ "requires": [
+ "caps"
+ ]
+ },
+ {
+ "title": "K",
+ "requires": [
+ "shift"
+ ]
+ },
+ {
+ "title": "k",
+ "requires": [
+ "caps",
+ "shift"
+ ]
+ }
+ ],
+ "l": [
+ {
+ "title": "l",
+ "requires": []
+ },
+ {
+ "title": "L",
+ "requires": [
+ "caps"
+ ]
+ },
+ {
+ "title": "L",
+ "requires": [
+ "shift"
+ ]
+ },
+ {
+ "title": "l",
+ "requires": [
+ "caps",
+ "shift"
+ ]
+ }
+ ],
+ "z": [
+ {
+ "title": "z",
+ "requires": []
+ },
+ {
+ "title": "Z",
+ "requires": [
+ "caps"
+ ]
+ },
+ {
+ "title": "Z",
+ "requires": [
+ "shift"
+ ]
+ },
+ {
+ "title": "z",
+ "requires": [
+ "caps",
+ "shift"
+ ]
+ }
+ ],
+ "x": [
+ {
+ "title": "x",
+ "requires": []
+ },
+ {
+ "title": "X",
+ "requires": [
+ "caps"
+ ]
+ },
+ {
+ "title": "X",
+ "requires": [
+ "shift"
+ ]
+ },
+ {
+ "title": "x",
+ "requires": [
+ "caps",
+ "shift"
+ ]
+ }
+ ],
+ "c": [
+ {
+ "title": "c",
+ "requires": []
+ },
+ {
+ "title": "C",
+ "requires": [
+ "caps"
+ ]
+ },
+ {
+ "title": "C",
+ "requires": [
+ "shift"
+ ]
+ },
+ {
+ "title": "c",
+ "requires": [
+ "caps",
+ "shift"
+ ]
+ }
+ ],
+ "v": [
+ {
+ "title": "v",
+ "requires": []
+ },
+ {
+ "title": "V",
+ "requires": [
+ "caps"
+ ]
+ },
+ {
+ "title": "V",
+ "requires": [
+ "shift"
+ ]
+ },
+ {
+ "title": "v",
+ "requires": [
+ "caps",
+ "shift"
+ ]
+ }
+ ],
+ "b": [
+ {
+ "title": "b",
+ "requires": []
+ },
+ {
+ "title": "B",
+ "requires": [
+ "caps"
+ ]
+ },
+ {
+ "title": "B",
+ "requires": [
+ "shift"
+ ]
+ },
+ {
+ "title": "b",
+ "requires": [
+ "caps",
+ "shift"
+ ]
+ }
+ ],
+ "n": [
+ {
+ "title": "n",
+ "requires": []
+ },
+ {
+ "title": "N",
+ "requires": [
+ "caps"
+ ]
+ },
+ {
+ "title": "N",
+ "requires": [
+ "shift"
+ ]
+ },
+ {
+ "title": "n",
+ "requires": [
+ "caps",
+ "shift"
+ ]
+ }
+ ],
+ "m": [
+ {
+ "title": "m",
+ "requires": []
+ },
+ {
+ "title": "M",
+ "requires": [
+ "caps"
+ ]
+ },
+ {
+ "title": "M",
+ "requires": [
+ "shift"
+ ]
+ },
+ {
+ "title": "m",
+ "requires": [
+ "caps",
+ "shift"
+ ]
+ }
+ ]
+ },
+ "layout": [
+ [
+ "Esc",
+ 0.7,
+ "F1",
+ "F2",
+ "F3",
+ "F4",
+ 0.7,
+ "F5",
+ "F6",
+ "F7",
+ "F8",
+ 0.7,
+ "F9",
+ "F10",
+ "F11",
+ "F12"
+ ],
+ [
+ 0.1
+ ],
+ {
+ "main": {
+ "alpha": [
+ [
+ "`",
+ "1",
+ "2",
+ "3",
+ "4",
+ "5",
+ "6",
+ "7",
+ "8",
+ "9",
+ "0",
+ "-",
+ "=",
+ "Back"
+ ],
+ [
+ "Tab",
+ "q",
+ "w",
+ "e",
+ "r",
+ "t",
+ "y",
+ "u",
+ "i",
+ "o",
+ "p",
+ "[",
+ "]",
+ "\\"
+ ],
+ [
+ "Caps",
+ "a",
+ "s",
+ "d",
+ "f",
+ "g",
+ "h",
+ "j",
+ "k",
+ "l",
+ ";",
+ "'",
+ "Enter"
+ ],
+ [
+ "LShift",
+ "z",
+ "x",
+ "c",
+ "v",
+ "b",
+ "n",
+ "m",
+ ",",
+ ".",
+ "/",
+ "RShift"
+ ],
+ [
+ "LCtrl",
+ "Super",
+ "LAlt",
+ "Space",
+ "RAlt",
+ "Menu",
+ "RCtrl"
+ ]
+ ],
+ "movement": [
+ [
+ "Ins",
+ "Home",
+ "PgUp"
+ ],
+ [
+ "Del",
+ "End",
+ "PgDn"
+ ],
+ [
+ 1
+ ],
+ [
+ "Up"
+ ],
+ [
+ "Left",
+ "Down",
+ "Right"
+ ]
+ ]
+ }
+ }
+ ],
+ "keyWidths": {
+ "Back": 2,
+ "Tab": 1.5,
+ "\\": 1.5,
+ "Caps": 1.85,
+ "Enter": 2.25,
+ "LShift": 2.1,
+ "RShift": 3.1,
+ "LCtrl": 1.6,
+ "Super": 1.6,
+ "LAlt": 1.6,
+ "Space": 6.1,
+ "RAlt": 1.6,
+ "Menu": 1.6,
+ "RCtrl": 1.6,
+ "Ins": 1.6,
+ "Home": 1.6,
+ "PgUp": 1.6,
+ "Del": 1.6,
+ "End": 1.6,
+ "PgDn": 1.6
+ }
+ };
+});
\ No newline at end of file
diff --git a/src/sunstone/public/app/utils/guacamole/layouts/es-es-qwerty.js b/src/sunstone/public/app/utils/guacamole/layouts/es-es-qwerty.js
new file mode 100644
index 0000000000..6e51d1821d
--- /dev/null
+++ b/src/sunstone/public/app/utils/guacamole/layouts/es-es-qwerty.js
@@ -0,0 +1,1376 @@
+define(function() {
+ return {
+ "language": "es_ES",
+ "type": "qwerty",
+ "width": 23,
+ "keys": {
+ "Esc": 65307,
+ "F1": 65470,
+ "F2": 65471,
+ "F3": 65472,
+ "F4": 65473,
+ "F5": 65474,
+ "F6": 65475,
+ "F7": 65476,
+ "F8": 65477,
+ "F9": 65478,
+ "F10": 65479,
+ "F11": 65480,
+ "F12": 65481,
+ "Space": " ",
+ "Back": [
+ {
+ "title": "⟵",
+ "keysym": 65288
+ }
+ ],
+ "Tab": [
+ {
+ "title": "Tab ↹",
+ "keysym": 65289
+ }
+ ],
+ "Enter": [
+ {
+ "title": "↵",
+ "keysym": 65293
+ }
+ ],
+ "Home": [
+ {
+ "title": "Inicio",
+ "keysym": 65360
+ }
+ ],
+ "PgUp": [
+ {
+ "title": "RePág ↑",
+ "keysym": 65365
+ }
+ ],
+ "PgDn": [
+ {
+ "title": "AvPág ↓",
+ "keysym": 65366
+ }
+ ],
+ "End": [
+ {
+ "title": "Fin",
+ "keysym": 65367
+ }
+ ],
+ "Ins": [
+ {
+ "title": "Ins",
+ "keysym": 65379
+ }
+ ],
+ "Del": [
+ {
+ "title": "Supr",
+ "keysym": 65535
+ }
+ ],
+ "Left": [
+ {
+ "title": "←",
+ "keysym": 65361
+ }
+ ],
+ "Up": [
+ {
+ "title": "↑",
+ "keysym": 65362
+ }
+ ],
+ "Right": [
+ {
+ "title": "→",
+ "keysym": 65363
+ }
+ ],
+ "Down": [
+ {
+ "title": "↓",
+ "keysym": 65364
+ }
+ ],
+ "Menu": [
+ {
+ "title": "Menu",
+ "modifier": "super",
+ "keysym": 65383
+ }
+ ],
+ "LShift": [
+ {
+ "title": "Shift",
+ "modifier": "shift",
+ "keysym": 65505
+ }
+ ],
+ "RShift": [
+ {
+ "title": "Shift",
+ "modifier": "shift",
+ "keysym": 65506
+ }
+ ],
+ "LCtrl": [
+ {
+ "title": "Ctrl",
+ "modifier": "control",
+ "keysym": 65507
+ }
+ ],
+ "RCtrl": [
+ {
+ "title": "Ctrl",
+ "modifier": "control",
+ "keysym": 65508
+ }
+ ],
+ "Caps": [
+ {
+ "title": "Caps",
+ "modifier": "caps",
+ "keysym": 65509
+ }
+ ],
+ "LAlt": [
+ {
+ "title": "Alt",
+ "modifier": "alt",
+ "keysym": 65513
+ }
+ ],
+ "AltGr": [
+ {
+ "title": "AltGr",
+ "modifier": "alt-gr",
+ "keysym": 65027
+ }
+ ],
+ "Super": [
+ {
+ "title": "Super",
+ "modifier": "super",
+ "keysym": 65515
+ }
+ ],
+ "º": [
+ {
+ "title": "º",
+ "requires": []
+ },
+ {
+ "title": "ª",
+ "requires": [
+ "shift"
+ ]
+ },
+ {
+ "title": "\\",
+ "requires": [
+ "alt-gr"
+ ]
+ }
+ ],
+ "1": [
+ {
+ "title": "1",
+ "requires": []
+ },
+ {
+ "title": "!",
+ "requires": [
+ "shift"
+ ]
+ },
+ {
+ "title": "|",
+ "requires": [
+ "alt-gr"
+ ]
+ }
+ ],
+ "2": [
+ {
+ "title": "2",
+ "requires": []
+ },
+ {
+ "title": "\"",
+ "requires": [
+ "shift"
+ ]
+ },
+ {
+ "title": "@",
+ "requires": [
+ "alt-gr"
+ ]
+ }
+ ],
+ "3": [
+ {
+ "title": "3",
+ "requires": []
+ },
+ {
+ "title": ".",
+ "requires": [
+ "shift"
+ ]
+ },
+ {
+ "title": "#",
+ "requires": [
+ "alt-gr"
+ ]
+ }
+ ],
+ "4": [
+ {
+ "title": "4",
+ "requires": []
+ },
+ {
+ "title": "$",
+ "requires": [
+ "shift"
+ ]
+ },
+ {
+ "title": "~",
+ "requires": [
+ "alt-gr"
+ ]
+ }
+ ],
+ "5": [
+ {
+ "title": "5",
+ "requires": []
+ },
+ {
+ "title": "%",
+ "requires": [
+ "shift"
+ ]
+ },
+ {
+ "title": "€",
+ "requires": [
+ "alt-gr"
+ ]
+ }
+ ],
+ "6": [
+ {
+ "title": "6",
+ "requires": []
+ },
+ {
+ "title": "&",
+ "requires": [
+ "shift"
+ ]
+ },
+ {
+ "title": "¬",
+ "requires": [
+ "alt-gr"
+ ]
+ }
+ ],
+ "7": [
+ {
+ "title": "7",
+ "requires": []
+ },
+ {
+ "title": "/",
+ "requires": [
+ "shift"
+ ]
+ }
+ ],
+ "8": [
+ {
+ "title": "8",
+ "requires": []
+ },
+ {
+ "title": "(",
+ "requires": [
+ "shift"
+ ]
+ }
+ ],
+ "9": [
+ {
+ "title": "9",
+ "requires": []
+ },
+ {
+ "title": ")",
+ "requires": [
+ "shift"
+ ]
+ }
+ ],
+ "0": [
+ {
+ "title": "0",
+ "requires": []
+ },
+ {
+ "title": "=",
+ "requires": [
+ "shift"
+ ]
+ }
+ ],
+ "'": [
+ {
+ "title": "'",
+ "requires": []
+ },
+ {
+ "title": "?",
+ "requires": [
+ "shift"
+ ]
+ }
+ ],
+ "¡": [
+ {
+ "title": "¡",
+ "requires": []
+ },
+ {
+ "title": "¿",
+ "requires": [
+ "shift"
+ ]
+ }
+ ],
+ "q": [
+ {
+ "title": "q",
+ "requires": []
+ },
+ {
+ "title": "Q",
+ "requires": [
+ "caps"
+ ]
+ },
+ {
+ "title": "Q",
+ "requires": [
+ "shift"
+ ]
+ },
+ {
+ "title": "q",
+ "requires": [
+ "caps",
+ "shift"
+ ]
+ }
+ ],
+ "w": [
+ {
+ "title": "w",
+ "requires": []
+ },
+ {
+ "title": "W",
+ "requires": [
+ "caps"
+ ]
+ },
+ {
+ "title": "W",
+ "requires": [
+ "shift"
+ ]
+ },
+ {
+ "title": "w",
+ "requires": [
+ "caps",
+ "shift"
+ ]
+ }
+ ],
+ "e": [
+ {
+ "title": "e",
+ "requires": []
+ },
+ {
+ "title": "E",
+ "requires": [
+ "caps"
+ ]
+ },
+ {
+ "title": "E",
+ "requires": [
+ "shift"
+ ]
+ },
+ {
+ "title": "e",
+ "requires": [
+ "caps",
+ "shift"
+ ]
+ },
+ {
+ "title": "€",
+ "requires": [
+ "alt-gr"
+ ]
+ }
+ ],
+ "r": [
+ {
+ "title": "r",
+ "requires": []
+ },
+ {
+ "title": "R",
+ "requires": [
+ "caps"
+ ]
+ },
+ {
+ "title": "R",
+ "requires": [
+ "shift"
+ ]
+ },
+ {
+ "title": "r",
+ "requires": [
+ "caps",
+ "shift"
+ ]
+ }
+ ],
+ "t": [
+ {
+ "title": "t",
+ "requires": []
+ },
+ {
+ "title": "T",
+ "requires": [
+ "caps"
+ ]
+ },
+ {
+ "title": "T",
+ "requires": [
+ "shift"
+ ]
+ },
+ {
+ "title": "t",
+ "requires": [
+ "caps",
+ "shift"
+ ]
+ }
+ ],
+ "y": [
+ {
+ "title": "y",
+ "requires": []
+ },
+ {
+ "title": "Y",
+ "requires": [
+ "caps"
+ ]
+ },
+ {
+ "title": "Y",
+ "requires": [
+ "shift"
+ ]
+ },
+ {
+ "title": "y",
+ "requires": [
+ "caps",
+ "shift"
+ ]
+ }
+ ],
+ "u": [
+ {
+ "title": "u",
+ "requires": []
+ },
+ {
+ "title": "U",
+ "requires": [
+ "caps"
+ ]
+ },
+ {
+ "title": "U",
+ "requires": [
+ "shift"
+ ]
+ },
+ {
+ "title": "u",
+ "requires": [
+ "caps",
+ "shift"
+ ]
+ }
+ ],
+ "i": [
+ {
+ "title": "i",
+ "requires": []
+ },
+ {
+ "title": "I",
+ "requires": [
+ "caps"
+ ]
+ },
+ {
+ "title": "I",
+ "requires": [
+ "shift"
+ ]
+ },
+ {
+ "title": "i",
+ "requires": [
+ "caps",
+ "shift"
+ ]
+ }
+ ],
+ "o": [
+ {
+ "title": "o",
+ "requires": []
+ },
+ {
+ "title": "O",
+ "requires": [
+ "caps"
+ ]
+ },
+ {
+ "title": "O",
+ "requires": [
+ "shift"
+ ]
+ },
+ {
+ "title": "o",
+ "requires": [
+ "caps",
+ "shift"
+ ]
+ }
+ ],
+ "p": [
+ {
+ "title": "p",
+ "requires": []
+ },
+ {
+ "title": "P",
+ "requires": [
+ "caps"
+ ]
+ },
+ {
+ "title": "P",
+ "requires": [
+ "shift"
+ ]
+ },
+ {
+ "title": "p",
+ "requires": [
+ "caps",
+ "shift"
+ ]
+ }
+ ],
+ "`": [
+ {
+ "title": "`",
+ "requires": []
+ },
+ {
+ "title": "`",
+ "requires": [
+ "caps"
+ ]
+ },
+ {
+ "title": "^",
+ "requires": [
+ "shift"
+ ]
+ },
+ {
+ "title": "^",
+ "requires": [
+ "caps",
+ "shift"
+ ]
+ },
+ {
+ "title": "[",
+ "requires": [
+ "alt-gr"
+ ]
+ }
+ ],
+ "+": [
+ {
+ "title": "+",
+ "requires": []
+ },
+ {
+ "title": "+",
+ "requires": [
+ "caps"
+ ]
+ },
+ {
+ "title": "*",
+ "requires": [
+ "shift"
+ ]
+ },
+ {
+ "title": "*",
+ "requires": [
+ "caps",
+ "shift"
+ ]
+ },
+ {
+ "title": "]",
+ "requires": [
+ "alt-gr"
+ ]
+ }
+ ],
+ "a": [
+ {
+ "title": "a",
+ "requires": []
+ },
+ {
+ "title": "A",
+ "requires": [
+ "caps"
+ ]
+ },
+ {
+ "title": "A",
+ "requires": [
+ "shift"
+ ]
+ },
+ {
+ "title": "a",
+ "requires": [
+ "caps",
+ "shift"
+ ]
+ }
+ ],
+ "s": [
+ {
+ "title": "s",
+ "requires": []
+ },
+ {
+ "title": "S",
+ "requires": [
+ "caps"
+ ]
+ },
+ {
+ "title": "S",
+ "requires": [
+ "shift"
+ ]
+ },
+ {
+ "title": "s",
+ "requires": [
+ "caps",
+ "shift"
+ ]
+ }
+ ],
+ "d": [
+ {
+ "title": "d",
+ "requires": []
+ },
+ {
+ "title": "D",
+ "requires": [
+ "caps"
+ ]
+ },
+ {
+ "title": "D",
+ "requires": [
+ "shift"
+ ]
+ },
+ {
+ "title": "d",
+ "requires": [
+ "caps",
+ "shift"
+ ]
+ }
+ ],
+ "f": [
+ {
+ "title": "f",
+ "requires": []
+ },
+ {
+ "title": "F",
+ "requires": [
+ "caps"
+ ]
+ },
+ {
+ "title": "F",
+ "requires": [
+ "shift"
+ ]
+ },
+ {
+ "title": "f",
+ "requires": [
+ "caps",
+ "shift"
+ ]
+ }
+ ],
+ "g": [
+ {
+ "title": "g",
+ "requires": []
+ },
+ {
+ "title": "G",
+ "requires": [
+ "caps"
+ ]
+ },
+ {
+ "title": "G",
+ "requires": [
+ "shift"
+ ]
+ },
+ {
+ "title": "g",
+ "requires": [
+ "caps",
+ "shift"
+ ]
+ }
+ ],
+ "h": [
+ {
+ "title": "h",
+ "requires": []
+ },
+ {
+ "title": "H",
+ "requires": [
+ "caps"
+ ]
+ },
+ {
+ "title": "H",
+ "requires": [
+ "shift"
+ ]
+ },
+ {
+ "title": "h",
+ "requires": [
+ "caps",
+ "shift"
+ ]
+ }
+ ],
+ "j": [
+ {
+ "title": "j",
+ "requires": []
+ },
+ {
+ "title": "J",
+ "requires": [
+ "caps"
+ ]
+ },
+ {
+ "title": "J",
+ "requires": [
+ "shift"
+ ]
+ },
+ {
+ "title": "j",
+ "requires": [
+ "caps",
+ "shift"
+ ]
+ }
+ ],
+ "k": [
+ {
+ "title": "k",
+ "requires": []
+ },
+ {
+ "title": "K",
+ "requires": [
+ "caps"
+ ]
+ },
+ {
+ "title": "K",
+ "requires": [
+ "shift"
+ ]
+ },
+ {
+ "title": "k",
+ "requires": [
+ "caps",
+ "shift"
+ ]
+ }
+ ],
+ "l": [
+ {
+ "title": "l",
+ "requires": []
+ },
+ {
+ "title": "L",
+ "requires": [
+ "caps"
+ ]
+ },
+ {
+ "title": "L",
+ "requires": [
+ "shift"
+ ]
+ },
+ {
+ "title": "l",
+ "requires": [
+ "caps",
+ "shift"
+ ]
+ }
+ ],
+ "ñ": [
+ {
+ "title": "ñ",
+ "requires": []
+ },
+ {
+ "title": "Ñ",
+ "requires": [
+ "caps"
+ ]
+ },
+ {
+ "title": "Ñ",
+ "requires": [
+ "shift"
+ ]
+ },
+ {
+ "title": "ñ",
+ "requires": [
+ "caps",
+ "shift"
+ ]
+ }
+ ],
+ "´": [
+ {
+ "title": "´",
+ "requires": [],
+ "keysym": 65105
+ },
+ {
+ "title": "´",
+ "requires": [
+ "caps"
+ ]
+ },
+ {
+ "title": "¨",
+ "requires": [
+ "shift"
+ ]
+ },
+ {
+ "title": "¨",
+ "requires": [
+ "caps",
+ "shift"
+ ]
+ },
+ {
+ "title": "{",
+ "requires": [
+ "alt-gr"
+ ]
+ }
+ ],
+ "ç": [
+ {
+ "title": "ç",
+ "requires": []
+ },
+ {
+ "title": "Ç",
+ "requires": [
+ "caps"
+ ]
+ },
+ {
+ "title": "Ç",
+ "requires": [
+ "shift"
+ ]
+ },
+ {
+ "title": "ç",
+ "requires": [
+ "caps",
+ "shift"
+ ]
+ },
+ {
+ "title": "}",
+ "requires": [
+ "alt-gr"
+ ]
+ }
+ ],
+ "<": [
+ {
+ "title": "<",
+ "requires": []
+ },
+ {
+ "title": "<",
+ "requires": [
+ "caps"
+ ]
+ },
+ {
+ "title": ">",
+ "requires": [
+ "shift"
+ ]
+ },
+ {
+ "title": ">",
+ "requires": [
+ "caps",
+ "shift"
+ ]
+ }
+ ],
+ "z": [
+ {
+ "title": "z",
+ "requires": []
+ },
+ {
+ "title": "Z",
+ "requires": [
+ "caps"
+ ]
+ },
+ {
+ "title": "Z",
+ "requires": [
+ "shift"
+ ]
+ },
+ {
+ "title": "z",
+ "requires": [
+ "caps",
+ "shift"
+ ]
+ }
+ ],
+ "x": [
+ {
+ "title": "x",
+ "requires": []
+ },
+ {
+ "title": "X",
+ "requires": [
+ "caps"
+ ]
+ },
+ {
+ "title": "X",
+ "requires": [
+ "shift"
+ ]
+ },
+ {
+ "title": "x",
+ "requires": [
+ "caps",
+ "shift"
+ ]
+ }
+ ],
+ "c": [
+ {
+ "title": "c",
+ "requires": []
+ },
+ {
+ "title": "C",
+ "requires": [
+ "caps"
+ ]
+ },
+ {
+ "title": "C",
+ "requires": [
+ "shift"
+ ]
+ },
+ {
+ "title": "c",
+ "requires": [
+ "caps",
+ "shift"
+ ]
+ }
+ ],
+ "v": [
+ {
+ "title": "v",
+ "requires": []
+ },
+ {
+ "title": "V",
+ "requires": [
+ "caps"
+ ]
+ },
+ {
+ "title": "V",
+ "requires": [
+ "shift"
+ ]
+ },
+ {
+ "title": "v",
+ "requires": [
+ "caps",
+ "shift"
+ ]
+ }
+ ],
+ "b": [
+ {
+ "title": "b",
+ "requires": []
+ },
+ {
+ "title": "B",
+ "requires": [
+ "caps"
+ ]
+ },
+ {
+ "title": "B",
+ "requires": [
+ "shift"
+ ]
+ },
+ {
+ "title": "b",
+ "requires": [
+ "caps",
+ "shift"
+ ]
+ }
+ ],
+ "n": [
+ {
+ "title": "n",
+ "requires": []
+ },
+ {
+ "title": "N",
+ "requires": [
+ "caps"
+ ]
+ },
+ {
+ "title": "N",
+ "requires": [
+ "shift"
+ ]
+ },
+ {
+ "title": "n",
+ "requires": [
+ "caps",
+ "shift"
+ ]
+ }
+ ],
+ "m": [
+ {
+ "title": "m",
+ "requires": []
+ },
+ {
+ "title": "M",
+ "requires": [
+ "caps"
+ ]
+ },
+ {
+ "title": "M",
+ "requires": [
+ "shift"
+ ]
+ },
+ {
+ "title": "m",
+ "requires": [
+ "caps",
+ "shift"
+ ]
+ }
+ ],
+ ",": [
+ {
+ "title": ",",
+ "requires": []
+ },
+ {
+ "title": ";",
+ "requires": [
+ "shift"
+ ]
+ }
+ ],
+ ".": [
+ {
+ "title": ".",
+ "requires": []
+ },
+ {
+ "title": ":",
+ "requires": [
+ "shift"
+ ]
+ }
+ ],
+ "-": [
+ {
+ "title": "-",
+ "requires": []
+ },
+ {
+ "title": "_",
+ "requires": [
+ "shift"
+ ]
+ }
+ ]
+ },
+ "layout": [
+ [
+ "Esc",
+ 0.8,
+ "F1",
+ "F2",
+ "F3",
+ "F4",
+ 0.8,
+ "F5",
+ "F6",
+ "F7",
+ "F8",
+ 0.8,
+ "F9",
+ "F10",
+ "F11",
+ "F12"
+ ],
+ [
+ 0.1
+ ],
+ {
+ "main": {
+ "alpha": [
+ [
+ "º",
+ "1",
+ "2",
+ "3",
+ "4",
+ "5",
+ "6",
+ "7",
+ "8",
+ "9",
+ "0",
+ "'",
+ "¡",
+ "Back"
+ ],
+ [
+ "Tab",
+ "q",
+ "w",
+ "e",
+ "r",
+ "t",
+ "y",
+ "u",
+ "i",
+ "o",
+ "p",
+ "`",
+ "+",
+ 1,
+ 0.6
+ ],
+ [
+ "Caps",
+ "a",
+ "s",
+ "d",
+ "f",
+ "g",
+ "h",
+ "j",
+ "k",
+ "l",
+ "ñ",
+ "´",
+ "ç",
+ "Enter"
+ ],
+ [
+ "LShift",
+ "<",
+ "z",
+ "x",
+ "c",
+ "v",
+ "b",
+ "n",
+ "m",
+ ",",
+ ".",
+ "-",
+ "RShift"
+ ],
+ [
+ "LCtrl",
+ "Super",
+ "LAlt",
+ "Space",
+ "AltGr",
+ "Menu",
+ "RCtrl"
+ ]
+ ],
+ "movement": [
+ [
+ "Ins",
+ "Home",
+ "PgUp"
+ ],
+ [
+ "Del",
+ "End",
+ "PgDn"
+ ],
+ [
+ 1
+ ],
+ [
+ "Up"
+ ],
+ [
+ "Left",
+ "Down",
+ "Right"
+ ]
+ ]
+ }
+ }
+ ],
+ "keyWidths": {
+ "Back": 2.3,
+ "Tab": 1.75,
+ "\\": 1.25,
+ "Caps": 1.75,
+ "Enter": 1.5,
+ "LShift": 2.2,
+ "RShift": 2.2,
+ "LCtrl": 1.6,
+ "Super": 1.6,
+ "LAlt": 1.6,
+ "Space": 6.4,
+ "AltGr": 1.6,
+ "Menu": 1.6,
+ "RCtrl": 1.6,
+ "Ins": 1.6,
+ "Home": 1.6,
+ "PgUp": 1.6,
+ "Del": 1.6,
+ "End": 1.6,
+ "PgDn": 1.6
+ }
+ };
+});
\ No newline at end of file
diff --git a/src/sunstone/public/app/utils/guacamole/types/client-properties.js b/src/sunstone/public/app/utils/guacamole/types/client-properties.js
new file mode 100644
index 0000000000..2d710266c6
--- /dev/null
+++ b/src/sunstone/public/app/utils/guacamole/types/client-properties.js
@@ -0,0 +1,91 @@
+/* -------------------------------------------------------------------------- */
+/* Copyright 2002-2021, OpenNebula Project, OpenNebula Systems */
+/* */
+/* Licensed under the Apache License, Version 2.0 (the "License"); you may */
+/* not use this file except in compliance with the License. You may obtain */
+/* a copy of the License at */
+/* */
+/* http://www.apache.org/licenses/LICENSE-2.0 */
+/* */
+/* Unless required by applicable law or agreed to in writing, software */
+/* distributed under the License is distributed on an "AS IS" BASIS, */
+/* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. */
+/* See the License for the specific language governing permissions and */
+/* limitations under the License. */
+/* -------------------------------------------------------------------------- */
+
+define(function() {
+
+ /**
+ * Object used for interacting with a guacClient directive.
+ *
+ * @constructor
+ * @param {ClientProperties|Object} [template = {}]
+ * The object whose properties should be copied within the new ClientProperties.
+ */
+ function ClientProperties(template = {}) {
+ /**
+ * Whether the display should be scaled automatically to fit within the
+ * available space.
+ *
+ * @type Boolean
+ */
+ this.autoFit = template.autoFit || true;
+
+ /**
+ * The current scale. If autoFit is true, the effect of setting this
+ * value is undefined.
+ *
+ * @type Number
+ */
+ this.scale = template.scale || 1;
+
+ /**
+ * The minimum scale value.
+ *
+ * @type Number
+ */
+ this.minScale = template.minScale || 1;
+
+ /**
+ * The maximum scale value.
+ *
+ * @type Number
+ */
+ this.maxScale = template.maxScale || 3;
+
+ /**
+ * Whether or not the client should listen to keyboard events.
+ *
+ * @type Boolean
+ */
+ this.keyboardEnabled = template.keyboardEnabled || true;
+
+ /**
+ * Whether translation of touch to mouse events should emulate an
+ * absolute pointer device, or a relative pointer device.
+ *
+ * @type Boolean
+ */
+ this.emulateAbsoluteMouse = template.emulateAbsoluteMouse || true;
+
+ /**
+ * The relative Y coordinate of the scroll offset of the display within
+ * the client element.
+ *
+ * @type Number
+ */
+ this.scrollTop = template.scrollTop || 0;
+
+ /**
+ * The relative X coordinate of the scroll offset of the display within
+ * the client element.
+ *
+ * @type Number
+ */
+ this.scrollLeft = template.scrollLeft || 0;
+ };
+
+ return ClientProperties;
+
+});
\ No newline at end of file
diff --git a/src/sunstone/public/app/utils/guacamole/types/client-state.js b/src/sunstone/public/app/utils/guacamole/types/client-state.js
new file mode 100644
index 0000000000..ba7bb06827
--- /dev/null
+++ b/src/sunstone/public/app/utils/guacamole/types/client-state.js
@@ -0,0 +1,127 @@
+/* -------------------------------------------------------------------------- */
+/* Copyright 2002-2021, OpenNebula Project, OpenNebula Systems */
+/* */
+/* Licensed under the Apache License, Version 2.0 (the "License"); you may */
+/* not use this file except in compliance with the License. You may obtain */
+/* a copy of the License at */
+/* */
+/* http://www.apache.org/licenses/LICENSE-2.0 */
+/* */
+/* Unless required by applicable law or agreed to in writing, software */
+/* distributed under the License is distributed on an "AS IS" BASIS, */
+/* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. */
+/* See the License for the specific language governing permissions and */
+/* limitations under the License. */
+/* -------------------------------------------------------------------------- */
+
+define(function(require) {
+
+ require("guacamole-common-js")
+
+ /**
+ * 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.
+ */
+ function ManagedClientState(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;
+
+ /**
+ * Whether the network connection used by the tunnel seems unstable. If
+ * the network connection is unstable, the remote desktop connection
+ * may perform poorly or disconnect.
+ *
+ * @type {Boolean}
+ * @default false
+ */
+ this.tunnelUnstable = template.tunnelUnstable || false;
+
+ /**
+ * 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 = {
+ IDLE : "IDLE",
+ CONNECTING : "CONNECTING",
+ WAITING : "WAITING",
+ CONNECTED : "CONNECTED",
+ DISCONNECTED : "DISCONNECTED",
+ CLIENT_ERROR : "CLIENT_ERROR",
+ 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. If the
+ * client state was previously marked as unstable, that flag is implicitly
+ * cleared.
+ *
+ * @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;
+ clientState.tunnelUnstable = false;
+
+ // Set status code, if given
+ if (statusCode) {
+ clientState.statusCode = statusCode;
+ }
+ };
+
+ /**
+ * Updates the given client state, setting whether the underlying tunnel
+ * is currently unstable. An unstable tunnel is not necessarily
+ * disconnected, but appears to be misbehaving and may be disconnected.
+ *
+ * @param {ManagedClientState} clientState The ManagedClientState to update.
+ *
+ * @param {Boolean} unstable
+ * Whether the underlying tunnel of the connection currently appears unstable.
+ */
+ ManagedClientState.setTunnelUnstable = function(clientState, unstable) {
+ clientState.tunnelUnstable = unstable;
+ };
+
+ return ManagedClientState;
+
+});
\ No newline at end of file
diff --git a/src/sunstone/public/app/utils/guacamole/types/client-thumbnail.js b/src/sunstone/public/app/utils/guacamole/types/client-thumbnail.js
new file mode 100644
index 0000000000..65c4891a20
--- /dev/null
+++ b/src/sunstone/public/app/utils/guacamole/types/client-thumbnail.js
@@ -0,0 +1,47 @@
+/* -------------------------------------------------------------------------- */
+/* Copyright 2002-2021, OpenNebula Project, OpenNebula Systems */
+/* */
+/* Licensed under the Apache License, Version 2.0 (the "License"); you may */
+/* not use this file except in compliance with the License. You may obtain */
+/* a copy of the License at */
+/* */
+/* http://www.apache.org/licenses/LICENSE-2.0 */
+/* */
+/* Unless required by applicable law or agreed to in writing, software */
+/* distributed under the License is distributed on an "AS IS" BASIS, */
+/* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. */
+/* See the License for the specific language governing permissions and */
+/* limitations under the License. */
+/* -------------------------------------------------------------------------- */
+
+define(function() {
+
+ /**
+ * Object which represents a thumbnail of the Guacamole client display,
+ * along with the time that the thumbnail was generated.
+ *
+ * @constructor
+ * @param {ManagedClientThumbnail|Object} [template = {}]
+ * The object whose properties should be copied within the new
+ * ManagedClientThumbnail.
+ */
+ function ManagedClientThumbnail(template = {}) {
+ /**
+ * The time that this thumbnail was generated, as the number of
+ * milliseconds elapsed since midnight of January 1, 1970 UTC.
+ *
+ * @type Number
+ */
+ this.timestamp = template.timestamp;
+
+ /**
+ * The thumbnail of the Guacamole client display.
+ *
+ * @type HTMLCanvasElement
+ */
+ this.canvas = template.canvas;
+ };
+
+ return ManagedClientThumbnail;
+
+});
\ No newline at end of file
diff --git a/src/sunstone/public/app/utils/guacamole/types/client.js b/src/sunstone/public/app/utils/guacamole/types/client.js
new file mode 100644
index 0000000000..c7adc7ee0c
--- /dev/null
+++ b/src/sunstone/public/app/utils/guacamole/types/client.js
@@ -0,0 +1,543 @@
+/* -------------------------------------------------------------------------- */
+/* Copyright 2002-2021, OpenNebula Project, OpenNebula Systems */
+/* */
+/* Licensed under the Apache License, Version 2.0 (the "License"); you may */
+/* not use this file except in compliance with the License. You may obtain */
+/* a copy of the License at */
+/* */
+/* http://www.apache.org/licenses/LICENSE-2.0 */
+/* */
+/* Unless required by applicable law or agreed to in writing, software */
+/* distributed under the License is distributed on an "AS IS" BASIS, */
+/* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. */
+/* See the License for the specific language governing permissions and */
+/* limitations under the License. */
+/* -------------------------------------------------------------------------- */
+
+define(function(require) {
+
+ require("guacamole-common-js")
+ var ClientProperties = require("utils/guacamole/types/client-properties");
+ var ClipboardData = require("utils/guacamole/types/clipboard-data");
+ var Config = require("sunstone-config");
+ var ManagedClientState = require("utils/guacamole/types/client-state");
+ var ManagedClientThumbnail = require("utils/guacamole/types/client-thumbnail");
+ var ManagedDisplay = require("utils/guacamole/types/display");
+
+ /**
+ * The minimum amount of time to wait between updates to the client
+ * thumbnail, in milliseconds.
+ *
+ * @type Number
+ */
+ var THUMBNAIL_UPDATE_FREQUENCY = 5000;
+
+ /**
+ * 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.
+ */
+ function ManagedClient(template = {}) {
+ /**
+ * The token of the connection associated with this client.
+ *
+ * @type String
+ */
+ this.token = template.token;
+
+ /**
+ * The time that the connection was last brought to the foreground of
+ * the current tab, as the number of milliseconds elapsed since
+ * midnight of January 1, 1970 UTC. If the connection has not yet been
+ * viewed, this will be 0.
+ *
+ * @type Number
+ */
+ this.lastUsed = template.lastUsed || 0;
+
+ /**
+ * 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 title which should be displayed as the page title for this
+ * client.
+ *
+ * @type String
+ */
+ this.title = template.title;
+
+ /**
+ * The most recently-generated thumbnail for this connection, as
+ * stored within the local connection history. If no thumbnail is
+ * stored, this will be null.
+ *
+ * @type ManagedClientThumbnail
+ */
+ this.thumbnail = template.thumbnail;
+
+ /**
+ * The current clipboard contents.
+ *
+ * @type ClipboardData
+ */
+ this.clipboardData = template.clipboardData || new ClipboardData({
+ type : 'text/plain',
+ data : ''
+ });
+
+ /**
+ * The current state of all parameters requested by the server via
+ * "required" instructions, where each object key is the name of a
+ * requested parameter and each value is the current value entered by
+ * the user or null if no parameters are currently being requested.
+ *
+ * @type Object.
+ */
+ this.requiredParameters = null;
+
+ /**
+ * 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 || [];
+
+ /**
+ * All currently-exposed filesystems. When the Guacamole server exposes
+ * a filesystem object, that object will be made available as a
+ * ManagedFilesystem within this array.
+ *
+ * @type ManagedFilesystem[]
+ */
+ this.filesystems = template.filesystems || [];
+
+ /**
+ * All available share links generated for the this ManagedClient via
+ * ManagedClient.createShareLink(). Each resulting share link is stored
+ * under the identifier of its corresponding SharingProfile.
+ *
+ * @type Object.
+ */
+ this.shareLinks = template.shareLinks || {};
+
+ /**
+ * The number of simultaneous touch contacts supported by the remote
+ * desktop. Unless explicitly declared otherwise by the remote desktop
+ * after connecting, this will be 0 (multi-touch unsupported).
+ *
+ * @type Number
+ */
+ this.multiTouchSupport = template.multiTouchSupport || 0;
+
+ /**
+ * 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();
+
+ /**
+ * All editable arguments (connection parameters), stored by their
+ * names. Arguments will only be present within this set if their
+ * current values have been exposed by the server via an inbound "argv"
+ * stream and the server has confirmed that the value may be changed
+ * through a successful "ack" to an outbound "argv" stream.
+ *
+ * @type {Object.}
+ */
+ this.arguments = template.arguments || {};
+
+ };
+
+ /**
+ * The mimetype of audio data to be sent along the Guacamole connection if
+ * audio input is supported.
+ *
+ * @constant
+ * @type String
+ */
+ ManagedClient.AUDIO_INPUT_MIMETYPE = 'audio/L16;rate=44100,channels=2';
+
+ /**
+ * Returns a promise which resolves with 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/image formats. The returned promise is
+ * guaranteed to resolve successfully.
+ *
+ * @param {String} token The identifier representing the connection or group to connect to.
+ * @param {String[]} [connectionParameters] Any additional HTTP parameters to pass while connecting.
+ * @param {Element} [display] Element where the connection will be displayed.
+ *
+ * @returns {String} A string of connection parameters to be passed to the Guacamole client.
+ */
+ function getConnectString(token, connectionParameters, display = window) {
+ // Calculate optimal width/height for display
+ var pixel_density = window.devicePixelRatio || 1;
+ var optimal_width = display.innerWidth * pixel_density;
+ var optimal_height = display.innerHeight * pixel_density;
+ var optimal_dpi = pixel_density * 96;
+
+ // Build base connect string
+ var connectString = [
+ "token=" + encodeURIComponent(token),
+ "width=" + Math.floor(optimal_width),
+ "height=" + Math.floor(optimal_height),
+ "dpi=" + Math.floor(optimal_dpi)
+ ];
+
+ connectionParameters && connectString.concat(connectionParameters);
+
+ return connectString.join('&');
+ }
+
+ /**
+ * Requests the creation of a new audio stream, recorded from the user's
+ * local audio input device. If audio input is supported by the connection,
+ * an audio stream will be created which will remain open until the remote
+ * desktop requests that it be closed. If the audio stream is successfully
+ * created but is later closed, a new audio stream will automatically be
+ * established to take its place. The mimetype used for all audio streams
+ * produced by this function is defined by
+ * ManagedClient.AUDIO_INPUT_MIMETYPE.
+ *
+ * @param {Guacamole.Client} client
+ * The Guacamole.Client for which the audio stream is being requested.
+ */
+ var requestAudioStream = function requestAudioStream(client) {
+ // Create new audio stream, associating it with an AudioRecorder
+ var stream = client.createAudioStream(ManagedClient.AUDIO_INPUT_MIMETYPE);
+ var recorder = Guacamole.AudioRecorder.getInstance(stream, ManagedClient.AUDIO_INPUT_MIMETYPE);
+
+ // If creation of the AudioRecorder failed, simply end the stream
+ if (!recorder) {
+ stream.sendEnd();
+ }
+ // Otherwise, ensure that another audio stream is created after this
+ // audio stream is closed
+ else {
+ recorder.onclose = requestAudioStream.bind(this, client);
+ }
+ };
+
+ /**
+ * Creates a new ManagedClient, connecting it to the specified connection
+ * or group.
+ *
+ * @param {String} token The token of the connection.
+ * @param {String[]} [connectionParameters] Any additional HTTP parameters to pass while connecting.
+ * @param {Element} [display] Element where the connection will be displayed.
+ *
+ * @returns {ManagedClient}
+ * A new ManagedClient instance which is connected to the connection or
+ * connection group having the given ID.
+ */
+ ManagedClient.getInstance = function getInstance(token, connectionParameters, display = window) {
+ var endpoint = new URL(Config.publicFireedgeEndpoint);
+
+ var websocketProtocol = endpoint.protocol === 'https:' ? 'wss:' : 'ws:';
+ var fireedgeWebsocket = websocketProtocol + '//' + endpoint.host + '/fireedge/guacamole'
+
+ // Get new websocket tunnel instance
+ var tunnel = new Guacamole.WebSocketTunnel(fireedgeWebsocket);
+
+ // Get new client instance
+ var client = new Guacamole.Client(tunnel);
+
+ // Associate new managed client with new client and tunnel
+ var managedClient = new ManagedClient({
+ id : token,
+ client : client,
+ tunnel : tunnel
+ });
+
+ tunnel.onerror = function tunnelError(status) {
+ ManagedClientState.setConnectionState(
+ managedClient.clientState,
+ ManagedClientState.ConnectionState.TUNNEL_ERROR,
+ status.code
+ );
+ };
+
+ // Update connection state as tunnel state changes
+ tunnel.onstatechange = function tunnelStateChanged(state) {
+ switch (state) {
+ // Connection is being established
+ case Guacamole.Tunnel.State.CONNECTING:
+ ManagedClientState.setConnectionState(
+ managedClient.clientState,
+ ManagedClientState.ConnectionState.CONNECTING
+ );
+ break;
+
+ // Connection is established / no longer unstable
+ case Guacamole.Tunnel.State.OPEN:
+ ManagedClientState.setTunnelUnstable(managedClient.clientState, false);
+ break;
+
+ // Connection is established but misbehaving
+ case Guacamole.Tunnel.State.UNSTABLE:
+ ManagedClientState.setTunnelUnstable(managedClient.clientState, true);
+ break;
+
+ // Connection has closed
+ case Guacamole.Tunnel.State.CLOSED:
+ ManagedClientState.setConnectionState(
+ managedClient.clientState,
+ ManagedClientState.ConnectionState.DISCONNECTED
+ );
+ break;
+ }
+ };
+
+ // Update connection state as client state changes
+ client.onstatechange = function clientStateChanged(clientState) {
+ switch (clientState) {
+ // Idle
+ case 0:
+ ManagedClientState.setConnectionState(
+ managedClient.clientState,
+ ManagedClientState.ConnectionState.IDLE
+ );
+ break;
+
+ // Ignore "connecting" state
+ case 1: // Connecting
+ break;
+
+ // Connected + waiting
+ case 2:
+ ManagedClientState.setConnectionState(
+ managedClient.clientState,
+ ManagedClientState.ConnectionState.WAITING
+ );
+ break;
+
+ // Connected
+ case 3:
+ ManagedClientState.setConnectionState(
+ managedClient.clientState,
+ ManagedClientState.ConnectionState.CONNECTED
+ );
+
+ // Send any clipboard data already provided
+ if (managedClient.clipboardData) {
+ ManagedClient.setClipboard(managedClient, managedClient.clipboardData);
+ }
+
+ // Begin streaming audio input if possible
+ // requestAudioStream(client);
+
+ // Update thumbnail with initial display contents
+ //ManagedClient.updateThumbnail(managedClient);
+ break;
+
+ // Update history when disconnecting
+ case 4: // Disconnecting
+ case 5: // Disconnected
+ //ManagedClient.updateThumbnail(managedClient);
+ break;
+ }
+ };
+
+ // Disconnect and update status when the client receives an error
+ client.onerror = function clientError(status) {
+ // Disconnect, if connected
+ client.disconnect();
+
+ // Update state
+ ManagedClientState.setConnectionState(
+ managedClient.clientState,
+ ManagedClientState.ConnectionState.CLIENT_ERROR,
+ status.code
+ );
+ };
+
+ // Automatically update the client thumbnail
+ client.onsync = function syncReceived() {
+ var thumbnail = managedClient.thumbnail;
+ var timestamp = new Date().getTime();
+
+ // Update thumbnail if it doesn't exist or is old
+ if (!thumbnail || timestamp - thumbnail.timestamp >= THUMBNAIL_UPDATE_FREQUENCY) {
+ //ManagedClient.updateThumbnail(managedClient);
+ }
+ };
+
+ // Handle any received clipboard data
+ client.onclipboard = function clientClipboardReceived(stream, mimetype) {
+ var reader;
+
+ // If the received data is text, read it as a simple string
+ if (/^text\//.exec(mimetype)) {
+ reader = new Guacamole.StringReader(stream);
+
+ // Assemble received data into a single string
+ var data = '';
+ reader.ontext = function textReceived(text) {
+ data += text;
+ };
+
+ // Set clipboard contents once stream is finished
+ reader.onend = function textComplete() {
+ managedClient.clipboardData = new ClipboardData({
+ type : mimetype,
+ data : data
+ });
+ };
+ }
+ // Otherwise read the clipboard data as a Blob
+ else {
+ reader = new Guacamole.BlobReader(stream, mimetype);
+ reader.onend = function blobComplete() {
+ managedClient.clipboardData = new ClipboardData({
+ type : mimetype,
+ data : reader.getBlob()
+ });
+ };
+ }
+ };
+
+ // Update title when a "name" instruction is received
+ client.onname = function clientNameReceived(name) {
+ managedClient.title = name;
+ };
+
+ // Manage the client display
+ managedClient.managedDisplay = ManagedDisplay.getInstance(client.getDisplay());
+
+ // Connect the Guacamole client
+ var connectString = getConnectString(token, connectionParameters, display);
+ client.connect(connectString);
+
+ return managedClient;
+ }
+
+ /**
+ * Sends the given clipboard data over the given Guacamole client, setting
+ * the contents of the remote clipboard to the data provided.
+ *
+ * @param {ManagedClient} managedClient
+ * The ManagedClient over which the given clipboard data is to be sent.
+ *
+ * @param {ClipboardData} data
+ * The clipboard data to send.
+ */
+ ManagedClient.setClipboard = function setClipboard(managedClient, data) {
+ var writer;
+
+ // Create stream with proper mimetype
+ var stream = managedClient.client.createClipboardStream(data.type);
+
+ // Send data as a string if it is stored as a string
+ if (typeof data.data === 'string') {
+ writer = new Guacamole.StringWriter(stream);
+
+ for (var i = 0; i < data.data.length; i += 4096) {
+ writer.sendText(data.data.substring(i, i + 4096));
+ }
+
+ writer.sendEnd();
+ }
+ // Otherwise, assume the data is a File/Blob
+ else {
+ // Write File/Blob asynchronously
+ writer = new Guacamole.BlobWriter(stream);
+ writer.oncomplete = function clipboardSent() {
+ writer.sendEnd();
+ };
+
+ // Begin sending data
+ writer.sendBlob(data.data);
+ }
+ };
+
+ /**
+ * 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 {ManagedClient} managedClient
+ * The client whose history entry should be updated.
+ */
+ ManagedClient.updateThumbnail = function updateThumbnail(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.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
+ );
+
+ // Store updated thumbnail within client
+ managedClient.thumbnail = new ManagedClientThumbnail({
+ timestamp : new Date().getTime(),
+ canvas : thumbnail
+ });
+
+ // Update historical thumbnail
+ // guacHistory.updateThumbnail(managedClient.id, thumbnail.toDataURL("image/png"));
+
+ }
+
+};
+
+ return ManagedClient;
+
+});
\ No newline at end of file
diff --git a/src/sunstone/public/app/utils/guacamole/types/clipboard-data.js b/src/sunstone/public/app/utils/guacamole/types/clipboard-data.js
new file mode 100644
index 0000000000..bb66ec24ec
--- /dev/null
+++ b/src/sunstone/public/app/utils/guacamole/types/clipboard-data.js
@@ -0,0 +1,47 @@
+/* -------------------------------------------------------------------------- */
+/* Copyright 2002-2021, OpenNebula Project, OpenNebula Systems */
+/* */
+/* Licensed under the Apache License, Version 2.0 (the "License"); you may */
+/* not use this file except in compliance with the License. You may obtain */
+/* a copy of the License at */
+/* */
+/* http://www.apache.org/licenses/LICENSE-2.0 */
+/* */
+/* Unless required by applicable law or agreed to in writing, software */
+/* distributed under the License is distributed on an "AS IS" BASIS, */
+/* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. */
+/* See the License for the specific language governing permissions and */
+/* limitations under the License. */
+/* -------------------------------------------------------------------------- */
+
+define(function() {
+
+ /**
+ * Arbitrary data which can be contained by the clipboard.
+ *
+ * @constructor
+ * @param {ClipboardData|Object} [template = {}]
+ * The object whose properties should be copied within the new
+ * ClipboardData.
+ */
+ function ClipboardData(template = {}) {
+ /**
+ * The mimetype of the data currently stored within the clipboard.
+ *
+ * @type String
+ */
+ this.type = template.type || 'text/plain';
+
+ /**
+ * The data currently stored within the clipboard. Depending on the
+ * nature of the stored data, this may be either a String, a Blob, or a
+ * File.
+ *
+ * @type String|Blob|File
+ */
+ this.data = template.data || '';
+ };
+
+ return ClipboardData;
+
+});
\ No newline at end of file
diff --git a/src/sunstone/public/app/utils/guacamole/types/display.js b/src/sunstone/public/app/utils/guacamole/types/display.js
new file mode 100644
index 0000000000..1f00af472d
--- /dev/null
+++ b/src/sunstone/public/app/utils/guacamole/types/display.js
@@ -0,0 +1,145 @@
+/* -------------------------------------------------------------------------- */
+/* Copyright 2002-2021, OpenNebula Project, OpenNebula Systems */
+/* */
+/* Licensed under the Apache License, Version 2.0 (the "License"); you may */
+/* not use this file except in compliance with the License. You may obtain */
+/* a copy of the License at */
+/* */
+/* http://www.apache.org/licenses/LICENSE-2.0 */
+/* */
+/* Unless required by applicable law or agreed to in writing, software */
+/* distributed under the License is distributed on an "AS IS" BASIS, */
+/* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. */
+/* See the License for the specific language governing permissions and */
+/* limitations under the License. */
+/* -------------------------------------------------------------------------- */
+
+define(function() {
+
+ /**
+ * 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.
+ */
+ function ManagedDisplay(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 = {}) {
+ /**
+ * 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 = {}) {
+ /**
+ * 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() {
+ managedDisplay.size = new ManagedDisplay.Dimensions({
+ width : display.getWidth(),
+ height : display.getHeight()
+ });
+ };
+
+ // Store changes to display cursor
+ display.oncursor = function setClientCursor(canvas, x, y) {
+ managedDisplay.cursor = new ManagedDisplay.Cursor({
+ canvas : canvas,
+ x : x,
+ y : y
+ });
+ };
+
+ return managedDisplay;
+ };
+
+ return ManagedDisplay;
+
+});
\ No newline at end of file
diff --git a/src/sunstone/public/app/utils/guacamole/utils.js b/src/sunstone/public/app/utils/guacamole/utils.js
new file mode 100644
index 0000000000..0a04db38b4
--- /dev/null
+++ b/src/sunstone/public/app/utils/guacamole/utils.js
@@ -0,0 +1,101 @@
+/* -------------------------------------------------------------------------- */
+/* Copyright 2002-2021, OpenNebula Project, OpenNebula Systems */
+/* */
+/* Licensed under the Apache License, Version 2.0 (the "License"); you may */
+/* not use this file except in compliance with the License. You may obtain */
+/* a copy of the License at */
+/* */
+/* http://www.apache.org/licenses/LICENSE-2.0 */
+/* */
+/* Unless required by applicable law or agreed to in writing, software */
+/* distributed under the License is distributed on an "AS IS" BASIS, */
+/* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. */
+/* See the License for the specific language governing permissions and */
+/* limitations under the License. */
+/* -------------------------------------------------------------------------- */
+
+define(function() {
+
+ /**
+ * Create a new function that limits calls to func to once every given time frame.
+ *
+ * @param {*} func Throttled function
+ * @param {*} delay Time delay between calls
+ */
+ function throttle(func, delay) {
+ var wait = false;
+
+ return function() {
+ if (!wait) {
+ func();
+ wait = true;
+
+ setTimeout(function() {
+ wait = false;
+ }, delay)
+ }
+ }
+ }
+
+ // A function to modify the property's getters and
+ // setters so that a custom callback handler can be run in the main
+ // program each time the property is changed
+ function observe(subject, property, callbackHandler) {
+ Object.defineProperty(subject, property, {
+ // Return the default value of the property
+ // ("this.value" automatically gives you the property's current value)
+ get: function() {
+ return this.value;
+ },
+
+ // Set the property with a new value
+ set: function(newValue) {
+ // Assign the new value
+ this.value = newValue;
+
+ // Bind the observer's changeHandler to the subject
+ subject.changeHandler = callbackHandler;
+
+ // Tell the subject to call the changeHandler when this property is changed.
+ // (This is like a custom event dispatcher)
+ subject.changeHandler(newValue)
+ },
+
+ // Set the default parameters for how this property can be accessed and changed.
+ // You probably don't need to change these unless you want to lock down the
+ // property values to prevent your program from changing them
+ enumerable: true,
+ configurable: true,
+ writeable: true
+ });
+ }
+
+ // An optional function to stop watching properties.
+ // It normalizes the getter and setter and removes the callback handler
+ function unobserve(subject, property) {
+ // Delete the changeHandler
+ delete subject.changeHandler;
+
+ // Reset the getter and setter
+ Object.defineProperty(subject, property, {
+ get: function() {
+ return this.value;
+ },
+ set: function(newValue) {
+ this.value = newValue;
+ },
+ enumerable: true,
+ configurable: true,
+ writeable: true
+ });
+ }
+
+ var Utils = {
+ throttle: throttle,
+ observe: observe,
+ unobserve: unobserve
+ }
+
+ return Utils;
+
+});
\ No newline at end of file
diff --git a/src/sunstone/public/scss/_layout.scss b/src/sunstone/public/scss/_layout.scss
index c2fa9579c4..aa33b6b5a3 100644
--- a/src/sunstone/public/scss/_layout.scss
+++ b/src/sunstone/public/scss/_layout.scss
@@ -161,8 +161,7 @@ progress{
}
}
-.guacamole-display {
- cursor: none;
+.guacamole-main {
width: 100%;
height: fit-content;
display: flex;
@@ -172,4 +171,251 @@ progress{
& > div {
z-index: 1;
}
+ & .guacamole-display {
+ cursor: none;
+ }
}
+
+#guacVMDialog {
+ overflow: hidden;
+}
+
+.osk-container {
+ z-index: 2;
+ background: rgba(0, 0, 0, 0.59);
+ position: absolute;
+ top: 30%;
+ left: 0;
+ display: none;
+ border: 1px solid #acacac;
+ border-radius: 6px;
+ box-shadow: 0 0 20px #acacac;
+ & > .osk-container-header {
+ background: linear-gradient(to top, #ebebeb, #d5d5d5);
+ color: #4d494d;
+ font-size: 11pt;
+ line-height: 20px;
+ text-align: center;
+ width: 100%;
+ height: 28px;
+ user-select: none;
+ cursor: default;
+ border-top: 1px solid #f3f1f3;
+ border-bottom: 1px solid #b1aeb1;
+ border-top-left-radius: 6px;
+ border-top-right-radius: 6px;
+ & .buttons {
+ padding-left: 8px;
+ padding-top: 3px;
+ float: left;
+ line-height: 0;
+ & .close {
+ background: #ff5c5c;
+ font-size: 13px;
+ font-weight: bold;
+ width: 15px;
+ height: 15px;
+ border-radius: 50%;
+ display: inline-block;
+ }
+ }
+ & .layouts {
+ padding-right: 8px;
+ padding-top: 3px;
+ float: right;
+ line-height: 0;
+ }
+ }
+}
+
+.guac-keyboard {
+ display: inline-block;
+ width: 100%;
+
+ margin: 0;
+ padding: 0;
+ cursor: default;
+
+ text-align: left;
+ vertical-align: middle;
+}
+
+.guac-keyboard,
+.guac-keyboard * {
+ overflow: hidden;
+ white-space: nowrap;
+}
+
+.guac-keyboard .guac-keyboard-key-container {
+ display: inline-block;
+ margin: 0.05em;
+ position: relative;
+}
+
+.guac-keyboard .guac-keyboard-key {
+
+ position: absolute;
+ left: 0;
+ right: 0;
+ top: 0;
+ bottom: 0;
+
+ background: #444;
+
+ border: 0.125em solid #666;
+ -moz-border-radius: 0.25em;
+ -webkit-border-radius: 0.25em;
+ -khtml-border-radius: 0.25em;
+ border-radius: 0.25em;
+
+ color: white;
+ font-size: 40%;
+ font-weight: lighter;
+ text-align: center;
+ white-space: pre;
+
+ text-shadow: 1px 1px 0 rgba(0, 0, 0, 0.25),
+ 1px -1px 0 rgba(0, 0, 0, 0.25),
+ -1px 1px 0 rgba(0, 0, 0, 0.25),
+ -1px -1px 0 rgba(0, 0, 0, 0.25);
+
+}
+
+.guac-keyboard .guac-keyboard-key:hover {
+ cursor: pointer;
+}
+
+.guac-keyboard .guac-keyboard-key.highlight {
+ background: #666;
+ border-color: #666;
+}
+
+/* Align some keys to the left */
+.guac-keyboard .guac-keyboard-key-caps,
+.guac-keyboard .guac-keyboard-key-enter,
+.guac-keyboard .guac-keyboard-key-tab,
+.guac-keyboard .guac-keyboard-key-lalt,
+.guac-keyboard .guac-keyboard-key-ralt,
+.guac-keyboard .guac-keyboard-key-alt-gr,
+.guac-keyboard .guac-keyboard-key-lctrl,
+.guac-keyboard .guac-keyboard-key-rctrl,
+.guac-keyboard .guac-keyboard-key-lshift,
+.guac-keyboard .guac-keyboard-key-rshift {
+ text-align: left;
+ padding-left: 0.75em;
+}
+
+/* Active shift */
+.guac-keyboard.guac-keyboard-modifier-shift .guac-keyboard-key-rshift,
+.guac-keyboard.guac-keyboard-modifier-shift .guac-keyboard-key-lshift,
+
+/* Active ctrl */
+.guac-keyboard.guac-keyboard-modifier-control .guac-keyboard-key-rctrl,
+.guac-keyboard.guac-keyboard-modifier-control .guac-keyboard-key-lctrl,
+
+/* Active alt */
+.guac-keyboard.guac-keyboard-modifier-alt .guac-keyboard-key-ralt,
+.guac-keyboard.guac-keyboard-modifier-alt .guac-keyboard-key-lalt,
+
+/* Active alt-gr */
+.guac-keyboard.guac-keyboard-modifier-alt-gr .guac-keyboard-key-alt-gr,
+
+/* Active caps */
+.guac-keyboard.guac-keyboard-modifier-caps .guac-keyboard-key-caps,
+
+/* Active super */
+.guac-keyboard.guac-keyboard-modifier-super .guac-keyboard-key-super {
+ background: #882;
+ border-color: #DD4;
+}
+
+.guac-keyboard .guac-keyboard-key.guac-keyboard-pressed {
+ background: #822;
+ border-color: #D44;
+}
+
+.guac-keyboard .guac-keyboard-group {
+ line-height: 0;
+}
+
+.guac-keyboard .guac-keyboard-group.guac-keyboard-alpha,
+.guac-keyboard .guac-keyboard-group.guac-keyboard-movement {
+ display: inline-block;
+ text-align: center;
+ vertical-align: top;
+}
+
+.guac-keyboard .guac-keyboard-group.guac-keyboard-main {
+
+ /* IE10 */
+ display: -ms-flexbox;
+ -ms-flex-align: stretch;
+ -ms-flex-direction: row;
+
+ /* Ancient Mozilla */
+ display: -moz-box;
+ -moz-box-align: stretch;
+ -moz-box-orient: horizontal;
+
+ /* Ancient WebKit */
+ display: -webkit-box;
+ -webkit-box-align: stretch;
+ -webkit-box-orient: horizontal;
+
+ /* Old WebKit */
+ display: -webkit-flex;
+ -webkit-align-items: stretch;
+ -webkit-flex-direction: row;
+
+ /* W3C */
+ display: flex;
+ align-items: stretch;
+ flex-direction: row;
+
+}
+
+.guac-keyboard .guac-keyboard-group.guac-keyboard-movement {
+ -ms-flex: 1 1 auto;
+ -moz-box-flex: 1;
+ -webkit-box-flex: 1;
+ -webkit-flex: 1 1 auto;
+ flex: 1 1 auto;
+}
+
+.guac-keyboard .guac-keyboard-gap {
+ display: inline-block;
+}
+
+/* Hide keycaps requiring modifiers which are NOT currently active. */
+.guac-keyboard:not(.guac-keyboard-modifier-caps)
+.guac-keyboard-cap.guac-keyboard-requires-caps,
+
+.guac-keyboard:not(.guac-keyboard-modifier-shift)
+.guac-keyboard-cap.guac-keyboard-requires-shift,
+
+.guac-keyboard:not(.guac-keyboard-modifier-alt-gr)
+.guac-keyboard-cap.guac-keyboard-requires-alt-gr,
+
+/* Hide keycaps NOT requiring modifiers which ARE currently active, where that
+ modifier is used to determine which cap is displayed for the current key. */
+.guac-keyboard.guac-keyboard-modifier-shift
+.guac-keyboard-key.guac-keyboard-uses-shift
+.guac-keyboard-cap:not(.guac-keyboard-requires-shift),
+
+.guac-keyboard.guac-keyboard-modifier-caps
+.guac-keyboard-key.guac-keyboard-uses-caps
+.guac-keyboard-cap:not(.guac-keyboard-requires-caps),
+
+.guac-keyboard.guac-keyboard-modifier-alt-gr
+.guac-keyboard-key.guac-keyboard-uses-alt-gr
+.guac-keyboard-cap:not(.guac-keyboard-requires-alt-gr) {
+
+ display: none;
+
+}
+
+/* Fade out keys which do not use AltGr if AltGr is active */
+.guac-keyboard.guac-keyboard-modifier-alt-gr
+.guac-keyboard-key:not(.guac-keyboard-uses-alt-gr):not(.guac-keyboard-key-alt-gr) {
+ opacity: 0.5;
+}
\ No newline at end of file