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();