diff --git a/guacamole-common-js/src/main/resources/guacamole.js b/guacamole-common-js/src/main/resources/guacamole.js index ead548450..db7ad9b6b 100644 --- a/guacamole-common-js/src/main/resources/guacamole.js +++ b/guacamole-common-js/src/main/resources/guacamole.js @@ -16,7 +16,22 @@ * You should have received a copy of the GNU Affero General Public License */ -function GuacamoleClient(display, tunnel) { +// Guacamole namespace +var Guacamole = Guacamole || {}; + +/** + * Guacamole protocol client. Given a display element and {@link Guacamole.Tunnel}, + * automatically handles incoming and outgoing Guacamole instructions via the + * provided tunnel, updating the display using one or more canvas elements. + * + * @constructor + * @param {Element} display The display element to add canvas elements to. + * @param {Guacamole.Tunnel} tunnel The tunnel to use to send and receive + * Guacamole instructions. + */ +Guacamole.Client = function(display, tunnel) { + + var guac_client = this; var STATE_IDLE = 0; var STATE_CONNECTING = 1; @@ -26,9 +41,13 @@ function GuacamoleClient(display, tunnel) { var STATE_DISCONNECTED = 5; var currentState = STATE_IDLE; - var stateChangeHandler = null; - tunnel.setInstructionHandler(doInstruction); + tunnel.oninstruction = doInstruction; + + tunnel.onerror = function(message) { + if (guac_client.onerror) + guac_client.onerror(message); + }; // Display must be relatively positioned for mouse to be handled properly display.style.position = "relative"; @@ -36,15 +55,11 @@ function GuacamoleClient(display, tunnel) { function setState(state) { if (state != currentState) { currentState = state; - if (stateChangeHandler) - stateChangeHandler(currentState); + if (guac_client.onstatechange) + guac_client.onstatechange(currentState); } } - this.setOnStateChangeHandler = function(handler) { - stateChangeHandler = handler; - }; - function isConnected() { return currentState == STATE_CONNECTED || currentState == STATE_WAITING; @@ -54,7 +69,6 @@ function GuacamoleClient(display, tunnel) { var cursorHotspotX = 0; var cursorHotspotY = 0; - // FIXME: Make object. Clean up. var cursorRectX = 0; var cursorRectY = 0; var cursorRectW = 0; @@ -83,7 +97,7 @@ function GuacamoleClient(display, tunnel) { cursor.drawImage(cursorRectX, cursorRectY, cursorImage); } - this.sendKeyEvent = function(pressed, keysym) { + guac_client.sendKeyEvent = function(pressed, keysym) { // Do not send requests if not connected if (!isConnected()) return; @@ -91,7 +105,7 @@ function GuacamoleClient(display, tunnel) { tunnel.sendMessage("key:" + keysym + "," + pressed + ";"); }; - this.sendMouseState = function(mouseState) { + guac_client.sendMouseState = function(mouseState) { // Do not send requests if not connected if (!isConnected()) @@ -100,24 +114,24 @@ function GuacamoleClient(display, tunnel) { // Draw client-side cursor if (cursorImage != null) { redrawCursor( - mouseState.getX(), - mouseState.getY() + mouseState.x, + mouseState.y ); } // Build mask var buttonMask = 0; - if (mouseState.getLeft()) buttonMask |= 1; - if (mouseState.getMiddle()) buttonMask |= 2; - if (mouseState.getRight()) buttonMask |= 4; - if (mouseState.getUp()) buttonMask |= 8; - if (mouseState.getDown()) buttonMask |= 16; + if (mouseState.left) buttonMask |= 1; + if (mouseState.middle) buttonMask |= 2; + if (mouseState.right) buttonMask |= 4; + if (mouseState.up) buttonMask |= 8; + if (mouseState.down) buttonMask |= 16; // Send message - tunnel.sendMessage("mouse:" + mouseState.getX() + "," + mouseState.getY() + "," + buttonMask + ";"); + tunnel.sendMessage("mouse:" + mouseState.x + "," + mouseState.y + "," + buttonMask + ";"); }; - this.setClipboard = function(data) { + guac_client.setClipboard = function(data) { // Do not send requests if not connected if (!isConnected()) @@ -127,21 +141,10 @@ function GuacamoleClient(display, tunnel) { }; // Handlers - - var nameHandler = null; - this.setNameHandler = function(handler) { - nameHandler = handler; - } - - var errorHandler = null; - this.setErrorHandler = function(handler) { - errorHandler = handler; - }; - - var clipboardHandler = null; - this.setClipboardHandler = function(handler) { - clipboardHandler = handler; - }; + guac_client.onstatechange = null; + guac_client.onname = null; + guac_client.onerror = null; + guac_client.onclipboard = null; // Layers var displayWidth = 0; @@ -151,7 +154,7 @@ function GuacamoleClient(display, tunnel) { var buffers = new Array(); var cursor = null; - this.getLayers = function() { + guac_client.getLayers = function() { return layers; }; @@ -165,8 +168,8 @@ function GuacamoleClient(display, tunnel) { // Create buffer if necessary if (buffer == null) { - buffer = new Layer(0, 0); - buffer.setAutosize(1); + buffer = new Guacamole.Layer(0, 0); + buffer.autosize = 1; buffers[index] = buffer; } @@ -180,7 +183,14 @@ function GuacamoleClient(display, tunnel) { if (layer == null) { // Add new layer - layer = new Layer(displayWidth, displayHeight); + layer = new Guacamole.Layer(displayWidth, displayHeight); + + // Set layer position + var canvas = layer.getCanvas(); + canvas.style.position = "absolute"; + canvas.style.left = "0px"; + canvas.style.top = "0px"; + layers[index] = layer; // (Re)-add existing layers in order @@ -189,18 +199,18 @@ function GuacamoleClient(display, tunnel) { // If already present, remove if (layers[i].parentNode === display) - display.removeChild(layers[i]); + display.removeChild(layers[i].getCanvas()); // Add to end - display.appendChild(layers[i]); + display.appendChild(layers[i].getCanvas()); } } // Add cursor layer last if (cursor != null) { if (cursor.parentNode === display) - display.removeChild(cursor); - display.appendChild(cursor); + display.removeChild(cursor.getCanvas()); + display.appendChild(cursor.getCanvas()); } } @@ -217,16 +227,16 @@ function GuacamoleClient(display, tunnel) { var instructionHandlers = { "error": function(parameters) { - if (errorHandler) errorHandler(unescapeGuacamoleString(parameters[0])); + if (guac_client.onerror) guac_client.onerror(unescapeGuacamoleString(parameters[0])); disconnect(); }, "name": function(parameters) { - if (nameHandler) nameHandler(unescapeGuacamoleString(parameters[0])); + if (guac_client.onname) guac_client.onname(unescapeGuacamoleString(parameters[0])); }, "clipboard": function(parameters) { - if (clipboardHandler) clipboardHandler(unescapeGuacamoleString(parameters[0])); + if (guac_client.onclipboard) guac_client.onclipboard(unescapeGuacamoleString(parameters[0])); }, "size": function(parameters) { @@ -292,6 +302,40 @@ function GuacamoleClient(display, tunnel) { }, + "rect": function(parameters) { + + var channelMask = parseInt(parameters[0]); + var layer = getLayer(parseInt(parameters[1])); + var x = parseInt(parameters[2]); + var y = parseInt(parameters[3]); + var w = parseInt(parameters[4]); + var h = parseInt(parameters[5]); + var r = parseInt(parameters[6]); + var g = parseInt(parameters[7]); + var b = parseInt(parameters[8]); + var a = parseInt(parameters[9]); + + layer.setChannelMask(channelMask); + + layer.drawRect( + x, y, w, h, + r, g, b, a + ); + + }, + + "clip": function(parameters) { + + var layer = getLayer(parseInt(parameters[0])); + var x = parseInt(parameters[1]); + var y = parseInt(parameters[2]); + var w = parseInt(parameters[3]); + var h = parseInt(parameters[4]); + + layer.clipRect(x, y, w, h); + + }, + "cursor": function(parameters) { var x = parseInt(parameters[0]); @@ -299,8 +343,14 @@ function GuacamoleClient(display, tunnel) { var data = parameters[2]; if (cursor == null) { - cursor = new Layer(displayWidth, displayHeight); - display.appendChild(cursor); + cursor = new Guacamole.Layer(displayWidth, displayHeight); + + var canvas = cursor.getCanvas(); + canvas.style.position = "absolute"; + canvas.style.left = "0px"; + canvas.style.top = "0px"; + + display.appendChild(canvas); } // Start cursor image load @@ -354,7 +404,7 @@ function GuacamoleClient(display, tunnel) { if (layersToSync == 0) tunnel.sendMessage("sync:" + timestamp + ";"); - }, + } }; @@ -433,8 +483,8 @@ function GuacamoleClient(display, tunnel) { } - this.disconnect = disconnect; - this.connect = function(data) { + guac_client.disconnect = disconnect; + guac_client.connect = function(data) { setState(STATE_CONNECTING); @@ -449,7 +499,7 @@ function GuacamoleClient(display, tunnel) { setState(STATE_WAITING); }; - this.escapeGuacamoleString = escapeGuacamoleString; - this.unescapeGuacamoleString = unescapeGuacamoleString; + guac_client.escapeGuacamoleString = escapeGuacamoleString; + guac_client.unescapeGuacamoleString = unescapeGuacamoleString; } diff --git a/guacamole-common-js/src/main/resources/keyboard.js b/guacamole-common-js/src/main/resources/keyboard.js index b5e6e935c..3dfc31a16 100644 --- a/guacamole-common-js/src/main/resources/keyboard.js +++ b/guacamole-common-js/src/main/resources/keyboard.js @@ -17,20 +17,105 @@ * along with this program. If not, see . */ -function GuacamoleKeyboard(element) { +// Guacamole namespace +var Guacamole = Guacamole || {}; - /*****************************************/ - /*** Keyboard Handler ***/ - /*****************************************/ +/** + * Provides cross-browser and cross-keyboard keyboard for a specific element. + * Browser and keyboard layout variation is abstracted away, providing events + * which represent keys as their corresponding X11 keysym. + * + * @constructor + * @param {Element} element The Element to use to provide keyboard events. + */ +Guacamole.Keyboard = function(element) { + + /** + * Reference to this Guacamole.Keyboard. + * @private + */ + var guac_keyboard = this; + + /** + * Fired whenever the user presses a key with the element associated + * with this Guacamole.Keyboard in focus. + * + * @event + * @param {Number} keysym The keysym of the key being pressed. + */ + this.onkeydown = null; + + /** + * Fired whenever the user releases a key with the element associated + * with this Guacamole.Keyboard in focus. + * + * @event + * @param {Number} keysym The keysym of the key being released. + */ + this.onkeyup = null; + + /** + * Map of known JavaScript keycodes which do not map to typable characters + * to their unshifted X11 keysym equivalents. + * @private + */ + var unshiftedKeySym = { + 8: 0xFF08, // backspace + 9: 0xFF09, // tab + 13: 0xFF0D, // enter + 16: 0xFFE1, // shift + 17: 0xFFE3, // ctrl + 18: 0xFFE9, // alt + 19: 0xFF13, // pause/break + 20: 0xFFE5, // caps lock + 27: 0xFF1B, // escape + 33: 0xFF55, // page up + 34: 0xFF56, // page down + 35: 0xFF57, // end + 36: 0xFF50, // home + 37: 0xFF51, // left arrow + 38: 0xFF52, // up arrow + 39: 0xFF53, // right arrow + 40: 0xFF54, // down arrow + 45: 0xFF63, // insert + 46: 0xFFFF, // delete + 91: 0xFFEB, // left window key (super_l) + 92: 0xFF67, // right window key (menu key?) + 93: null, // select key + 112: 0xFFBE, // f1 + 113: 0xFFBF, // f2 + 114: 0xFFC0, // f3 + 115: 0xFFC1, // f4 + 116: 0xFFC2, // f5 + 117: 0xFFC3, // f6 + 118: 0xFFC4, // f7 + 119: 0xFFC5, // f8 + 120: 0xFFC6, // f9 + 121: 0xFFC7, // f10 + 122: 0xFFC8, // f11 + 123: 0xFFC9, // f12 + 144: 0xFF7F, // num lock + 145: 0xFF14 // scroll lock + }; + + /** + * Map of known JavaScript keycodes which do not map to typable characters + * to their shifted X11 keysym equivalents. Keycodes must only be listed + * here if their shifted X11 keysym equivalents differ from their unshifted + * equivalents. + * @private + */ + var shiftedKeySym = { + 18: 0xFFE7 // alt + }; // Single key state/modifier buffer - var modShift = 0; - var modCtrl = 0; - var modAlt = 0; + var modShift = false; + var modCtrl = false; + var modAlt = false; var keydownChar = new Array(); - // ID of routine repeating keystrokes. -1 = not repeating. var repeatKeyTimeoutId = -1; var repeatKeyIntervalId = -1; @@ -52,27 +137,27 @@ function GuacamoleKeyboard(element) { function getKeySymFromKeyIdentifier(shifted, keyIdentifier) { - var unicodePrefixLocation = keyIdentifier.indexOf("U+"); - if (unicodePrefixLocation >= 0) { + var unicodePrefixLocation = keyIdentifier.indexOf("U+"); + if (unicodePrefixLocation >= 0) { - var hex = keyIdentifier.substring(unicodePrefixLocation+2); - var codepoint = parseInt(hex, 16); - var typedCharacter; + var hex = keyIdentifier.substring(unicodePrefixLocation+2); + var codepoint = parseInt(hex, 16); + var typedCharacter; - // Convert case if shifted - if (shifted == 0) - typedCharacter = String.fromCharCode(codepoint).toLowerCase(); - else - typedCharacter = String.fromCharCode(codepoint).toUpperCase(); + // Convert case if shifted + if (shifted == 0) + typedCharacter = String.fromCharCode(codepoint).toLowerCase(); + else + typedCharacter = String.fromCharCode(codepoint).toUpperCase(); - // Get codepoint - codepoint = typedCharacter.charCodeAt(0); + // Get codepoint + codepoint = typedCharacter.charCodeAt(0); - return getKeySymFromCharCode(codepoint); + return getKeySymFromCharCode(codepoint); - } + } - return null; + return null; } @@ -91,7 +176,7 @@ function GuacamoleKeyboard(element) { function getKeySymFromKeyCode(keyCode) { var keysym = null; - if (modShift == 0) keysym = unshiftedKeySym[keyCode]; + if (!modShift) keysym = unshiftedKeySym[keyCode]; else { keysym = shiftedKeySym[keyCode]; if (keysym == null) keysym = unshiftedKeySym[keyCode]; @@ -104,14 +189,14 @@ function GuacamoleKeyboard(element) { // Sends a single keystroke over the network function sendKeyPressed(keysym) { - if (keysym != null && keyPressedHandler) - keyPressedHandler(keysym); + if (keysym != null && guac_keyboard.onkeydown) + guac_keyboard.onkeydown(keysym); } // Sends a single keystroke over the network function sendKeyReleased(keysym) { - if (keysym != null) - keyReleasedHandler(keysym); + if (keysym != null && guac_keyboard.onkeyup) + guac_keyboard.onkeyup(keysym); } @@ -125,7 +210,7 @@ function GuacamoleKeyboard(element) { element.onkeydown = function(e) { // Only intercept if handler set - if (!keyPressedHandler) return true; + if (!guac_keyboard.onkeydown) return true; var keynum; if (window.event) keynum = window.event.keyCode; @@ -133,11 +218,11 @@ function GuacamoleKeyboard(element) { // Ctrl/Alt/Shift if (keynum == 16) - modShift = 1; + modShift = true; else if (keynum == 17) - modCtrl = 1; + modCtrl = true; else if (keynum == 18) - modAlt = 1; + modAlt = true; var keysym = getKeySymFromKeyCode(keynum); if (keysym) { @@ -146,7 +231,7 @@ function GuacamoleKeyboard(element) { } // If modifier keys are held down, and we have keyIdentifier - else if ((modCtrl == 1 || modAlt == 1) && e.keyIdentifier) { + else if ((modCtrl || modAlt) && e.keyIdentifier) { // Get keysym from keyIdentifier keysym = getKeySymFromKeyIdentifier(modShift, e.keyIdentifier); @@ -183,13 +268,15 @@ function GuacamoleKeyboard(element) { return false; } + return true; + }; // When key pressed element.onkeypress = function(e) { // Only intercept if handler set - if (!keyPressedHandler) return true; + if (!guac_keyboard.onkeydown) return true; if (keySymSource != KEYPRESS) return false; @@ -224,7 +311,7 @@ function GuacamoleKeyboard(element) { element.onkeyup = function(e) { // Only intercept if handler set - if (!keyReleasedHandler) return true; + if (!guac_keyboard.onkeyup) return true; var keynum; if (window.event) keynum = window.event.keyCode; @@ -232,11 +319,11 @@ function GuacamoleKeyboard(element) { // Ctrl/Alt/Shift if (keynum == 16) - modShift = 0; + modShift = false; else if (keynum == 17) - modCtrl = 0; + modCtrl = false; else if (keynum == 18) - modAlt = 0; + modAlt = false; else stopRepeat(); @@ -253,18 +340,10 @@ function GuacamoleKeyboard(element) { }; // When focus is lost, clear modifiers. - var docOnblur = element.onblur; element.onblur = function() { - modAlt = 0; - modCtrl = 0; - modShift = 0; - if (docOnblur != null) docOnblur(); + modAlt = false; + modCtrl = false; + modShift = false; }; - var keyPressedHandler = null; - var keyReleasedHandler = null; - - this.setKeyPressedHandler = function(kh) { keyPressedHandler = kh; }; - this.setKeyReleasedHandler = function(kh) { keyReleasedHandler = kh; }; - -} +}; diff --git a/guacamole-common-js/src/main/resources/keymap.js b/guacamole-common-js/src/main/resources/keymap.js deleted file mode 100644 index 016bf5939..000000000 --- a/guacamole-common-js/src/main/resources/keymap.js +++ /dev/null @@ -1,72 +0,0 @@ - -/* - * Guacamole - Clientless Remote Desktop - * Copyright (C) 2010 Michael Jumper - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - - -// Keymap - -var unshiftedKeySym = new Array(); -unshiftedKeySym[8] = 0xFF08; // backspace -unshiftedKeySym[9] = 0xFF09; // tab -unshiftedKeySym[13] = 0xFF0D; // enter -unshiftedKeySym[16] = 0xFFE1; // shift -unshiftedKeySym[17] = 0xFFE3; // ctrl -unshiftedKeySym[18] = 0xFFE9; // alt -unshiftedKeySym[19] = 0xFF13; // pause/break -unshiftedKeySym[20] = 0xFFE5; // caps lock -unshiftedKeySym[27] = 0xFF1B; // escape -unshiftedKeySym[33] = 0xFF55; // page up -unshiftedKeySym[34] = 0xFF56; // page down -unshiftedKeySym[35] = 0xFF57; // end -unshiftedKeySym[36] = 0xFF50; // home -unshiftedKeySym[37] = 0xFF51; // left arrow -unshiftedKeySym[38] = 0xFF52; // up arrow -unshiftedKeySym[39] = 0xFF53; // right arrow -unshiftedKeySym[40] = 0xFF54; // down arrow -unshiftedKeySym[45] = 0xFF63; // insert -unshiftedKeySym[46] = 0xFFFF; // delete -unshiftedKeySym[91] = 0xFFEB; // left window key (super_l) -unshiftedKeySym[92] = 0xFF67; // right window key (menu key?) -unshiftedKeySym[93] = null; // select key -unshiftedKeySym[112] = 0xFFBE; // f1 -unshiftedKeySym[113] = 0xFFBF; // f2 -unshiftedKeySym[114] = 0xFFC0; // f3 -unshiftedKeySym[115] = 0xFFC1; // f4 -unshiftedKeySym[116] = 0xFFC2; // f5 -unshiftedKeySym[117] = 0xFFC3; // f6 -unshiftedKeySym[118] = 0xFFC4; // f7 -unshiftedKeySym[119] = 0xFFC5; // f8 -unshiftedKeySym[120] = 0xFFC6; // f9 -unshiftedKeySym[121] = 0xFFC7; // f10 -unshiftedKeySym[122] = 0xFFC8; // f11 -unshiftedKeySym[123] = 0xFFC9; // f12 -unshiftedKeySym[144] = 0xFF7F; // num lock -unshiftedKeySym[145] = 0xFF14; // scroll lock - -// Shifted versions, IF DIFFERENT FROM UNSHIFTED! -// If any of these are null, the unshifted one will be used. -var shiftedKeySym = new Array(); -shiftedKeySym[18] = 0xFFE7; // alt - -// Constants for keysyms for special keys -var KEYSYM_CTRL = 65507; -var KEYSYM_ALT = 65513; -var KEYSYM_DELETE = 65535; -var KEYSYM_SHIFT = 65505; - - diff --git a/guacamole-common-js/src/main/resources/layer.js b/guacamole-common-js/src/main/resources/layer.js index 6921eb490..3ef8aeba4 100644 --- a/guacamole-common-js/src/main/resources/layer.js +++ b/guacamole-common-js/src/main/resources/layer.js @@ -17,29 +17,136 @@ * along with this program. If not, see . */ -function Layer(width, height) { +// Guacamole namespace +var Guacamole = Guacamole || {}; - // Off-screen buffer +/** + * Abstract ordered drawing surface. Each Layer contains a canvas element and + * provides simple drawing instructions for drawing to that canvas element, + * however unlike the canvas element itself, drawing operations on a Layer are + * guaranteed to run in order, even if such an operation must wait for an image + * to load before completing. + * + * @constructor + * + * @param {Number} width The width of the Layer, in pixels. The canvas element + * backing this Layer will be given this width. + * + * @param {Number} height The height of the Layer, in pixels. The canvas element + * backing this Layer will be given this height. + */ +Guacamole.Layer = function(width, height) { + + /** + * Reference to this Layer. + * @private + */ + var layer = this; + + /** + * The canvas element backing this Layer. + * @private + */ var display = document.createElement("canvas"); + + /** + * The 2D display context of the canvas element backing this Layer. + * @private + */ var displayContext = display.getContext("2d"); + displayContext.save(); + /** + * The queue of all pending Tasks. Tasks will be run in order, with new + * tasks added at the end of the queue and old tasks removed from the + * front of the queue (FIFO). + * @private + */ + var tasks = new Array(); + + /** + * Map of all Guacamole channel masks to HTML5 canvas composite operation + * names. Not all channel mask combinations are currently implemented. + * @private + */ + var compositeOperation = { + /* 0x0 NOT IMPLEMENTED */ + 0x1: "destination-in", + 0x2: "destination-out", + /* 0x3 NOT IMPLEMENTED */ + 0x4: "source-in", + /* 0x5 NOT IMPLEMENTED */ + 0x6: "source-atop", + /* 0x7 NOT IMPLEMENTED */ + 0x8: "source-out", + 0x9: "destination-atop", + 0xA: "xor", + 0xB: "destination-over", + 0xC: "copy", + /* 0xD NOT IMPLEMENTED */ + 0xE: "source-over", + 0xF: "lighter" + }; + + /** + * Resizes the canvas element backing this Layer without testing the + * new size. This function should only be used internally. + * + * @private + * @param {Number} newWidth The new width to assign to this Layer. + * @param {Number} newHeight The new height to assign to this Layer. + */ function resize(newWidth, newHeight) { - display.style.position = "absolute"; - display.style.left = "0px"; - display.style.top = "0px"; + + // Only preserve old data if width/height are both non-zero + var oldData = null; + if (width != 0 && height != 0) { + + // Create canvas and context for holding old data + oldData = document.createElement("canvas"); + oldData.width = width; + oldData.height = height; + + var oldDataContext = oldData.getContext("2d"); + + // Copy image data from current + oldDataContext.drawImage(display, + 0, 0, width, height, + 0, 0, width, height); + + } + + // Resize canvas display.width = newWidth; display.height = newHeight; + // Redraw old data, if any + if (oldData) + displayContext.drawImage(oldData, + 0, 0, width, height, + 0, 0, width, height); + width = newWidth; height = newHeight; } - display.resize = function(newWidth, newHeight) { - if (newWidth != width || newHeight != height) - resize(newWidth, newHeight); - }; - + /** + * Given the X and Y coordinates of the upper-left corner of a rectangle + * and the rectangle's width and height, resize the backing canvas element + * as necessary to ensure that the rectangle fits within the canvas + * element's coordinate space. This function will only make the canvas + * larger. If the rectangle already fits within the canvas element's + * coordinate space, the canvas is left unchanged. + * + * @private + * @param {Number} x The X coordinate of the upper-left corner of the + * rectangle to fit. + * @param {Number} y The Y coordinate of the upper-left corner of the + * rectangle to fit. + * @param {Number} w The width of the the rectangle to fit. + * @param {Number} h The height of the the rectangle to fit. + */ function fitRect(x, y, w, h) { // Calculate bounds @@ -66,176 +173,345 @@ function Layer(width, height) { } - resize(width, height); - - var readyHandler = null; - var updates = new Array(); - var autosize = 0; - - function Update(updateHandler) { - - this.setHandler = function(handler) { - updateHandler = handler; - }; - - this.hasHandler = function() { - return updateHandler != null; - }; - - this.handle = function() { - updateHandler(); - } - + /** + * A container for an task handler. Each operation which must be ordered + * is associated with a Task that goes into a task queue. Tasks in this + * queue are executed in order once their handlers are set, while Tasks + * without handlers block themselves and any following Tasks from running. + * + * @constructor + * @private + * @param {function} taskHandler The function to call when this task + * runs, if any. + */ + function Task(taskHandler) { + + /** + * The handler this Task is associated with, if any. + * + * @type function + */ + this.handler = taskHandler; + } - display.setAutosize = function(flag) { - autosize = flag; - }; - - function reserveJob(handler) { + /** + * If no tasks are pending or running, run the provided handler immediately, + * if any. Otherwise, schedule a task to run immediately after all currently + * running or pending tasks are complete. + * + * @private + * @param {function} handler The function to call when possible, if any. + * @returns {Task} The Task created and added to the queue for future + * running, if any, or null if the handler was run + * immediately and no Task needed to be created. + */ + function scheduleTask(handler) { - // If no pending updates, just call (if available) and exit - if (display.isReady() && handler != null) { + // If no pending tasks, just call (if available) and exit + if (layer.isReady() && handler != null) { handler(); return null; } - // If updates are pending/executing, schedule a pending update + // If tasks are pending/executing, schedule a pending task // and return a reference to it. - var update = new Update(handler); - updates.push(update); - return update; + var task = new Task(handler); + tasks.push(task); + return task; } - function handlePendingUpdates() { + var tasksInProgress = false; - // Draw all pending updates. - var update; - while ((update = updates[0]) != null && update.hasHandler()) { - update.handle(); - updates.shift(); + /** + * Run any Tasks which were pending but are now ready to run and are not + * blocked by other Tasks. + * @private + */ + function handlePendingTasks() { + + if (tasksInProgress) + return; + + tasksInProgress = true; + + // Draw all pending tasks. + var task; + while ((task = tasks[0]) != null && task.handler) { + tasks.shift(); + task.handler(); } - // If done with updates, call ready handler - if (display.isReady() && readyHandler != null) - readyHandler(); + tasksInProgress = false; } - display.isReady = function() { - return updates.length == 0; + /** + * Set to true if this Layer should resize itself to accomodate the + * dimensions of any drawing operation, and false (the default) otherwise. + * + * Note that setting this property takes effect immediately, and thus may + * take effect on operations that were started in the past but have not + * yet completed. If you wish the setting of this flag to only modify + * future operations, you will need to make the setting of this flag an + * operation with sync(). + * + * @example + * // Set autosize to true for all future operations + * layer.sync(function() { + * layer.autosize = true; + * }); + * + * @type Boolean + * @default false + */ + this.autosize = false; + + /** + * Returns the canvas element backing this Layer. + * @returns {Element} The canvas element backing this Layer. + */ + this.getCanvas = function() { + return display; }; - display.setReadyHandler = function(handler) { - readyHandler = handler; + /** + * Returns whether this Layer is ready. A Layer is ready if it has no + * pending operations and no operations in-progress. + * + * @returns {Boolean} true if this Layer is ready, false otherwise. + */ + this.isReady = function() { + return tasks.length == 0; }; + /** + * Changes the size of this Layer to the given width and height. Resizing + * is only attempted if the new size provided is actually different from + * the current size. + * + * @param {Number} newWidth The new width to assign to this Layer. + * @param {Number} newHeight The new height to assign to this Layer. + */ + this.resize = function(newWidth, newHeight) { + scheduleTask(function() { + if (newWidth != width || newHeight != height) + resize(newWidth, newHeight); + }); + }; - display.drawImage = function(x, y, image) { - reserveJob(function() { - if (autosize != 0) fitRect(x, y, image.width, image.height); + /** + * Draws the specified image at the given coordinates. The image specified + * must already be loaded. + * + * @param {Number} x The destination X coordinate. + * @param {Number} y The destination Y coordinate. + * @param {Image} image The image to draw. Note that this is an Image + * object - not a URL. + */ + this.drawImage = function(x, y, image) { + scheduleTask(function() { + if (layer.autosize != 0) fitRect(x, y, image.width, image.height); displayContext.drawImage(image, x, y); }); }; - - display.draw = function(x, y, url) { - var update = reserveJob(null); + /** + * Draws the image at the specified URL at the given coordinates. The image + * will be loaded automatically, and this and any future operations will + * wait for the image to finish loading. + * + * @param {Number} x The destination X coordinate. + * @param {Number} y The destination Y coordinate. + * @param {String} url The URL of the image to draw. + */ + this.draw = function(x, y, url) { + var task = scheduleTask(null); var image = new Image(); image.onload = function() { - update.setHandler(function() { - if (autosize != 0) fitRect(x, y, image.width, image.height); + task.handler = function() { + if (layer.autosize != 0) fitRect(x, y, image.width, image.height); displayContext.drawImage(image, x, y); - }); + }; - // As this update originally had no handler and may have blocked - // other updates, handle any blocked updates. - handlePendingUpdates(); + // As this task originally had no handler and may have blocked + // other tasks, handle any blocked tasks. + handlePendingTasks(); }; image.src = url; }; - // Run arbitrary function as soon as currently pending operations complete. - // Future operations will not block this function from being called (unlike - // the ready handler, which requires no pending updates) - display.sync = function(handler) { - reserveJob(handler); - } + /** + * Run an arbitrary function as soon as currently pending operations + * are complete. + * + * @param {function} handler The function to call once all currently + * pending operations are complete. + */ + this.sync = function(handler) { + scheduleTask(handler); + }; - display.copyRect = function(srcLayer, srcx, srcy, w, h, x, y) { + /** + * Copy a rectangle of image data from one Layer to this Layer. This + * operation will copy exactly the image data that will be drawn once all + * operations of the source Layer that were pending at the time this + * function was called are complete. This operation will not alter the + * size of the source Layer even if its autosize property is set to true. + * + * @param {Guacamole.Layer} srcLayer The Layer to copy image data from. + * @param {Number} srcx The X coordinate of the upper-left corner of the + * rectangle within the source Layer's coordinate + * space to copy data from. + * @param {Number} srcy The Y coordinate of the upper-left corner of the + * rectangle within the source Layer's coordinate + * space to copy data from. + * @param {Number} srcw The width of the rectangle within the source Layer's + * coordinate space to copy data from. + * @param {Number} srch The height of the rectangle within the source + * Layer's coordinate space to copy data from. + * @param {Number} x The destination X coordinate. + * @param {Number} y The destination Y coordinate. + */ + this.copyRect = function(srcLayer, srcx, srcy, srcw, srch, x, y) { function doCopyRect() { - if (autosize != 0) fitRect(x, y, w, h); - displayContext.drawImage(srcLayer, srcx, srcy, w, h, x, y, w, h); + if (layer.autosize != 0) fitRect(x, y, srcw, srch); + displayContext.drawImage(srcLayer.getCanvas(), srcx, srcy, srcw, srch, x, y, srcw, srch); } // If we ARE the source layer, no need to sync. // Syncing would result in deadlock. - if (display === srcLayer) - reserveJob(doCopyRect); + if (layer === srcLayer) + scheduleTask(doCopyRect); // Otherwise synchronize copy operation with source layer else { - var update = reserveJob(null); + var task = scheduleTask(null); srcLayer.sync(function() { - update.setHandler(doCopyRect); + task.handler = doCopyRect; - // As this update originally had no handler and may have blocked - // other updates, handle any blocked updates. - handlePendingUpdates(); + // As this task originally had no handler and may have blocked + // other tasks, handle any blocked tasks. + handlePendingTasks(); }); } }; - display.clearRect = function(x, y, w, h) { - reserveJob(function() { - if (autosize != 0) fitRect(x, y, w, h); + /** + * Clear the specified rectangle of image data. + * + * @param {Number} x The X coordinate of the upper-left corner of the + * rectangle to clear. + * @param {Number} y The Y coordinate of the upper-left corner of the + * rectangle to clear. + * @param {Number} w The width of the rectangle to clear. + * @param {Number} h The height of the rectangle to clear. + */ + this.clearRect = function(x, y, w, h) { + scheduleTask(function() { + if (layer.autosize != 0) fitRect(x, y, w, h); displayContext.clearRect(x, y, w, h); }); }; - display.filter = function(filter) { - reserveJob(function() { + /** + * Fill the specified rectangle of image data with the specified color. + * + * @param {Number} x The X coordinate of the upper-left corner of the + * rectangle to draw. + * @param {Number} y The Y coordinate of the upper-left corner of the + * rectangle to draw. + * @param {Number} w The width of the rectangle to draw. + * @param {Number} h The height of the rectangle to draw. + * @param {Number} r The red component of the color of the rectangle. + * @param {Number} g The green component of the color of the rectangle. + * @param {Number} b The blue component of the color of the rectangle. + * @param {Number} a The alpha component of the color of the rectangle. + */ + this.drawRect = function(x, y, w, h, r, g, b, a) { + scheduleTask(function() { + if (layer.autosize != 0) fitRect(x, y, w, h); + displayContext.fillStyle = "rgba(" + + r + "," + g + "," + b + "," + a / 255 + ")"; + displayContext.fillRect(x, y, w, h); + }); + }; + + /** + * Clip all future drawing operations by the specified rectangle. + * + * @param {Number} x The X coordinate of the upper-left corner of the + * rectangle to use for the clipping region. + * @param {Number} y The Y coordinate of the upper-left corner of the + * rectangle to use for the clipping region. + * @param {Number} w The width of the rectangle to use for the clipping region. + * @param {Number} h The height of the rectangle to use for the clipping region. + */ + this.clipRect = function(x, y, w, h) { + scheduleTask(function() { + + // Clear any current clipping region + displayContext.restore(); + displayContext.save(); + + if (layer.autosize != 0) fitRect(x, y, w, h); + + // Set new clipping region + displayContext.beginPath(); + displayContext.rect(x, y, w, h); + displayContext.clip(); + + }); + }; + + /** + * Provides the given filtering function with a writable snapshot of + * image data and the current width and height of the Layer. + * + * @param {function} filter A function which accepts an array of image + * data (as returned by the canvas element's + * display context's getImageData() function), + * the width of the Layer, and the height of the + * Layer as parameters, in that order. This + * function must accomplish its filtering by + * modifying the given image data array directly. + */ + this.filter = function(filter) { + scheduleTask(function() { var imageData = displayContext.getImageData(0, 0, width, height); filter(imageData.data, width, height); displayContext.putImageData(imageData, 0, 0); }); }; - var compositeOperation = { - /* 0x0 NOT IMPLEMENTED */ - 0x1: "destination-in", - 0x2: "destination-out", - /* 0x3 NOT IMPLEMENTED */ - 0x4: "source-in", - /* 0x5 NOT IMPLEMENTED */ - 0x6: "source-atop", - /* 0x7 NOT IMPLEMENTED */ - 0x8: "source-out", - 0x9: "destination-atop", - 0xA: "xor", - 0xB: "destination-over", - 0xC: "copy", - /* 0xD NOT IMPLEMENTED */ - 0xE: "source-over", - 0xF: "lighter", - }; - - display.setChannelMask = function(mask) { - reserveJob(function() { + /** + * Sets the channel mask for future operations on this Layer. The channel + * mask is a Guacamole-specific compositing operation identifier with a + * single bit representing each of four channels (in order): source image + * where destination transparent, source where destination opaque, + * destination where source transparent, and destination where source + * opaque. + * + * @param {Number} mask The channel mask for future operations on this + * Layer. + */ + this.setChannelMask = function(mask) { + scheduleTask(function() { displayContext.globalCompositeOperation = compositeOperation[mask]; }); }; - return display; - -} + // Initialize canvas dimensions + display.width = width; + display.height = height; +}; diff --git a/guacamole-common-js/src/main/resources/mouse.js b/guacamole-common-js/src/main/resources/mouse.js index 745b50c0b..35a68fafc 100644 --- a/guacamole-common-js/src/main/resources/mouse.js +++ b/guacamole-common-js/src/main/resources/mouse.js @@ -17,60 +17,93 @@ * along with this program. If not, see . */ +// Guacamole namespace +var Guacamole = Guacamole || {}; -function GuacamoleMouse(element) { +/** + * Provides cross-browser mouse events for a given element. The events of + * the given element are automatically populated with handlers that translate + * mouse events into a non-browser-specific event provided by the + * Guacamole.Mouse instance. + * + * Touch event support is planned, but currently only in testing (translate + * touch events into mouse events). + * + * @constructor + * @param {Element} element The Element to use to provide mouse events. + */ +Guacamole.Mouse = function(element) { - /*****************************************/ - /*** Mouse Handler ***/ - /*****************************************/ + /** + * Reference to this Guacamole.Mouse. + * @private + */ + var guac_mouse = this; + /** + * The current mouse state. The properties of this state are updated when + * mouse events fire. This state object is also passed in as a parameter to + * the handler of any mouse events. + * + * @type Guacamole.Mouse.State + */ + this.currentState = new Guacamole.Mouse.State( + 0, 0, + false, false, false, false, false + ); - var mouseIndex = 0; + /** + * Fired whenever the user presses a mouse button down over the element + * associated with this Guacamole.Mouse. + * + * @event + * @param {Guacamole.Mouse.State} state The current mouse state. + */ + this.onmousedown = null; - var mouseLeftButton = 0; - var mouseMiddleButton = 0; - var mouseRightButton = 0; - - var mouseX = 0; - var mouseY = 0; - - var absoluteMouseX = 0; - var absoluteMouseY = 0; - - - function getMouseState(up, down) { - var mouseState = new MouseEvent(mouseX, mouseY, - mouseLeftButton, mouseMiddleButton, mouseRightButton, up, down); - - return mouseState; - } + /** + * Fired whenever the user releases a mouse button down over the element + * associated with this Guacamole.Mouse. + * + * @event + * @param {Guacamole.Mouse.State} state The current mouse state. + */ + this.onmouseup = null; + /** + * Fired whenever the user moves the mouse over the element associated with + * this Guacamole.Mouse. + * + * @event + * @param {Guacamole.Mouse.State} state The current mouse state. + */ + this.onmousemove = null; function moveMouse(pageX, pageY) { - absoluteMouseX = pageX; - absoluteMouseY = pageY; - - mouseX = absoluteMouseX - element.offsetLeft; - mouseY = absoluteMouseY - element.offsetTop; + guac_mouse.currentState.x = pageX - element.offsetLeft; + guac_mouse.currentState.y = pageY - element.offsetTop; // This is all JUST so we can get the mouse position within the element var parent = element.offsetParent; while (parent) { if (parent.offsetLeft && parent.offsetTop) { - mouseX -= parent.offsetLeft; - mouseY -= parent.offsetTop; + guac_mouse.currentState.x -= parent.offsetLeft; + guac_mouse.currentState.y -= parent.offsetTop; } parent = parent.offsetParent; } - movementHandler(getMouseState(0, 0)); + if (guac_mouse.onmousemove) + guac_mouse.onmousemove(guac_mouse.currentState); } // Block context menu so right-click gets sent properly - element.oncontextmenu = function(e) {return false;}; + element.oncontextmenu = function(e) { + return false; + }; element.onmousemove = function(e) { @@ -136,17 +169,19 @@ function GuacamoleMouse(element) { switch (e.button) { case 0: - mouseLeftButton = 1; + guac_mouse.currentState.left = true; break; case 1: - mouseMiddleButton = 1; + guac_mouse.currentState.middle = true; break; case 2: - mouseRightButton = 1; + guac_mouse.currentState.right = true; break; } - buttonPressedHandler(getMouseState(0, 0)); + if (guac_mouse.onmousedown) + guac_mouse.onmousedown(guac_mouse.currentState); + }; @@ -156,17 +191,19 @@ function GuacamoleMouse(element) { switch (e.button) { case 0: - mouseLeftButton = 0; + guac_mouse.currentState.left = false; break; case 1: - mouseMiddleButton = 0; + guac_mouse.currentState.middle = false; break; case 2: - mouseRightButton = 0; + guac_mouse.currentState.right = false; break; } - buttonReleasedHandler(getMouseState(0, 0)); + if (guac_mouse.onmouseup) + guac_mouse.onmouseup(guac_mouse.currentState); + }; element.onmouseout = function(e) { @@ -174,12 +211,16 @@ function GuacamoleMouse(element) { e.stopPropagation(); // Release all buttons - if (mouseLeftButton || mouseMiddleButton || mouseRightButton) { - mouseLeftButton = 0; - mouseMiddleButton = 0; - mouseRightButton = 0; + if (guac_mouse.currentState.left + || guac_mouse.currentState.middle + || guac_mouse.currentState.right) { - buttonReleasedHandler(getMouseState(0, 0)); + guac_mouse.currentState.left = false; + guac_mouse.currentState.middle = false; + guac_mouse.currentState.right = false; + + if (guac_mouse.onmouseup) + guac_mouse.onmouseup(guac_mouse.currentState); } }; @@ -200,14 +241,28 @@ function GuacamoleMouse(element) { // Up if (delta < 0) { - buttonPressedHandler(getMouseState(1, 0)); - buttonReleasedHandler(getMouseState(0, 0)); + if (guac_mouse.onmousedown) { + guac_mouse.currentState.up = true; + guac_mouse.onmousedown(guac_mouse.currentState); + } + + if (guac_mouse.onmouseup) { + guac_mouse.currentState.up = false; + guac_mouse.onmouseup(guac_mouse.currentState); + } } // Down if (delta > 0) { - buttonPressedHandler(getMouseState(0, 1)); - buttonReleasedHandler(getMouseState(0, 0)); + if (guac_mouse.onmousedown) { + guac_mouse.currentState.down = true; + guac_mouse.onmousedown(guac_mouse.currentState); + } + + if (guac_mouse.onmouseup) { + guac_mouse.currentState.down = false; + guac_mouse.onmouseup(guac_mouse.currentState); + } } if (e.preventDefault) @@ -220,53 +275,71 @@ function GuacamoleMouse(element) { element.onmousewheel = function(e) { handleScroll(e); - } - - var buttonPressedHandler = null; - var buttonReleasedHandler = null; - var movementHandler = null; - - this.setButtonPressedHandler = function(mh) {buttonPressedHandler = mh;}; - this.setButtonReleasedHandler = function(mh) {buttonReleasedHandler = mh;}; - this.setMovementHandler = function(mh) {movementHandler = mh;}; - - - this.getX = function() {return mouseX;}; - this.getY = function() {return mouseY;}; - this.getLeftButton = function() {return mouseLeftButton;}; - this.getMiddleButton = function() {return mouseMiddleButton;}; - this.getRightButton = function() {return mouseRightButton;}; - -} - -function MouseEvent(x, y, left, middle, right, up, down) { - - this.getX = function() { - return x; }; - this.getY = function() { - return y; - }; +}; - this.getLeft = function() { - return left; - }; +/** + * Simple container for properties describing the state of a mouse. + * + * @constructor + * @param {Number} x The X position of the mouse pointer in pixels. + * @param {Number} y The Y position of the mouse pointer in pixels. + * @param {Boolean} left Whether the left mouse button is pressed. + * @param {Boolean} middle Whether the middle mouse button is pressed. + * @param {Boolean} right Whether the right mouse button is pressed. + * @param {Boolean} up Whether the up mouse button is pressed (the fourth + * button, usually part of a scroll wheel). + * @param {Boolean} down Whether the down mouse button is pressed (the fifth + * button, usually part of a scroll wheel). + */ +Guacamole.Mouse.State = function(x, y, left, middle, right, up, down) { - this.getMiddle = function() { - return middle; - }; + /** + * The current X position of the mouse pointer. + * @type Number + */ + this.x = x; - this.getRight = function() { - return right; - }; + /** + * The current Y position of the mouse pointer. + * @type Number + */ + this.y = y; - this.getUp = function() { - return up; - }; + /** + * Whether the left mouse button is currently pressed. + * @type Boolean + */ + this.left = left; - this.getDown = function() { - return down; - }; + /** + * Whether the middle mouse button is currently pressed. + * @type Boolean + */ + this.middle = middle + + /** + * Whether the right mouse button is currently pressed. + * @type Boolean + */ + this.right = right; + + /** + * Whether the up mouse button is currently pressed. This is the fourth + * mouse button, associated with upward scrolling of the mouse scroll + * wheel. + * @type Boolean + */ + this.up = up; + + /** + * Whether the down mouse button is currently pressed. This is the fifth + * mouse button, associated with downward scrolling of the mouse scroll + * wheel. + * @type Boolean + */ + this.down = down; + +}; -} diff --git a/guacamole-common-js/src/main/resources/oskeyboard.js b/guacamole-common-js/src/main/resources/oskeyboard.js index 872bac223..34c27810c 100644 --- a/guacamole-common-js/src/main/resources/oskeyboard.js +++ b/guacamole-common-js/src/main/resources/oskeyboard.js @@ -17,10 +17,19 @@ * along with this program. If not, see . */ +// Guacamole namespace +var Guacamole = Guacamole || {}; -function GuacamoleOnScreenKeyboard(url) { +/** + * Dynamic on-screen keyboard. Given the URL to an XML keyboard layout file, + * this object will download and use the XML to construct a clickable on-screen + * keyboard with its own key events. + * + * @constructor + * @param {String} url The URL of an XML keyboard layout file. + */ +Guacamole.OnScreenKeyboard = function(url) { - var tabIndex = 1; var allKeys = new Array(); var modifierState = new function() {}; diff --git a/guacamole-common-js/src/main/resources/tunnel.js b/guacamole-common-js/src/main/resources/tunnel.js index d5a785ac3..9d6671f4f 100644 --- a/guacamole-common-js/src/main/resources/tunnel.js +++ b/guacamole-common-js/src/main/resources/tunnel.js @@ -16,7 +16,77 @@ * You should have received a copy of the GNU Affero General Public License */ -function GuacamoleHTTPTunnel(tunnelURL) { +// Guacamole namespace +var Guacamole = Guacamole || {}; + +/** + * Core object providing abstract communication for Guacamole. This object + * is a null implementation whose functions do nothing. Guacamole applications + * should use {@link Guacamole.HTTPTunnel} instead, or implement their own tunnel based + * on this one. + * + * @constructor + * @see Guacamole.HTTPTunnel + */ +Guacamole.Tunnel = function() { + + /** + * Connect to the tunnel with the given optional data. This data is + * typically used for authentication. The format of data accepted is + * up to the tunnel implementation. + * + * @param {String} data The data to send to the tunnel when connecting. + */ + this.connect = function(data) {}; + + /** + * Disconnect from the tunnel. + */ + this.disconnect = function() {}; + + /** + * Send the given message through the tunnel to the service on the other + * side. All messages are guaranteed to be received in the order sent. + * + * @param {String} message The message to send to the service on the other + * side of the tunnel. + */ + this.sendMessage = function(message) {}; + + /** + * Fired whenever an error is encountered by the tunnel. + * + * @event + * @param {String} message A human-readable description of the error that + * occurred. + */ + this.onerror = null; + + /** + * Fired once for every complete Guacamole instruction received, in order. + * + * @event + * @param {String} opcode The Guacamole instruction opcode. + * @param {Array} parameters The parameters provided for the instruction, + * if any. + */ + this.oninstruction = null; + +}; + +/** + * Guacamole Tunnel implemented over HTTP via XMLHttpRequest. + * + * @constructor + * @augments Guacamole.Tunnel + * @param {String} tunnelURL The URL of the HTTP tunneling service. + */ +Guacamole.HTTPTunnel = function(tunnelURL) { + + /** + * Reference to this HTTP tunnel. + */ + var tunnel = this; var tunnel_uuid; @@ -36,12 +106,10 @@ function GuacamoleHTTPTunnel(tunnelURL) { // Default to polling - will be turned off automatically if not needed var pollingMode = POLLING_ENABLED; - var instructionHandler = null; - - var sendingMessages = 0; + var sendingMessages = false; var outputMessageBuffer = ""; - function sendMessage(message) { + this.sendMessage = function(message) { // Do not attempt to send messages if not connected if (currentState != STATE_CONNECTED) @@ -49,16 +117,16 @@ function GuacamoleHTTPTunnel(tunnelURL) { // Add event to queue, restart send loop if finished. outputMessageBuffer += message; - if (sendingMessages == 0) + if (!sendingMessages) sendPendingMessages(); - } + }; function sendPendingMessages() { if (outputMessageBuffer.length > 0) { - sendingMessages = 1; + sendingMessages = true; var message_xmlhttprequest = new XMLHttpRequest(); message_xmlhttprequest.open("POST", TUNNEL_WRITE + tunnel_uuid); @@ -75,7 +143,7 @@ function GuacamoleHTTPTunnel(tunnelURL) { } else - sendingMessages = 0; + sendingMessages = false; } @@ -101,8 +169,8 @@ function GuacamoleHTTPTunnel(tunnelURL) { return; } - // Start next request as soon as possible - if (xmlhttprequest.readyState >= 2 && nextRequest == null) + // Start next request as soon as possible IF request was successful + if (xmlhttprequest.readyState >= 2 && nextRequest == null && xmlhttprequest.status == 200) nextRequest = makeRequest(); // Parse stream when data is received and when complete. @@ -117,9 +185,25 @@ function GuacamoleHTTPTunnel(tunnelURL) { clearInterval(interval); } + // If canceled, stop transfer + if (xmlhttprequest.status == 0) { + tunnel.disconnect(); + return; + } + // Halt on error during request - if (xmlhttprequest.status == 0 || xmlhttprequest.status != 200) { - disconnect(); + else if (xmlhttprequest.status != 200) { + + // Get error message (if any) + var message = xmlhttprequest.getResponseHeader("X-Guacamole-Error-Message"); + if (!message) + message = "Internal server error"; + + // Call error handler + if (tunnel.onerror) tunnel.onerror(message); + + // Finish + tunnel.disconnect(); return; } @@ -160,8 +244,8 @@ function GuacamoleHTTPTunnel(tunnelURL) { } // Call instruction handler. - if (instructionHandler != null) - instructionHandler(opcode, parameters); + if (tunnel.oninstruction != null) + tunnel.oninstruction(opcode, parameters); } // Start search at end of string. @@ -213,7 +297,7 @@ function GuacamoleHTTPTunnel(tunnelURL) { } - function connect(data) { + this.connect = function(data) { // Start tunnel and connect synchronously var connect_xmlhttprequest = new XMLHttpRequest(); @@ -239,18 +323,12 @@ function GuacamoleHTTPTunnel(tunnelURL) { currentState = STATE_CONNECTED; handleResponse(makeRequest()); - } - - function disconnect() { - currentState = STATE_DISCONNECTED; - } - - // External API - this.connect = connect; - this.disconnect = disconnect; - this.sendMessage = sendMessage; - this.setInstructionHandler = function(handler) { - instructionHandler = handler; }; -} + this.disconnect = function() { + currentState = STATE_DISCONNECTED; + }; + +}; + +Guacamole.HTTPTunnel.prototype = new Guacamole.Tunnel();