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