Merge branch 'unstable' into touch-support

Conflicts:
	src/main/resources/mouse.js
This commit is contained in:
Michael Jumper
2011-08-26 14:00:14 -07:00
7 changed files with 901 additions and 408 deletions

View File

@@ -16,7 +16,22 @@
* You should have received a copy of the GNU Affero General Public License * 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_IDLE = 0;
var STATE_CONNECTING = 1; var STATE_CONNECTING = 1;
@@ -26,9 +41,13 @@ function GuacamoleClient(display, tunnel) {
var STATE_DISCONNECTED = 5; var STATE_DISCONNECTED = 5;
var currentState = STATE_IDLE; 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 must be relatively positioned for mouse to be handled properly
display.style.position = "relative"; display.style.position = "relative";
@@ -36,15 +55,11 @@ function GuacamoleClient(display, tunnel) {
function setState(state) { function setState(state) {
if (state != currentState) { if (state != currentState) {
currentState = state; currentState = state;
if (stateChangeHandler) if (guac_client.onstatechange)
stateChangeHandler(currentState); guac_client.onstatechange(currentState);
} }
} }
this.setOnStateChangeHandler = function(handler) {
stateChangeHandler = handler;
};
function isConnected() { function isConnected() {
return currentState == STATE_CONNECTED return currentState == STATE_CONNECTED
|| currentState == STATE_WAITING; || currentState == STATE_WAITING;
@@ -54,7 +69,6 @@ function GuacamoleClient(display, tunnel) {
var cursorHotspotX = 0; var cursorHotspotX = 0;
var cursorHotspotY = 0; var cursorHotspotY = 0;
// FIXME: Make object. Clean up.
var cursorRectX = 0; var cursorRectX = 0;
var cursorRectY = 0; var cursorRectY = 0;
var cursorRectW = 0; var cursorRectW = 0;
@@ -83,7 +97,7 @@ function GuacamoleClient(display, tunnel) {
cursor.drawImage(cursorRectX, cursorRectY, cursorImage); cursor.drawImage(cursorRectX, cursorRectY, cursorImage);
} }
this.sendKeyEvent = function(pressed, keysym) { guac_client.sendKeyEvent = function(pressed, keysym) {
// Do not send requests if not connected // Do not send requests if not connected
if (!isConnected()) if (!isConnected())
return; return;
@@ -91,7 +105,7 @@ function GuacamoleClient(display, tunnel) {
tunnel.sendMessage("key:" + keysym + "," + pressed + ";"); tunnel.sendMessage("key:" + keysym + "," + pressed + ";");
}; };
this.sendMouseState = function(mouseState) { guac_client.sendMouseState = function(mouseState) {
// Do not send requests if not connected // Do not send requests if not connected
if (!isConnected()) if (!isConnected())
@@ -100,24 +114,24 @@ function GuacamoleClient(display, tunnel) {
// Draw client-side cursor // Draw client-side cursor
if (cursorImage != null) { if (cursorImage != null) {
redrawCursor( redrawCursor(
mouseState.getX(), mouseState.x,
mouseState.getY() mouseState.y
); );
} }
// Build mask // Build mask
var buttonMask = 0; var buttonMask = 0;
if (mouseState.getLeft()) buttonMask |= 1; if (mouseState.left) buttonMask |= 1;
if (mouseState.getMiddle()) buttonMask |= 2; if (mouseState.middle) buttonMask |= 2;
if (mouseState.getRight()) buttonMask |= 4; if (mouseState.right) buttonMask |= 4;
if (mouseState.getUp()) buttonMask |= 8; if (mouseState.up) buttonMask |= 8;
if (mouseState.getDown()) buttonMask |= 16; if (mouseState.down) buttonMask |= 16;
// Send message // 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 // Do not send requests if not connected
if (!isConnected()) if (!isConnected())
@@ -127,21 +141,10 @@ function GuacamoleClient(display, tunnel) {
}; };
// Handlers // Handlers
guac_client.onstatechange = null;
var nameHandler = null; guac_client.onname = null;
this.setNameHandler = function(handler) { guac_client.onerror = null;
nameHandler = handler; guac_client.onclipboard = null;
}
var errorHandler = null;
this.setErrorHandler = function(handler) {
errorHandler = handler;
};
var clipboardHandler = null;
this.setClipboardHandler = function(handler) {
clipboardHandler = handler;
};
// Layers // Layers
var displayWidth = 0; var displayWidth = 0;
@@ -151,7 +154,7 @@ function GuacamoleClient(display, tunnel) {
var buffers = new Array(); var buffers = new Array();
var cursor = null; var cursor = null;
this.getLayers = function() { guac_client.getLayers = function() {
return layers; return layers;
}; };
@@ -165,8 +168,8 @@ function GuacamoleClient(display, tunnel) {
// Create buffer if necessary // Create buffer if necessary
if (buffer == null) { if (buffer == null) {
buffer = new Layer(0, 0); buffer = new Guacamole.Layer(0, 0);
buffer.setAutosize(1); buffer.autosize = 1;
buffers[index] = buffer; buffers[index] = buffer;
} }
@@ -180,7 +183,14 @@ function GuacamoleClient(display, tunnel) {
if (layer == null) { if (layer == null) {
// Add new layer // 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; layers[index] = layer;
// (Re)-add existing layers in order // (Re)-add existing layers in order
@@ -189,18 +199,18 @@ function GuacamoleClient(display, tunnel) {
// If already present, remove // If already present, remove
if (layers[i].parentNode === display) if (layers[i].parentNode === display)
display.removeChild(layers[i]); display.removeChild(layers[i].getCanvas());
// Add to end // Add to end
display.appendChild(layers[i]); display.appendChild(layers[i].getCanvas());
} }
} }
// Add cursor layer last // Add cursor layer last
if (cursor != null) { if (cursor != null) {
if (cursor.parentNode === display) if (cursor.parentNode === display)
display.removeChild(cursor); display.removeChild(cursor.getCanvas());
display.appendChild(cursor); display.appendChild(cursor.getCanvas());
} }
} }
@@ -217,16 +227,16 @@ function GuacamoleClient(display, tunnel) {
var instructionHandlers = { var instructionHandlers = {
"error": function(parameters) { "error": function(parameters) {
if (errorHandler) errorHandler(unescapeGuacamoleString(parameters[0])); if (guac_client.onerror) guac_client.onerror(unescapeGuacamoleString(parameters[0]));
disconnect(); disconnect();
}, },
"name": function(parameters) { "name": function(parameters) {
if (nameHandler) nameHandler(unescapeGuacamoleString(parameters[0])); if (guac_client.onname) guac_client.onname(unescapeGuacamoleString(parameters[0]));
}, },
"clipboard": function(parameters) { "clipboard": function(parameters) {
if (clipboardHandler) clipboardHandler(unescapeGuacamoleString(parameters[0])); if (guac_client.onclipboard) guac_client.onclipboard(unescapeGuacamoleString(parameters[0]));
}, },
"size": function(parameters) { "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) { "cursor": function(parameters) {
var x = parseInt(parameters[0]); var x = parseInt(parameters[0]);
@@ -299,8 +343,14 @@ function GuacamoleClient(display, tunnel) {
var data = parameters[2]; var data = parameters[2];
if (cursor == null) { if (cursor == null) {
cursor = new Layer(displayWidth, displayHeight); cursor = new Guacamole.Layer(displayWidth, displayHeight);
display.appendChild(cursor);
var canvas = cursor.getCanvas();
canvas.style.position = "absolute";
canvas.style.left = "0px";
canvas.style.top = "0px";
display.appendChild(canvas);
} }
// Start cursor image load // Start cursor image load
@@ -354,7 +404,7 @@ function GuacamoleClient(display, tunnel) {
if (layersToSync == 0) if (layersToSync == 0)
tunnel.sendMessage("sync:" + timestamp + ";"); tunnel.sendMessage("sync:" + timestamp + ";");
}, }
}; };
@@ -433,8 +483,8 @@ function GuacamoleClient(display, tunnel) {
} }
this.disconnect = disconnect; guac_client.disconnect = disconnect;
this.connect = function(data) { guac_client.connect = function(data) {
setState(STATE_CONNECTING); setState(STATE_CONNECTING);
@@ -449,7 +499,7 @@ function GuacamoleClient(display, tunnel) {
setState(STATE_WAITING); setState(STATE_WAITING);
}; };
this.escapeGuacamoleString = escapeGuacamoleString; guac_client.escapeGuacamoleString = escapeGuacamoleString;
this.unescapeGuacamoleString = unescapeGuacamoleString; guac_client.unescapeGuacamoleString = unescapeGuacamoleString;
} }

View File

@@ -17,20 +17,105 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
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 // Single key state/modifier buffer
var modShift = 0; var modShift = false;
var modCtrl = 0; var modCtrl = false;
var modAlt = 0; var modAlt = false;
var keydownChar = new Array(); var keydownChar = new Array();
// ID of routine repeating keystrokes. -1 = not repeating. // ID of routine repeating keystrokes. -1 = not repeating.
var repeatKeyTimeoutId = -1; var repeatKeyTimeoutId = -1;
var repeatKeyIntervalId = -1; var repeatKeyIntervalId = -1;
@@ -52,27 +137,27 @@ function GuacamoleKeyboard(element) {
function getKeySymFromKeyIdentifier(shifted, keyIdentifier) { function getKeySymFromKeyIdentifier(shifted, keyIdentifier) {
var unicodePrefixLocation = keyIdentifier.indexOf("U+"); var unicodePrefixLocation = keyIdentifier.indexOf("U+");
if (unicodePrefixLocation >= 0) { if (unicodePrefixLocation >= 0) {
var hex = keyIdentifier.substring(unicodePrefixLocation+2); var hex = keyIdentifier.substring(unicodePrefixLocation+2);
var codepoint = parseInt(hex, 16); var codepoint = parseInt(hex, 16);
var typedCharacter; var typedCharacter;
// Convert case if shifted // Convert case if shifted
if (shifted == 0) if (shifted == 0)
typedCharacter = String.fromCharCode(codepoint).toLowerCase(); typedCharacter = String.fromCharCode(codepoint).toLowerCase();
else else
typedCharacter = String.fromCharCode(codepoint).toUpperCase(); typedCharacter = String.fromCharCode(codepoint).toUpperCase();
// Get codepoint // Get codepoint
codepoint = typedCharacter.charCodeAt(0); codepoint = typedCharacter.charCodeAt(0);
return getKeySymFromCharCode(codepoint); return getKeySymFromCharCode(codepoint);
} }
return null; return null;
} }
@@ -91,7 +176,7 @@ function GuacamoleKeyboard(element) {
function getKeySymFromKeyCode(keyCode) { function getKeySymFromKeyCode(keyCode) {
var keysym = null; var keysym = null;
if (modShift == 0) keysym = unshiftedKeySym[keyCode]; if (!modShift) keysym = unshiftedKeySym[keyCode];
else { else {
keysym = shiftedKeySym[keyCode]; keysym = shiftedKeySym[keyCode];
if (keysym == null) keysym = unshiftedKeySym[keyCode]; if (keysym == null) keysym = unshiftedKeySym[keyCode];
@@ -104,14 +189,14 @@ function GuacamoleKeyboard(element) {
// Sends a single keystroke over the network // Sends a single keystroke over the network
function sendKeyPressed(keysym) { function sendKeyPressed(keysym) {
if (keysym != null && keyPressedHandler) if (keysym != null && guac_keyboard.onkeydown)
keyPressedHandler(keysym); guac_keyboard.onkeydown(keysym);
} }
// Sends a single keystroke over the network // Sends a single keystroke over the network
function sendKeyReleased(keysym) { function sendKeyReleased(keysym) {
if (keysym != null) if (keysym != null && guac_keyboard.onkeyup)
keyReleasedHandler(keysym); guac_keyboard.onkeyup(keysym);
} }
@@ -125,7 +210,7 @@ function GuacamoleKeyboard(element) {
element.onkeydown = function(e) { element.onkeydown = function(e) {
// Only intercept if handler set // Only intercept if handler set
if (!keyPressedHandler) return true; if (!guac_keyboard.onkeydown) return true;
var keynum; var keynum;
if (window.event) keynum = window.event.keyCode; if (window.event) keynum = window.event.keyCode;
@@ -133,11 +218,11 @@ function GuacamoleKeyboard(element) {
// Ctrl/Alt/Shift // Ctrl/Alt/Shift
if (keynum == 16) if (keynum == 16)
modShift = 1; modShift = true;
else if (keynum == 17) else if (keynum == 17)
modCtrl = 1; modCtrl = true;
else if (keynum == 18) else if (keynum == 18)
modAlt = 1; modAlt = true;
var keysym = getKeySymFromKeyCode(keynum); var keysym = getKeySymFromKeyCode(keynum);
if (keysym) { if (keysym) {
@@ -146,7 +231,7 @@ function GuacamoleKeyboard(element) {
} }
// If modifier keys are held down, and we have keyIdentifier // 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 // Get keysym from keyIdentifier
keysym = getKeySymFromKeyIdentifier(modShift, e.keyIdentifier); keysym = getKeySymFromKeyIdentifier(modShift, e.keyIdentifier);
@@ -183,13 +268,15 @@ function GuacamoleKeyboard(element) {
return false; return false;
} }
return true;
}; };
// When key pressed // When key pressed
element.onkeypress = function(e) { element.onkeypress = function(e) {
// Only intercept if handler set // Only intercept if handler set
if (!keyPressedHandler) return true; if (!guac_keyboard.onkeydown) return true;
if (keySymSource != KEYPRESS) return false; if (keySymSource != KEYPRESS) return false;
@@ -224,7 +311,7 @@ function GuacamoleKeyboard(element) {
element.onkeyup = function(e) { element.onkeyup = function(e) {
// Only intercept if handler set // Only intercept if handler set
if (!keyReleasedHandler) return true; if (!guac_keyboard.onkeyup) return true;
var keynum; var keynum;
if (window.event) keynum = window.event.keyCode; if (window.event) keynum = window.event.keyCode;
@@ -232,11 +319,11 @@ function GuacamoleKeyboard(element) {
// Ctrl/Alt/Shift // Ctrl/Alt/Shift
if (keynum == 16) if (keynum == 16)
modShift = 0; modShift = false;
else if (keynum == 17) else if (keynum == 17)
modCtrl = 0; modCtrl = false;
else if (keynum == 18) else if (keynum == 18)
modAlt = 0; modAlt = false;
else else
stopRepeat(); stopRepeat();
@@ -253,18 +340,10 @@ function GuacamoleKeyboard(element) {
}; };
// When focus is lost, clear modifiers. // When focus is lost, clear modifiers.
var docOnblur = element.onblur;
element.onblur = function() { element.onblur = function() {
modAlt = 0; modAlt = false;
modCtrl = 0; modCtrl = false;
modShift = 0; modShift = false;
if (docOnblur != null) docOnblur();
}; };
var keyPressedHandler = null; };
var keyReleasedHandler = null;
this.setKeyPressedHandler = function(kh) { keyPressedHandler = kh; };
this.setKeyReleasedHandler = function(kh) { keyReleasedHandler = kh; };
}

View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
// 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;

View File

@@ -17,29 +17,136 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
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"); var display = document.createElement("canvas");
/**
* The 2D display context of the canvas element backing this Layer.
* @private
*/
var displayContext = display.getContext("2d"); 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) { 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.width = newWidth;
display.height = newHeight; display.height = newHeight;
// Redraw old data, if any
if (oldData)
displayContext.drawImage(oldData,
0, 0, width, height,
0, 0, width, height);
width = newWidth; width = newWidth;
height = newHeight; height = newHeight;
} }
display.resize = function(newWidth, newHeight) { /**
if (newWidth != width || newHeight != height) * Given the X and Y coordinates of the upper-left corner of a rectangle
resize(newWidth, newHeight); * 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) { function fitRect(x, y, w, h) {
// Calculate bounds // Calculate bounds
@@ -66,176 +173,345 @@ function Layer(width, height) {
} }
resize(width, height); /**
* A container for an task handler. Each operation which must be ordered
var readyHandler = null; * is associated with a Task that goes into a task queue. Tasks in this
var updates = new Array(); * queue are executed in order once their handlers are set, while Tasks
var autosize = 0; * without handlers block themselves and any following Tasks from running.
*
function Update(updateHandler) { * @constructor
* @private
this.setHandler = function(handler) { * @param {function} taskHandler The function to call when this task
updateHandler = handler; * runs, if any.
}; */
function Task(taskHandler) {
this.hasHandler = function() {
return updateHandler != null; /**
}; * The handler this Task is associated with, if any.
*
this.handle = function() { * @type function
updateHandler(); */
} this.handler = taskHandler;
} }
display.setAutosize = function(flag) { /**
autosize = flag; * 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.
function reserveJob(handler) { *
* @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 no pending tasks, just call (if available) and exit
if (display.isReady() && handler != null) { if (layer.isReady() && handler != null) {
handler(); handler();
return null; 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. // and return a reference to it.
var update = new Update(handler); var task = new Task(handler);
updates.push(update); tasks.push(task);
return update; return task;
} }
function handlePendingUpdates() { var tasksInProgress = false;
// Draw all pending updates. /**
var update; * Run any Tasks which were pending but are now ready to run and are not
while ((update = updates[0]) != null && update.hasHandler()) { * blocked by other Tasks.
update.handle(); * @private
updates.shift(); */
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 tasksInProgress = false;
if (display.isReady() && readyHandler != null)
readyHandler();
} }
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() { * Draws the specified image at the given coordinates. The image specified
if (autosize != 0) fitRect(x, y, image.width, image.height); * 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); displayContext.drawImage(image, x, y);
}); });
}; };
/**
display.draw = function(x, y, url) { * Draws the image at the specified URL at the given coordinates. The image
var update = reserveJob(null); * 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(); var image = new Image();
image.onload = function() { image.onload = function() {
update.setHandler(function() { task.handler = function() {
if (autosize != 0) fitRect(x, y, image.width, image.height); if (layer.autosize != 0) fitRect(x, y, image.width, image.height);
displayContext.drawImage(image, x, y); displayContext.drawImage(image, x, y);
}); };
// As this update originally had no handler and may have blocked // As this task originally had no handler and may have blocked
// other updates, handle any blocked updates. // other tasks, handle any blocked tasks.
handlePendingUpdates(); handlePendingTasks();
}; };
image.src = url; image.src = url;
}; };
// Run arbitrary function as soon as currently pending operations complete. /**
// Future operations will not block this function from being called (unlike * Run an arbitrary function as soon as currently pending operations
// the ready handler, which requires no pending updates) * are complete.
display.sync = function(handler) { *
reserveJob(handler); * @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() { function doCopyRect() {
if (autosize != 0) fitRect(x, y, w, h); if (layer.autosize != 0) fitRect(x, y, srcw, srch);
displayContext.drawImage(srcLayer, srcx, srcy, w, h, x, y, w, h); displayContext.drawImage(srcLayer.getCanvas(), srcx, srcy, srcw, srch, x, y, srcw, srch);
} }
// If we ARE the source layer, no need to sync. // If we ARE the source layer, no need to sync.
// Syncing would result in deadlock. // Syncing would result in deadlock.
if (display === srcLayer) if (layer === srcLayer)
reserveJob(doCopyRect); scheduleTask(doCopyRect);
// Otherwise synchronize copy operation with source layer // Otherwise synchronize copy operation with source layer
else { else {
var update = reserveJob(null); var task = scheduleTask(null);
srcLayer.sync(function() { srcLayer.sync(function() {
update.setHandler(doCopyRect); task.handler = doCopyRect;
// As this update originally had no handler and may have blocked // As this task originally had no handler and may have blocked
// other updates, handle any blocked updates. // other tasks, handle any blocked tasks.
handlePendingUpdates(); handlePendingTasks();
}); });
} }
}; };
display.clearRect = function(x, y, w, h) { /**
reserveJob(function() { * Clear the specified rectangle of image data.
if (autosize != 0) fitRect(x, y, w, h); *
* @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); 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); var imageData = displayContext.getImageData(0, 0, width, height);
filter(imageData.data, width, height); filter(imageData.data, width, height);
displayContext.putImageData(imageData, 0, 0); displayContext.putImageData(imageData, 0, 0);
}); });
}; };
var compositeOperation = { /**
/* 0x0 NOT IMPLEMENTED */ * Sets the channel mask for future operations on this Layer. The channel
0x1: "destination-in", * mask is a Guacamole-specific compositing operation identifier with a
0x2: "destination-out", * single bit representing each of four channels (in order): source image
/* 0x3 NOT IMPLEMENTED */ * where destination transparent, source where destination opaque,
0x4: "source-in", * destination where source transparent, and destination where source
/* 0x5 NOT IMPLEMENTED */ * opaque.
0x6: "source-atop", *
/* 0x7 NOT IMPLEMENTED */ * @param {Number} mask The channel mask for future operations on this
0x8: "source-out", * Layer.
0x9: "destination-atop", */
0xA: "xor", this.setChannelMask = function(mask) {
0xB: "destination-over", scheduleTask(function() {
0xC: "copy",
/* 0xD NOT IMPLEMENTED */
0xE: "source-over",
0xF: "lighter",
};
display.setChannelMask = function(mask) {
reserveJob(function() {
displayContext.globalCompositeOperation = compositeOperation[mask]; displayContext.globalCompositeOperation = compositeOperation[mask];
}); });
}; };
return display; // Initialize canvas dimensions
display.width = width;
} display.height = height;
};

View File

@@ -17,60 +17,93 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
// 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; * Fired whenever the user releases a mouse button down over the element
var mouseRightButton = 0; * associated with this Guacamole.Mouse.
*
var mouseX = 0; * @event
var mouseY = 0; * @param {Guacamole.Mouse.State} state The current mouse state.
*/
var absoluteMouseX = 0; this.onmouseup = null;
var absoluteMouseY = 0;
function getMouseState(up, down) {
var mouseState = new MouseEvent(mouseX, mouseY,
mouseLeftButton, mouseMiddleButton, mouseRightButton, up, down);
return mouseState;
}
/**
* 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) { function moveMouse(pageX, pageY) {
absoluteMouseX = pageX; guac_mouse.currentState.x = pageX - element.offsetLeft;
absoluteMouseY = pageY; guac_mouse.currentState.y = pageY - element.offsetTop;
mouseX = absoluteMouseX - element.offsetLeft;
mouseY = absoluteMouseY - element.offsetTop;
// This is all JUST so we can get the mouse position within the element // This is all JUST so we can get the mouse position within the element
var parent = element.offsetParent; var parent = element.offsetParent;
while (parent) { while (parent) {
if (parent.offsetLeft && parent.offsetTop) { if (parent.offsetLeft && parent.offsetTop) {
mouseX -= parent.offsetLeft; guac_mouse.currentState.x -= parent.offsetLeft;
mouseY -= parent.offsetTop; guac_mouse.currentState.y -= parent.offsetTop;
} }
parent = parent.offsetParent; 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 // 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) { element.onmousemove = function(e) {
@@ -136,17 +169,19 @@ function GuacamoleMouse(element) {
switch (e.button) { switch (e.button) {
case 0: case 0:
mouseLeftButton = 1; guac_mouse.currentState.left = true;
break; break;
case 1: case 1:
mouseMiddleButton = 1; guac_mouse.currentState.middle = true;
break; break;
case 2: case 2:
mouseRightButton = 1; guac_mouse.currentState.right = true;
break; 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) { switch (e.button) {
case 0: case 0:
mouseLeftButton = 0; guac_mouse.currentState.left = false;
break; break;
case 1: case 1:
mouseMiddleButton = 0; guac_mouse.currentState.middle = false;
break; break;
case 2: case 2:
mouseRightButton = 0; guac_mouse.currentState.right = false;
break; break;
} }
buttonReleasedHandler(getMouseState(0, 0)); if (guac_mouse.onmouseup)
guac_mouse.onmouseup(guac_mouse.currentState);
}; };
element.onmouseout = function(e) { element.onmouseout = function(e) {
@@ -174,12 +211,16 @@ function GuacamoleMouse(element) {
e.stopPropagation(); e.stopPropagation();
// Release all buttons // Release all buttons
if (mouseLeftButton || mouseMiddleButton || mouseRightButton) { if (guac_mouse.currentState.left
mouseLeftButton = 0; || guac_mouse.currentState.middle
mouseMiddleButton = 0; || guac_mouse.currentState.right) {
mouseRightButton = 0;
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 // Up
if (delta < 0) { if (delta < 0) {
buttonPressedHandler(getMouseState(1, 0)); if (guac_mouse.onmousedown) {
buttonReleasedHandler(getMouseState(0, 0)); 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 // Down
if (delta > 0) { if (delta > 0) {
buttonPressedHandler(getMouseState(0, 1)); if (guac_mouse.onmousedown) {
buttonReleasedHandler(getMouseState(0, 0)); 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) if (e.preventDefault)
@@ -220,53 +275,71 @@ function GuacamoleMouse(element) {
element.onmousewheel = function(e) { element.onmousewheel = function(e) {
handleScroll(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;
};
}

View File

@@ -17,10 +17,19 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
// 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 allKeys = new Array();
var modifierState = new function() {}; var modifierState = new function() {};

View File

@@ -16,7 +16,77 @@
* You should have received a copy of the GNU Affero General Public License * 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; var tunnel_uuid;
@@ -36,12 +106,10 @@ function GuacamoleHTTPTunnel(tunnelURL) {
// Default to polling - will be turned off automatically if not needed // Default to polling - will be turned off automatically if not needed
var pollingMode = POLLING_ENABLED; var pollingMode = POLLING_ENABLED;
var instructionHandler = null; var sendingMessages = false;
var sendingMessages = 0;
var outputMessageBuffer = ""; var outputMessageBuffer = "";
function sendMessage(message) { this.sendMessage = function(message) {
// Do not attempt to send messages if not connected // Do not attempt to send messages if not connected
if (currentState != STATE_CONNECTED) if (currentState != STATE_CONNECTED)
@@ -49,16 +117,16 @@ function GuacamoleHTTPTunnel(tunnelURL) {
// Add event to queue, restart send loop if finished. // Add event to queue, restart send loop if finished.
outputMessageBuffer += message; outputMessageBuffer += message;
if (sendingMessages == 0) if (!sendingMessages)
sendPendingMessages(); sendPendingMessages();
} };
function sendPendingMessages() { function sendPendingMessages() {
if (outputMessageBuffer.length > 0) { if (outputMessageBuffer.length > 0) {
sendingMessages = 1; sendingMessages = true;
var message_xmlhttprequest = new XMLHttpRequest(); var message_xmlhttprequest = new XMLHttpRequest();
message_xmlhttprequest.open("POST", TUNNEL_WRITE + tunnel_uuid); message_xmlhttprequest.open("POST", TUNNEL_WRITE + tunnel_uuid);
@@ -75,7 +143,7 @@ function GuacamoleHTTPTunnel(tunnelURL) {
} }
else else
sendingMessages = 0; sendingMessages = false;
} }
@@ -101,8 +169,8 @@ function GuacamoleHTTPTunnel(tunnelURL) {
return; return;
} }
// Start next request as soon as possible // Start next request as soon as possible IF request was successful
if (xmlhttprequest.readyState >= 2 && nextRequest == null) if (xmlhttprequest.readyState >= 2 && nextRequest == null && xmlhttprequest.status == 200)
nextRequest = makeRequest(); nextRequest = makeRequest();
// Parse stream when data is received and when complete. // Parse stream when data is received and when complete.
@@ -117,9 +185,25 @@ function GuacamoleHTTPTunnel(tunnelURL) {
clearInterval(interval); clearInterval(interval);
} }
// If canceled, stop transfer
if (xmlhttprequest.status == 0) {
tunnel.disconnect();
return;
}
// Halt on error during request // Halt on error during request
if (xmlhttprequest.status == 0 || xmlhttprequest.status != 200) { else if (xmlhttprequest.status != 200) {
disconnect();
// 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; return;
} }
@@ -160,8 +244,8 @@ function GuacamoleHTTPTunnel(tunnelURL) {
} }
// Call instruction handler. // Call instruction handler.
if (instructionHandler != null) if (tunnel.oninstruction != null)
instructionHandler(opcode, parameters); tunnel.oninstruction(opcode, parameters);
} }
// Start search at end of string. // 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 // Start tunnel and connect synchronously
var connect_xmlhttprequest = new XMLHttpRequest(); var connect_xmlhttprequest = new XMLHttpRequest();
@@ -239,18 +323,12 @@ function GuacamoleHTTPTunnel(tunnelURL) {
currentState = STATE_CONNECTED; currentState = STATE_CONNECTED;
handleResponse(makeRequest()); 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();