mirror of
https://github.com/gyurix1968/guacamole-client.git
synced 2025-09-06 21:27:40 +00:00
Merge branch 'unstable' into touch-support
Conflicts: src/main/resources/mouse.js
This commit is contained in:
@@ -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;
|
||||
|
||||
}
|
||||
|
@@ -17,20 +17,105 @@
|
||||
* 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
|
||||
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; };
|
||||
|
||||
}
|
||||
};
|
||||
|
@@ -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;
|
||||
|
||||
|
@@ -17,29 +17,136 @@
|
||||
* 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");
|
||||
|
||||
/**
|
||||
* 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;
|
||||
|
||||
};
|
||||
|
@@ -17,60 +17,93 @@
|
||||
* 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;
|
||||
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;
|
||||
|
||||
};
|
||||
|
||||
}
|
||||
|
@@ -17,10 +17,19 @@
|
||||
* 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 modifierState = new function() {};
|
||||
|
||||
|
@@ -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();
|
||||
|
Reference in New Issue
Block a user