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 || {};
43 * Guacamole protocol client. Given a display element and {@link Guacamole.Tunnel},
44 * automatically handles incoming and outgoing Guacamole instructions via the
45 * provided tunnel, updating the display using one or more canvas elements.
48 * @param {Guacamole.Tunnel} tunnel The tunnel to use to send and receive
49 * Guacamole instructions.
51 Guacamole.Client = function(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;
68 var displayHeight = 0;
71 var display = document.createElement("div");
72 display.style.position = "relative";
73 display.style.width = displayWidth + "px";
74 display.style.height = displayHeight + "px";
76 // Create default layer
77 var default_layer_container = new Guacamole.Client.LayerContainer(displayWidth, displayHeight);
79 // Position default layer
80 var default_layer_container_element = default_layer_container.getElement();
81 default_layer_container_element.style.position = "absolute";
82 default_layer_container_element.style.left = "0px";
83 default_layer_container_element.style.top = "0px";
85 // Create cursor layer
86 var cursor = new Guacamole.Client.LayerContainer(0, 0);
87 cursor.getLayer().setCompositeOperation(Guacamole.Layer.SRC);
89 // Position cursor layer
90 var cursor_element = cursor.getElement();
91 cursor_element.style.position = "absolute";
92 cursor_element.style.left = "0px";
93 cursor_element.style.top = "0px";
95 // Add default layer and cursor to display
96 display.appendChild(default_layer_container.getElement());
97 display.appendChild(cursor.getElement());
99 // Initially, only default layer exists
100 var layers = [default_layer_container];
102 // No initial buffers
105 tunnel.onerror = function(message) {
106 if (guac_client.onerror)
107 guac_client.onerror(message);
110 function setState(state) {
111 if (state != currentState) {
112 currentState = state;
113 if (guac_client.onstatechange)
114 guac_client.onstatechange(currentState);
118 function isConnected() {
119 return currentState == STATE_CONNECTED
120 || currentState == STATE_WAITING;
123 var cursorHotspotX = 0;
124 var cursorHotspotY = 0;
129 function moveCursor(x, y) {
131 var element = cursor.getElement();
134 element.style.left = (x - cursorHotspotX) + "px";
135 element.style.top = (y - cursorHotspotY) + "px";
137 // Update stored position
143 guac_client.getDisplay = function() {
147 guac_client.sendKeyEvent = function(pressed, keysym) {
148 // Do not send requests if not connected
152 tunnel.sendMessage("key", keysym, pressed);
155 guac_client.sendMouseState = function(mouseState) {
157 // Do not send requests if not connected
161 // Update client-side cursor
169 if (mouseState.left) buttonMask |= 1;
170 if (mouseState.middle) buttonMask |= 2;
171 if (mouseState.right) buttonMask |= 4;
172 if (mouseState.up) buttonMask |= 8;
173 if (mouseState.down) buttonMask |= 16;
176 tunnel.sendMessage("mouse", mouseState.x, mouseState.y, buttonMask);
179 guac_client.setClipboard = function(data) {
181 // Do not send requests if not connected
185 tunnel.sendMessage("clipboard", data);
189 guac_client.onstatechange = null;
190 guac_client.onname = null;
191 guac_client.onerror = null;
192 guac_client.onclipboard = null;
195 function getBufferLayer(index) {
198 var buffer = buffers[index];
200 // Create buffer if necessary
201 if (buffer == null) {
202 buffer = new Guacamole.Layer(0, 0);
204 buffers[index] = buffer;
211 function getLayerContainer(index) {
213 var layer = layers[index];
217 layer = new Guacamole.Client.LayerContainer(displayWidth, displayHeight);
218 layers[index] = layer;
220 // Get and position layer
221 var layer_element = layer.getElement();
222 layer_element.style.position = "absolute";
223 layer_element.style.left = "0px";
224 layer_element.style.top = "0px";
226 // Add to default layer container
227 default_layer_container.getElement().appendChild(layer_element);
235 function getLayer(index) {
237 // If buffer, just get layer
239 return getBufferLayer(index);
241 // Otherwise, retrieve layer from layer container
242 return getLayerContainer(index).getLayer();
246 var instructionHandlers = {
248 "error": function(parameters) {
249 if (guac_client.onerror) guac_client.onerror(parameters[0]);
250 guac_client.disconnect();
253 "name": function(parameters) {
254 if (guac_client.onname) guac_client.onname(parameters[0]);
257 "clipboard": function(parameters) {
258 if (guac_client.onclipboard) guac_client.onclipboard(parameters[0]);
261 "size": function(parameters) {
263 var layer_index = parseInt(parameters[0]);
264 var width = parseInt(parameters[1]);
265 var height = parseInt(parameters[2]);
267 // Only valid for layers (buffers auto-resize)
268 if (layer_index >= 0) {
271 var layer_container = getLayerContainer(layer_index);
272 layer_container.resize(width, height);
274 // If layer is default, resize display
275 if (layer_index == 0) {
277 displayWidth = width;
278 displayHeight = height;
280 // Update (set) display size
281 display.style.width = displayWidth + "px";
282 display.style.height = displayHeight + "px";
286 } // end if layer (not buffer)
290 "move": function(parameters) {
292 var layer_index = parseInt(parameters[0]);
293 var parent_index = parseInt(parameters[1]);
294 var x = parseInt(parameters[2]);
295 var y = parseInt(parameters[3]);
296 var z = parseInt(parameters[4]);
298 // Only valid for non-default layers
299 if (layer_index > 0 && parent_index >= 0) {
301 // Get container element
302 var layer_container = getLayerContainer(layer_index).getElement();
303 var parent = getLayerContainer(parent_index).getElement();
305 // Set parent if necessary
306 if (!(layer_container.parentNode === parent))
307 parent.appendChild(layer_container);
310 layer_container.style.left = x + "px";
311 layer_container.style.top = y + "px";
312 layer_container.style.zIndex = z;
318 "dispose": function(parameters) {
320 var layer_index = parseInt(parameters[0]);
322 // If visible layer, remove from parent
323 if (layer_index > 0) {
325 // Get container element
326 var layer_container = getLayerContainer(layer_index).getElement();
328 // Remove from parent
329 layer_container.parentNode.removeChild(layer_container);
332 delete layers[layer_index];
336 // If buffer, just delete reference
337 else if (layer_index < 0)
338 delete buffers[-1 - layer_index];
340 // Attempting to dispose the root layer currently has no effect.
344 "png": function(parameters) {
346 var channelMask = parseInt(parameters[0]);
347 var layer = getLayer(parseInt(parameters[1]));
348 var x = parseInt(parameters[2]);
349 var y = parseInt(parameters[3]);
350 var data = parameters[4];
352 layer.setCompositeOperation(channelMask);
357 "data:image/png;base64," + data
360 // If received first update, no longer waiting.
361 if (currentState == STATE_WAITING)
362 setState(STATE_CONNECTED);
366 "copy": function(parameters) {
368 var srcL = getLayer(parseInt(parameters[0]));
369 var srcX = parseInt(parameters[1]);
370 var srcY = parseInt(parameters[2]);
371 var srcWidth = parseInt(parameters[3]);
372 var srcHeight = parseInt(parameters[4]);
373 var channelMask = parseInt(parameters[5]);
374 var dstL = getLayer(parseInt(parameters[6]));
375 var dstX = parseInt(parameters[7]);
376 var dstY = parseInt(parameters[8]);
378 dstL.setCompositeOperation(channelMask);
392 "rect": function(parameters) {
394 var channelMask = parseInt(parameters[0]);
395 var layer = getLayer(parseInt(parameters[1]));
396 var x = parseInt(parameters[2]);
397 var y = parseInt(parameters[3]);
398 var w = parseInt(parameters[4]);
399 var h = parseInt(parameters[5]);
400 var r = parseInt(parameters[6]);
401 var g = parseInt(parameters[7]);
402 var b = parseInt(parameters[8]);
403 var a = parseInt(parameters[9]);
405 layer.setCompositeOperation(channelMask);
414 "clip": function(parameters) {
416 var layer = getLayer(parseInt(parameters[0]));
417 var x = parseInt(parameters[1]);
418 var y = parseInt(parameters[2]);
419 var w = parseInt(parameters[3]);
420 var h = parseInt(parameters[4]);
422 layer.clipRect(x, y, w, h);
426 "cursor": function(parameters) {
428 cursorHotspotX = parseInt(parameters[0]);
429 cursorHotspotY = parseInt(parameters[1]);
430 var srcL = getLayer(parseInt(parameters[2]));
431 var srcX = parseInt(parameters[3]);
432 var srcY = parseInt(parameters[4]);
433 var srcWidth = parseInt(parameters[5]);
434 var srcHeight = parseInt(parameters[6]);
437 cursor.resize(srcWidth, srcHeight);
439 // Draw cursor to cursor layer
440 cursor.getLayer().copyRect(
450 // Update cursor position (hotspot may have changed)
451 moveCursor(cursorX, cursorY);
455 "sync": function(parameters) {
457 var timestamp = parameters[0];
459 // When all layers have finished rendering all instructions
460 // UP TO THIS POINT IN TIME, send sync response.
462 var layersToSync = 0;
463 function syncLayer() {
467 // Send sync response when layers are finished
468 if (layersToSync == 0) {
469 if (timestamp != currentTimestamp) {
470 tunnel.sendMessage("sync", timestamp);
471 currentTimestamp = timestamp;
477 // Count active, not-ready layers and install sync tracking hooks
478 for (var i=0; i<layers.length; i++) {
480 var layer = layers[i].getLayer();
481 if (layer && !layer.isReady()) {
483 layer.sync(syncLayer);
488 // If all layers are ready, then we didn't install any hooks.
489 // Send sync message now,
490 if (layersToSync == 0) {
491 if (timestamp != currentTimestamp) {
492 tunnel.sendMessage("sync", timestamp);
493 currentTimestamp = timestamp;
502 tunnel.oninstruction = function(opcode, parameters) {
504 var handler = instructionHandlers[opcode];
511 guac_client.disconnect = function() {
513 // Only attempt disconnection not disconnected.
514 if (currentState != STATE_DISCONNECTED
515 && currentState != STATE_DISCONNECTING) {
517 setState(STATE_DISCONNECTING);
521 window.clearInterval(pingInterval);
523 // Send disconnect message and disconnect
524 tunnel.sendMessage("disconnect");
526 setState(STATE_DISCONNECTED);
532 guac_client.connect = function(data) {
534 setState(STATE_CONNECTING);
537 tunnel.connect(data);
540 setState(STATE_IDLE);
544 // Ping every 5 seconds (ensure connection alive)
545 pingInterval = window.setInterval(function() {
546 tunnel.sendMessage("sync", currentTimestamp);
549 setState(STATE_WAITING);
556 * Simple container for Guacamole.Layer, allowing layers to be easily
557 * repositioned and nested. This allows certain operations to be accelerated
558 * through DOM manipulation, rather than raster operations.
562 * @param {Number} width The width of the Layer, in pixels. The canvas element
563 * backing this Layer will be given this width.
565 * @param {Number} height The height of the Layer, in pixels. The canvas element
566 * backing this Layer will be given this height.
568 Guacamole.Client.LayerContainer = function(width, height) {
571 * Reference to this LayerContainer.
574 var layer_container = this;
576 // Create layer with given size
577 var layer = new Guacamole.Layer(width, height);
579 // Set layer position
580 var canvas = layer.getCanvas();
581 canvas.style.position = "absolute";
582 canvas.style.left = "0px";
583 canvas.style.top = "0px";
585 // Create div with given size
586 var div = document.createElement("div");
587 div.appendChild(canvas);
588 div.style.width = width + "px";
589 div.style.height = height + "px";
592 * Changes the size of this LayerContainer and the contained Layer to the
593 * given width and height.
595 * @param {Number} width The new width to assign to this Layer.
596 * @param {Number} height The new height to assign to this Layer.
598 layer_container.resize = function(width, height) {
601 layer.resize(width, height);
603 // Resize containing div
604 div.style.width = width + "px";
605 div.style.height = height + "px";
610 * Returns the Layer contained within this LayerContainer.
611 * @returns {Guacamole.Layer} The Layer contained within this LayerContainer.
613 layer_container.getLayer = function() {
618 * Returns the element containing the Layer within this LayerContainer.
619 * @returns {Element} The element containing the Layer within this LayerContainer.
621 layer_container.getElement = function() {