2 /* ***** BEGIN LICENSE BLOCK *****
3 * Version: MPL 1.1/GPL 2.0/LGPL 2.1
5 * The contents of this file are subject to the Mozilla Public License Version
6 * 1.1 (the "License"); you may not use this file except in compliance with
7 * the License. You may obtain a copy of the License at
8 * http://www.mozilla.org/MPL/
10 * Software distributed under the License is distributed on an "AS IS" basis,
11 * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
12 * for the specific language governing rights and limitations under the
15 * The Original Code is guacamole-common-js.
17 * The Initial Developer of the Original Code is
19 * Portions created by the Initial Developer are Copyright (C) 2010
20 * the Initial Developer. All Rights Reserved.
24 * Alternatively, the contents of this file may be used under the terms of
25 * either the GNU General Public License Version 2 or later (the "GPL"), or
26 * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
27 * in which case the provisions of the GPL or the LGPL are applicable instead
28 * of those above. If you wish to allow use of your version of this file only
29 * under the terms of either the GPL or the LGPL, and not to allow others to
30 * use your version of this file under the terms of the MPL, indicate your
31 * decision by deleting the provisions above and replace them with the notice
32 * and other provisions required by the GPL or the LGPL. If you do not delete
33 * the provisions above, a recipient may use your version of this file under
34 * the terms of any one of the MPL, the GPL or the LGPL.
36 * ***** END LICENSE BLOCK ***** */
38 // Guacamole namespace
39 var Guacamole = Guacamole || {};
42 * Guacamole protocol client. Given a display element and {@link Guacamole.Tunnel},
43 * automatically handles incoming and outgoing Guacamole instructions via the
44 * provided tunnel, updating the display using one or more canvas elements.
47 * @param {Element} display The display element to add canvas elements to.
48 * @param {Guacamole.Tunnel} tunnel The tunnel to use to send and receive
49 * Guacamole instructions.
51 Guacamole.Client = function(display, tunnel) {
53 var guac_client = this;
56 var STATE_CONNECTING = 1;
57 var STATE_WAITING = 2;
58 var STATE_CONNECTED = 3;
59 var STATE_DISCONNECTING = 4;
60 var STATE_DISCONNECTED = 5;
62 var currentState = STATE_IDLE;
64 var currentTimestamp = 0;
65 var pingInterval = null;
67 tunnel.onerror = function(message) {
68 if (guac_client.onerror)
69 guac_client.onerror(message);
72 // Display must be relatively positioned for mouse to be handled properly
73 display.style.position = "relative";
75 function setState(state) {
76 if (state != currentState) {
78 if (guac_client.onstatechange)
79 guac_client.onstatechange(currentState);
83 function isConnected() {
84 return currentState == STATE_CONNECTED
85 || currentState == STATE_WAITING;
88 var cursorImage = null;
89 var cursorHotspotX = 0;
90 var cursorHotspotY = 0;
99 function redrawCursor(x, y) {
101 // Hide hardware cursor
102 if (cursorHidden == 0) {
103 display.className += " guac-hide-cursor";
108 cursor.clearRect(cursorRectX, cursorRectY, cursorRectW, cursorRectH);
111 cursorRectX = x - cursorHotspotX;
112 cursorRectY = y - cursorHotspotY;
113 cursorRectW = cursorImage.width;
114 cursorRectH = cursorImage.height;
117 cursor.drawImage(cursorRectX, cursorRectY, cursorImage);
120 guac_client.sendKeyEvent = function(pressed, keysym) {
121 // Do not send requests if not connected
125 tunnel.sendMessage("key", keysym, pressed);
128 guac_client.sendMouseState = function(mouseState) {
130 // Do not send requests if not connected
134 // Draw client-side cursor
135 if (cursorImage != null) {
144 if (mouseState.left) buttonMask |= 1;
145 if (mouseState.middle) buttonMask |= 2;
146 if (mouseState.right) buttonMask |= 4;
147 if (mouseState.up) buttonMask |= 8;
148 if (mouseState.down) buttonMask |= 16;
151 tunnel.sendMessage("mouse", mouseState.x, mouseState.y, buttonMask);
154 guac_client.setClipboard = function(data) {
156 // Do not send requests if not connected
160 tunnel.sendMessage("clipboard", data);
164 guac_client.onstatechange = null;
165 guac_client.onname = null;
166 guac_client.onerror = null;
167 guac_client.onclipboard = null;
170 var displayWidth = 0;
171 var displayHeight = 0;
173 var layers = new Array();
174 var buffers = new Array();
177 guac_client.getLayers = function() {
181 function getLayer(index) {
183 // If negative index, use buffer
187 var buffer = buffers[index];
189 // Create buffer if necessary
190 if (buffer == null) {
191 buffer = new Guacamole.Layer(0, 0);
193 buffers[index] = buffer;
199 // If non-negative, use visible layer
202 var layer = layers[index];
206 layer = new Guacamole.Layer(displayWidth, displayHeight);
208 // Set layer position
209 var canvas = layer.getCanvas();
210 canvas.style.position = "absolute";
211 canvas.style.left = "0px";
212 canvas.style.top = "0px";
214 layers[index] = layer;
216 // (Re)-add existing layers in order
217 for (var i=0; i<layers.length; i++) {
220 // If already present, remove
221 if (layers[i].parentNode === display)
222 display.removeChild(layers[i].getCanvas());
225 display.appendChild(layers[i].getCanvas());
229 // Add cursor layer last
230 if (cursor != null) {
231 if (cursor.parentNode === display)
232 display.removeChild(cursor.getCanvas());
233 display.appendChild(cursor.getCanvas());
239 layer.resize(displayWidth, displayHeight);
247 var instructionHandlers = {
249 "error": function(parameters) {
250 if (guac_client.onerror) guac_client.onerror(parameters[0]);
251 guac_client.disconnect();
254 "name": function(parameters) {
255 if (guac_client.onname) guac_client.onname(parameters[0]);
258 "clipboard": function(parameters) {
259 if (guac_client.onclipboard) guac_client.onclipboard(parameters[0]);
262 "size": function(parameters) {
264 var layer = getLayer(parseInt(parameters[0])); /* FIXME: Implement */
265 displayWidth = parseInt(parameters[1]);
266 displayHeight = parseInt(parameters[2]);
268 // Update (set) display size
269 display.style.width = displayWidth + "px";
270 display.style.height = displayHeight + "px";
272 // Set cursor layer width/height
274 cursor.resize(displayWidth, displayHeight);
278 "png": function(parameters) {
280 var channelMask = parseInt(parameters[0]);
281 var layer = getLayer(parseInt(parameters[1]));
282 var x = parseInt(parameters[2]);
283 var y = parseInt(parameters[3]);
284 var data = parameters[4];
286 layer.setChannelMask(channelMask);
291 "data:image/png;base64," + data
294 // If received first update, no longer waiting.
295 if (currentState == STATE_WAITING)
296 setState(STATE_CONNECTED);
300 "copy": function(parameters) {
302 var srcL = getLayer(parseInt(parameters[0]));
303 var srcX = parseInt(parameters[1]);
304 var srcY = parseInt(parameters[2]);
305 var srcWidth = parseInt(parameters[3]);
306 var srcHeight = parseInt(parameters[4]);
307 var channelMask = parseInt(parameters[5]);
308 var dstL = getLayer(parseInt(parameters[6]));
309 var dstX = parseInt(parameters[7]);
310 var dstY = parseInt(parameters[8]);
312 dstL.setChannelMask(channelMask);
326 "rect": function(parameters) {
328 var channelMask = parseInt(parameters[0]);
329 var layer = getLayer(parseInt(parameters[1]));
330 var x = parseInt(parameters[2]);
331 var y = parseInt(parameters[3]);
332 var w = parseInt(parameters[4]);
333 var h = parseInt(parameters[5]);
334 var r = parseInt(parameters[6]);
335 var g = parseInt(parameters[7]);
336 var b = parseInt(parameters[8]);
337 var a = parseInt(parameters[9]);
339 layer.setChannelMask(channelMask);
348 "clip": function(parameters) {
350 var layer = getLayer(parseInt(parameters[0]));
351 var x = parseInt(parameters[1]);
352 var y = parseInt(parameters[2]);
353 var w = parseInt(parameters[3]);
354 var h = parseInt(parameters[4]);
356 layer.clipRect(x, y, w, h);
360 "cursor": function(parameters) {
362 var x = parseInt(parameters[0]);
363 var y = parseInt(parameters[1]);
364 var data = parameters[2];
366 if (cursor == null) {
367 cursor = new Guacamole.Layer(displayWidth, displayHeight);
369 var canvas = cursor.getCanvas();
370 canvas.style.position = "absolute";
371 canvas.style.left = "0px";
372 canvas.style.top = "0px";
374 display.appendChild(canvas);
377 // Start cursor image load
378 var image = new Image();
379 image.onload = function() {
382 var cursorX = cursorRectX + cursorHotspotX;
383 var cursorY = cursorRectY + cursorHotspotY;
388 redrawCursor(cursorX, cursorY);
390 image.src = "data:image/png;base64," + data
394 "sync": function(parameters) {
396 var timestamp = parameters[0];
398 // When all layers have finished rendering all instructions
399 // UP TO THIS POINT IN TIME, send sync response.
401 var layersToSync = 0;
402 function syncLayer() {
406 // Send sync response when layers are finished
407 if (layersToSync == 0) {
408 if (timestamp != currentTimestamp) {
409 tunnel.sendMessage("sync", timestamp);
410 currentTimestamp = timestamp;
416 // Count active, not-ready layers and install sync tracking hooks
417 for (var i=0; i<layers.length; i++) {
419 var layer = layers[i];
420 if (layer && !layer.isReady()) {
422 layer.sync(syncLayer);
427 // If all layers are ready, then we didn't install any hooks.
428 // Send sync message now,
429 if (layersToSync == 0) {
430 if (timestamp != currentTimestamp) {
431 tunnel.sendMessage("sync", timestamp);
432 currentTimestamp = timestamp;
441 tunnel.oninstruction = function(opcode, parameters) {
443 var handler = instructionHandlers[opcode];
450 guac_client.disconnect = function() {
452 // Only attempt disconnection not disconnected.
453 if (currentState != STATE_DISCONNECTED
454 && currentState != STATE_DISCONNECTING) {
456 setState(STATE_DISCONNECTING);
460 window.clearInterval(pingInterval);
462 // Send disconnect message and disconnect
463 tunnel.sendMessage("disconnect");
465 setState(STATE_DISCONNECTED);
471 guac_client.connect = function(data) {
473 setState(STATE_CONNECTING);
476 tunnel.connect(data);
479 setState(STATE_IDLE);
483 // Ping every 5 seconds (ensure connection alive)
484 pingInterval = window.setInterval(function() {
485 tunnel.sendMessage("sync", currentTimestamp);
488 setState(STATE_WAITING);