diff --git a/guacamole/src/main/webapp/scripts/client-ui.js b/guacamole/src/main/webapp/scripts/client-ui.js index f59400ac8..a8b9bde90 100644 --- a/guacamole/src/main/webapp/scripts/client-ui.js +++ b/guacamole/src/main/webapp/scripts/client-ui.js @@ -12,7 +12,7 @@ GuacUI.Client = { /** * The normal default Guacamole client UI mode */ - "INTERACTIVE" : 0, + "INTERACTIVE" : 0, /** * Same as INTERACTIVE except with visible on-screen keyboard. @@ -46,388 +46,15 @@ GuacUI.Client = { "viewport" : document.getElementById("viewportClone"), "display" : document.getElementById("display"), - "client" : null, - /* Expected Input Rectangle */ - "expected_input_x" : 0, - "expected_input_y" : 0, - "expected_input_width" : 1, - "expected_input_height" : 1 + "expected_input_x" : 0, + "expected_input_y" : 0, + "expected_input_width" : 1, + "expected_input_height" : 1, -}; - -// Tie UI events / behavior to a specific Guacamole client -GuacUI.Client.attach = function(guac) { - - GuacUI.client = guac; - - var title_prefix = null; - var connection_name = "Guacamole"; - - var guac_display = guac.getDisplay(); - - // Set document title appropriately, based on prefix and connection name - function updateTitle() { - - // Use title prefix if present - if (title_prefix) { - - document.title = title_prefix; - - // Include connection name, if present - if (connection_name) - document.title += " " + connection_name; - - } - - // Otherwise, just set to connection name - else if (connection_name) - document.title = connection_name; - - } - - guac_display.onclick = function(e) { - e.preventDefault(); - return false; - }; - - // Mouse - var mouse = new Guacamole.Mouse(guac_display); - var touch = new Guacamole.Mouse.Touchpad(guac_display); - touch.onmousedown = touch.onmouseup = touch.onmousemove = - mouse.onmousedown = mouse.onmouseup = mouse.onmousemove = - function(mouseState) { - - // Determine mouse position within view - var mouse_view_x = mouseState.x + guac_display.offsetLeft - window.pageXOffset; - var mouse_view_y = mouseState.y + guac_display.offsetTop - window.pageYOffset; - - // Determine viewport dimensioins - var view_width = GuacUI.Client.viewport.offsetWidth; - var view_height = GuacUI.Client.viewport.offsetHeight; - - // Determine scroll amounts based on mouse position relative to document - - var scroll_amount_x; - if (mouse_view_x > view_width) - scroll_amount_x = mouse_view_x - view_width; - else if (mouse_view_x < 0) - scroll_amount_x = mouse_view_x; - else - scroll_amount_x = 0; - - var scroll_amount_y; - if (mouse_view_y > view_height) - scroll_amount_y = mouse_view_y - view_height; - else if (mouse_view_y < 0) - scroll_amount_y = mouse_view_y; - else - scroll_amount_y = 0; - - // Scroll (if necessary) to keep mouse on screen. - window.scrollBy(scroll_amount_x, scroll_amount_y); - - // Scale event by current scale - var scaledState = new Guacamole.Mouse.State( - mouseState.x / guac.getScale(), - mouseState.y / guac.getScale(), - mouseState.left, - mouseState.middle, - mouseState.right, - mouseState.up, - mouseState.down); - - // Send mouse event - guac.sendMouseState(scaledState); - - }; - - // Keyboard - var keyboard = new Guacamole.Keyboard(document); - var show_keyboard_gesture_possible = true; - - keyboard.onkeydown = function (keysym) { - guac.sendKeyEvent(1, keysym); - - // If key is NOT one of the expected keys, gesture not possible - if (keysym != 0xFFE3 && keysym != 0xFFE9 && keysym != 0xFFE1) - show_keyboard_gesture_possible = false; - - }; - - keyboard.onkeyup = function (keysym) { - guac.sendKeyEvent(0, keysym); - - // If lifting up on shift, toggle keyboard if rest of gesture - // conditions satisfied - if (show_keyboard_gesture_possible && keysym == 0xFFE1) { - if (keyboard.pressed[0xFFE3] && keyboard.pressed[0xFFE9]) { - - // If in INTERACTIVE mode, switch to OSK - if (GuacUI.StateManager.getState() == GuacUI.Client.states.INTERACTIVE) - GuacUI.StateManager.setState(GuacUI.Client.states.OSK); - - // If in OSK mode, switch to INTERACTIVE - else if (GuacUI.StateManager.getState() == GuacUI.Client.states.OSK) - GuacUI.StateManager.setState(GuacUI.Client.states.INTERACTIVE); - - } - } - - // Detect if no keys are pressed - var reset_gesture = true; - for (var pressed in keyboard.pressed) { - reset_gesture = false; - break; - } - - // Reset gesture state if possible - if (reset_gesture) - show_keyboard_gesture_possible = true; - - }; - - function isTypableCharacter(keysym) { - return (keysym & 0xFFFF00) != 0xFF00; - } - - function updateThumbnail() { - - // Get screenshot - var canvas = guac.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 - ); - - // Save thumbnail to history - var id = decodeURIComponent(window.location.search.substring(4)); - GuacamoleHistory.update(id, thumbnail.toDataURL()); - - } - - function updateDisplayScale() { - - // If auto-fit is enabled, scale display - if (GuacUI.sessionState.getProperty("auto-fit")) { - - // Calculate scale to fit screen - var fit_scale = Math.min( - window.innerWidth / guac.getWidth(), - window.innerHeight / guac.getHeight() - ); - - // Scale client - if (fit_scale != guac.getScale()) - guac.scale(fit_scale); - - } - - // Otherwise, scale to 100% - else if (guac.getScale() != 1.0) - guac.scale(1.0); - - } - - // Handle resize - guac.onresize = function(width, height) { - updateDisplayScale(); - } - - var last_status_notification = null; - function hideStatus() { - if (last_status_notification) - last_status_notification.hide(); - last_status_notification = null; - } - - function showStatus(status) { - hideStatus(); - - last_status_notification = new GuacUI.Client.ModalStatus(status); - last_status_notification.show(); - } - - function showError(status) { - hideStatus(); - - last_status_notification = new GuacUI.Client.ModalStatus(status); - last_status_notification.show(); - } - - // Handle client state change - guac.onstatechange = function(clientState) { - - switch (clientState) { - - // Idle - case 0: - showStatus("Idle."); - title_prefix = "[Idle]"; - break; - - // Connecting - case 1: - showStatus("Connecting..."); - title_prefix = "[Connecting...]"; - break; - - // Connected + waiting - case 2: - showStatus("Connected, waiting for first update..."); - title_prefix = "[Waiting...]"; - break; - - // Connected - case 3: - - hideStatus(); - title_prefix = null; - - // Update clipboard with current data - if (GuacUI.sessionState.getProperty("clipboard")) - guac.setClipboard(GuacUI.sessionState.getProperty("clipboard")); - - // Regularly update screenshot - window.setInterval(updateThumbnail, 1000); - - break; - - // Disconnecting - case 4: - showStatus("Disconnecting..."); - title_prefix = "[Disconnecting...]"; - break; - - // Disconnected - case 5: - showStatus("Disconnected."); - title_prefix = "[Disconnected]"; - break; - - // Unknown status code - default: - showStatus("[UNKNOWN STATUS]"); - - } - - updateTitle(); - }; - - // Name instruction handler - guac.onname = function(name) { - connection_name = name; - updateTitle(); - }; - - // Error handler - guac.onerror = function(error) { - - // Disconnect, if connected - guac.disconnect(); - - // Display error message - showError(error); - - }; - - // Disconnect and update thumbnail on close - window.onunload = function() { - - updateThumbnail(); - guac.disconnect(); - - }; - - // Send size events on resize - window.onresize = function() { - - guac.sendSize(window.innerWidth, window.innerHeight); - updateDisplayScale(); - - }; - - // Server copy handler - guac.onclipboard = function(data) { - GuacUI.sessionState.setProperty("clipboard", data); - }; - - GuacUI.sessionState.onchange = function(old_state, new_state, name) { - if (name == "clipboard") - guac.setClipboard(new_state[name]); - else if (name == "auto-fit") - updateDisplayScale(); - - }; - - var long_press_start_x = 0; - var long_press_start_y = 0; - var longPressTimeout = null; - - GuacUI.Client.startLongPressDetect = function() { - - if (!longPressTimeout) { - - longPressTimeout = window.setTimeout(function() { - longPressTimeout = null; - if (GuacUI.Client.client.getScale() != 1.0) - GuacUI.StateManager.setState(GuacUI.Client.states.MAGNIFIER); - else - GuacUI.StateManager.setState(GuacUI.Client.states.PAN); - }, GuacUI.Client.LONG_PRESS_DETECT_TIMEOUT); - - } - }; - - GuacUI.Client.stopLongPressDetect = function() { - window.clearTimeout(longPressTimeout); - longPressTimeout = null; - }; - - // Detect long-press at bottom of screen - GuacUI.Client.display.addEventListener('touchstart', function(e) { - - // Record touch location - if (e.touches.length == 1) { - var touch = e.touches[0]; - long_press_start_x = touch.screenX; - long_press_start_y = touch.screenY; - } - - // Start detection - GuacUI.Client.startLongPressDetect(); - - }, true); - - // Stop detection if touch moves significantly - GuacUI.Client.display.addEventListener('touchmove', function(e) { - - // If touch distance from start exceeds threshold, cancel long press - var touch = e.touches[0]; - if (Math.abs(touch.screenX - long_press_start_x) >= GuacUI.Client.LONG_PRESS_MOVEMENT_THRESHOLD - || Math.abs(touch.screenY - long_press_start_y) >= GuacUI.Client.LONG_PRESS_MOVEMENT_THRESHOLD) - GuacUI.Client.stopLongPressDetect(); - - }, true); - - // Stop detection if press stops - GuacUI.Client.display.addEventListener('touchend', GuacUI.Client.stopLongPressDetect, true); + "connectionName" : "Guacamole", + "attachedClient" : null }; @@ -497,9 +124,9 @@ GuacUI.Client.Magnifier = function() { // Update contents relative to new position var clip_x = x - / (window.innerWidth - width) * (GuacUI.Client.client.getWidth() - width); + / (window.innerWidth - width) * (GuacUI.Client.attachedClient.getWidth() - width); var clip_y = y - / (window.innerHeight - height) * (GuacUI.Client.client.getHeight() - height); + / (window.innerHeight - height) * (GuacUI.Client.attachedClient.getHeight() - height); magnifier_display.style.WebkitTransform = magnifier_display.style.MozTransform = @@ -523,9 +150,9 @@ GuacUI.Client.Magnifier = function() { this.show = function() { // Copy displayed image - magnifier_display.width = GuacUI.Client.client.getWidth(); - magnifier_display.height = GuacUI.Client.client.getHeight(); - magnifier_context.drawImage(GuacUI.Client.client.flatten(), 0, 0); + magnifier_display.width = GuacUI.Client.attachedClient.getWidth(); + magnifier_display.height = GuacUI.Client.attachedClient.getHeight(); + magnifier_context.drawImage(GuacUI.Client.attachedClient.flatten(), 0, 0); // Show magnifier container document.body.appendChild(magnifier_background); @@ -583,12 +210,12 @@ GuacUI.Client.ZoomedDisplay = function() { var old_scale = null; this.show = function() { - old_scale = GuacUI.Client.client.getScale(); - GuacUI.Client.client.scale(1.0); + old_scale = GuacUI.Client.attachedClient.getScale(); + GuacUI.Client.attachedClient.scale(1.0); }; this.hide = function() { - GuacUI.Client.client.scale(old_scale); + GuacUI.Client.attachedClient.scale(old_scale); }; }; @@ -756,11 +383,11 @@ GuacUI.Client.OnScreenKeyboard = function() { } keyboard.onkeydown = function(keysym) { - GuacUI.Client.client.sendKeyEvent(1, keysym); + GuacUI.Client.attachedClient.sendKeyEvent(1, keysym); }; keyboard.onkeyup = function(keysym) { - GuacUI.Client.client.sendKeyEvent(0, keysym); + GuacUI.Client.attachedClient.sendKeyEvent(0, keysym); }; @@ -844,3 +471,416 @@ GuacUI.Client.ModalStatus = function(text) { GuacUI.Client.ModalStatus.prototype = new GuacUI.Component(); +/** + * Flattens the attached Guacamole.Client, storing the result within the + * connection history. + */ +GuacUI.Client.updateThumbnail = function() { + + // Get screenshot + var canvas = GuacUI.Client.attachedClient.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 + ); + + // Save thumbnail to history + var id = decodeURIComponent(window.location.search.substring(4)); + GuacamoleHistory.update(id, thumbnail.toDataURL()); + +}; + +/** + * Updates the scale of the attached Guacamole.Client based on current window + * size and "auto-fit" setting. + */ +GuacUI.Client.updateDisplayScale = function() { + + // Currently attacched client + var guac = GuacUI.Client.attachedClient; + + // If auto-fit is enabled, scale display + if (GuacUI.sessionState.getProperty("auto-fit")) { + + // Calculate scale to fit screen + var fit_scale = Math.min( + window.innerWidth / guac.getWidth(), + window.innerHeight / guac.getHeight() + ); + + // Scale client + if (fit_scale != guac.getScale()) + guac.scale(fit_scale); + + } + + // Otherwise, scale to 100% + else if (guac.getScale() != 1.0) + guac.scale(1.0); + +}; + +/** + * Hides the currently-visible status overlay, if any. + */ +GuacUI.Client.hideStatus = function() { + if (GuacUI.Client.visibleStatus) + GuacUI.Client.visibleStatus.hide(); + GuacUI.Client.visibleStatus = null; +}; + +/** + * Displays a status overlay with the given text. + */ +GuacUI.Client.showStatus = function(status) { + GuacUI.Client.hideStatus(); + + GuacUI.Client.visibleStatus = new GuacUI.Client.ModalStatus(status); + GuacUI.Client.visibleStatus.show(); +}; + +/** + * Displays an error status overlay with the given text. + */ +GuacUI.Client.showError = function(status) { + GuacUI.Client.hideStatus(); + + GuacUI.Client.visibleStatus = new GuacUI.Client.ModalStatus(status); + GuacUI.Client.visibleStatus.show(); +} + +/** + * Attaches a Guacamole.Client to the client UI, such that Guacamole events + * affect the UI, and local events affect the Guacamole.Client. + * + * @param {Guacamole.Client} guac The Guacamole.Client to attach to the UI. + */ +GuacUI.Client.attach = function(guac) { + + // Store attached client + GuacUI.Client.attachedClient = guac; + + // Get display element + var guac_display = guac.getDisplay(); + + /* + * Update the scale of the display when the client display size changes. + */ + + guac.onresize = function(width, height) { + updateDisplayScale(); + } + + /* + * Update UI when the state of the Guacamole.Client changes. + */ + + guac.onstatechange = function(clientState) { + + var title_prefix; + + switch (clientState) { + + // Idle + case 0: + showStatus("Idle."); + title_prefix = "[Idle]"; + break; + + // Connecting + case 1: + showStatus("Connecting..."); + title_prefix = "[Connecting...]"; + break; + + // Connected + waiting + case 2: + showStatus("Connected, waiting for first update..."); + title_prefix = "[Waiting...]"; + break; + + // Connected + case 3: + + hideStatus(); + title_prefix = null; + + // Update clipboard with current data + if (GuacUI.sessionState.getProperty("clipboard")) + guac.setClipboard(GuacUI.sessionState.getProperty("clipboard")); + + break; + + // Disconnecting + case 4: + showStatus("Disconnecting..."); + title_prefix = "[Disconnecting...]"; + break; + + // Disconnected + case 5: + showStatus("Disconnected."); + title_prefix = "[Disconnected]"; + break; + + // Unknown status code + default: + showStatus("[UNKNOWN STATUS]"); + + } + + document.title = title_prefix + " " + GuacUI.Client.connectionName; + + }; + + /* + * Change UI to reflect the connection name + */ + + guac.onname = function(name) { + GuacUI.Client.connectionName = name; + }; + + /* + * Disconnect and display an error message when the Guacamole.Client + * receives an error. + */ + + guac.onerror = function(error) { + + // Disconnect, if connected + guac.disconnect(); + + // Display error message + showError(error); + + }; + + // Server copy handler + guac.onclipboard = function(data) { + GuacUI.sessionState.setProperty("clipboard", data); + }; + + /* + * Do nothing when the display element is clicked on. + */ + + guac_display.onclick = function(e) { + e.preventDefault(); + return false; + }; + + /* + * Handle mouse and touch events relative to the display element. + */ + + // Mouse + var mouse = new Guacamole.Mouse(guac_display); + var touch = new Guacamole.Mouse.Touchpad(document); + touch.onmousedown = touch.onmouseup = touch.onmousemove = + mouse.onmousedown = mouse.onmouseup = mouse.onmousemove = + function(mouseState) { + + // Determine mouse position within view + var mouse_view_x = mouseState.x + guac_display.offsetLeft - window.pageXOffset; + var mouse_view_y = mouseState.y + guac_display.offsetTop - window.pageYOffset; + + // Determine viewport dimensioins + var view_width = GuacUI.Client.viewport.offsetWidth; + var view_height = GuacUI.Client.viewport.offsetHeight; + + // Determine scroll amounts based on mouse position relative to document + + var scroll_amount_x; + if (mouse_view_x > view_width) + scroll_amount_x = mouse_view_x - view_width; + else if (mouse_view_x < 0) + scroll_amount_x = mouse_view_x; + else + scroll_amount_x = 0; + + var scroll_amount_y; + if (mouse_view_y > view_height) + scroll_amount_y = mouse_view_y - view_height; + else if (mouse_view_y < 0) + scroll_amount_y = mouse_view_y; + else + scroll_amount_y = 0; + + // Scroll (if necessary) to keep mouse on screen. + window.scrollBy(scroll_amount_x, scroll_amount_y); + + // Scale event by current scale + var scaledState = new Guacamole.Mouse.State( + mouseState.x / guac.getScale(), + mouseState.y / guac.getScale(), + mouseState.left, + mouseState.middle, + mouseState.right, + mouseState.up, + mouseState.down); + + // Send mouse event + guac.sendMouseState(scaledState); + + }; + + /* + * Route document-level keyboard events to the client. + */ + + + var keyboard = new Guacamole.Keyboard(document); + var show_keyboard_gesture_possible = true; + + keyboard.onkeydown = function (keysym) { + guac.sendKeyEvent(1, keysym); + + // If key is NOT one of the expected keys, gesture not possible + if (keysym != 0xFFE3 && keysym != 0xFFE9 && keysym != 0xFFE1) + show_keyboard_gesture_possible = false; + + }; + + keyboard.onkeyup = function (keysym) { + guac.sendKeyEvent(0, keysym); + + // If lifting up on shift, toggle keyboard if rest of gesture + // conditions satisfied + if (show_keyboard_gesture_possible && keysym == 0xFFE1) { + if (keyboard.pressed[0xFFE3] && keyboard.pressed[0xFFE9]) { + + // If in INTERACTIVE mode, switch to OSK + if (GuacUI.StateManager.getState() == GuacUI.Client.states.INTERACTIVE) + GuacUI.StateManager.setState(GuacUI.Client.states.OSK); + + // If in OSK mode, switch to INTERACTIVE + else if (GuacUI.StateManager.getState() == GuacUI.Client.states.OSK) + GuacUI.StateManager.setState(GuacUI.Client.states.INTERACTIVE); + + } + } + + // Detect if no keys are pressed + var reset_gesture = true; + for (var pressed in keyboard.pressed) { + reset_gesture = false; + break; + } + + // Reset gesture state if possible + if (reset_gesture) + show_keyboard_gesture_possible = true; + + }; + + var thumbnail_update_interval; + + window.onblur = function() { + + // Regularly update screenshot if window not visible + thumbnail_update_interval = window.setInterval(updateThumbnail, 1000); + + }; + + window.onfocus = function() { + window.clearInterval(thumbnail_update_interval); + }; + + /* + * Disconnect and update thumbnail on close + */ + window.onunload = function() { + + updateThumbnail(); + guac.disconnect(); + + }; + + /* + * Send size events on resize + */ + window.onresize = function() { + + guac.sendSize(window.innerWidth, window.innerHeight); + updateDisplayScale(); + + }; + + GuacUI.sessionState.onchange = function(old_state, new_state, name) { + if (name == "clipboard") + guac.setClipboard(new_state[name]); + else if (name == "auto-fit") + updateDisplayScale(); + }; + + var long_press_start_x = 0; + var long_press_start_y = 0; + var longPressTimeout = null; + + GuacUI.Client.startLongPressDetect = function() { + + if (!longPressTimeout) { + + longPressTimeout = window.setTimeout(function() { + longPressTimeout = null; + if (GuacUI.Client.attachedClient.getScale() != 1.0) + GuacUI.StateManager.setState(GuacUI.Client.states.MAGNIFIER); + else + GuacUI.StateManager.setState(GuacUI.Client.states.PAN); + }, GuacUI.Client.LONG_PRESS_DETECT_TIMEOUT); + + } + }; + + GuacUI.Client.stopLongPressDetect = function() { + window.clearTimeout(longPressTimeout); + longPressTimeout = null; + }; + + // Detect long-press at bottom of screen + GuacUI.Client.display.addEventListener('touchstart', function(e) { + + // Record touch location + if (e.touches.length == 1) { + var touch = e.touches[0]; + long_press_start_x = touch.screenX; + long_press_start_y = touch.screenY; + } + + // Start detection + GuacUI.Client.startLongPressDetect(); + + }, true); + + // Stop detection if touch moves significantly + GuacUI.Client.display.addEventListener('touchmove', function(e) { + + // If touch distance from start exceeds threshold, cancel long press + var touch = e.touches[0]; + if (Math.abs(touch.screenX - long_press_start_x) >= GuacUI.Client.LONG_PRESS_MOVEMENT_THRESHOLD + || Math.abs(touch.screenY - long_press_start_y) >= GuacUI.Client.LONG_PRESS_MOVEMENT_THRESHOLD) + GuacUI.Client.stopLongPressDetect(); + + }, true); + + // Stop detection if press stops + GuacUI.Client.display.addEventListener('touchend', GuacUI.Client.stopLongPressDetect, true); + +}; +