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 displayWidth = parseInt(parameters[0]);
265 displayHeight = parseInt(parameters[1]);
267 // Update (set) display size
268 display.style.width = displayWidth + "px";
269 display.style.height = displayHeight + "px";
271 // Set cursor layer width/height
273 cursor.resize(displayWidth, displayHeight);
277 "png": function(parameters) {
279 var channelMask = parseInt(parameters[0]);
280 var layer = getLayer(parseInt(parameters[1]));
281 var x = parseInt(parameters[2]);
282 var y = parseInt(parameters[3]);
283 var data = parameters[4];
285 layer.setChannelMask(channelMask);
290 "data:image/png;base64," + data
293 // If received first update, no longer waiting.
294 if (currentState == STATE_WAITING)
295 setState(STATE_CONNECTED);
299 "copy": function(parameters) {
301 var srcL = getLayer(parseInt(parameters[0]));
302 var srcX = parseInt(parameters[1]);
303 var srcY = parseInt(parameters[2]);
304 var srcWidth = parseInt(parameters[3]);
305 var srcHeight = parseInt(parameters[4]);
306 var channelMask = parseInt(parameters[5]);
307 var dstL = getLayer(parseInt(parameters[6]));
308 var dstX = parseInt(parameters[7]);
309 var dstY = parseInt(parameters[8]);
311 dstL.setChannelMask(channelMask);
325 "rect": function(parameters) {
327 var channelMask = parseInt(parameters[0]);
328 var layer = getLayer(parseInt(parameters[1]));
329 var x = parseInt(parameters[2]);
330 var y = parseInt(parameters[3]);
331 var w = parseInt(parameters[4]);
332 var h = parseInt(parameters[5]);
333 var r = parseInt(parameters[6]);
334 var g = parseInt(parameters[7]);
335 var b = parseInt(parameters[8]);
336 var a = parseInt(parameters[9]);
338 layer.setChannelMask(channelMask);
347 "clip": function(parameters) {
349 var layer = getLayer(parseInt(parameters[0]));
350 var x = parseInt(parameters[1]);
351 var y = parseInt(parameters[2]);
352 var w = parseInt(parameters[3]);
353 var h = parseInt(parameters[4]);
355 layer.clipRect(x, y, w, h);
359 "cursor": function(parameters) {
361 var x = parseInt(parameters[0]);
362 var y = parseInt(parameters[1]);
363 var data = parameters[2];
365 if (cursor == null) {
366 cursor = new Guacamole.Layer(displayWidth, displayHeight);
368 var canvas = cursor.getCanvas();
369 canvas.style.position = "absolute";
370 canvas.style.left = "0px";
371 canvas.style.top = "0px";
373 display.appendChild(canvas);
376 // Start cursor image load
377 var image = new Image();
378 image.onload = function() {
381 var cursorX = cursorRectX + cursorHotspotX;
382 var cursorY = cursorRectY + cursorHotspotY;
387 redrawCursor(cursorX, cursorY);
389 image.src = "data:image/png;base64," + data
393 "sync": function(parameters) {
395 var timestamp = parameters[0];
397 // When all layers have finished rendering all instructions
398 // UP TO THIS POINT IN TIME, send sync response.
400 var layersToSync = 0;
401 function syncLayer() {
405 // Send sync response when layers are finished
406 if (layersToSync == 0) {
407 tunnel.sendMessage("sync", timestamp);
408 currentTimestamp = timestamp;
413 // Count active, not-ready layers and install sync tracking hooks
414 for (var i=0; i<layers.length; i++) {
416 var layer = layers[i];
417 if (layer && !layer.isReady()) {
419 layer.sync(syncLayer);
424 // If all layers are ready, then we didn't install any hooks.
425 // Send sync message now,
426 if (layersToSync == 0) {
427 tunnel.sendMessage("sync", timestamp);
428 currentTimestamp = timestamp;
436 tunnel.oninstruction = function(opcode, parameters) {
438 var handler = instructionHandlers[opcode];
445 guac_client.disconnect = function() {
447 // Only attempt disconnection not disconnected.
448 if (currentState != STATE_DISCONNECTED
449 && currentState != STATE_DISCONNECTING) {
451 setState(STATE_DISCONNECTING);
455 window.clearInterval(pingInterval);
457 // Send disconnect message and disconnect
458 tunnel.sendMessage("disconnect");
460 setState(STATE_DISCONNECTED);
466 guac_client.connect = function(data) {
468 setState(STATE_CONNECTING);
471 tunnel.connect(data);
474 setState(STATE_IDLE);
478 // Ping every 5 seconds (ensure connection alive)
479 pingInterval = window.setInterval(function() {
480 tunnel.sendMessage("sync", currentTimestamp);
483 setState(STATE_WAITING);