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 * Map of all Guacamole binary raster operations to transfer functions.
74 var transferFunctions = {
76 0x10: function (src, dst) { return 0x00; }, /* BLACK */
77 0x1F: function (src, dst) { return 0xFF; }, /* WHITE */
79 0x13: function (src, dst) { return src; }, /* SRC */
80 0x15: function (src, dst) { return dst; }, /* DEST */
81 0x1C: function (src, dst) { return ~src; }, /* NSRC */
82 0x1A: function (src, dst) { return ~dst; }, /* NDEST */
84 0x11: function (src, dst) { return src & dst; }, /* AND */
85 0x1E: function (src, dst) { return ~(src & dst); }, /* NAND */
87 0x17: function (src, dst) { return src | dst; }, /* OR */
88 0x18: function (src, dst) { return ~(src | dst); }, /* NOR */
90 0x16: function (src, dst) { return src ^ dst; }, /* XOR */
91 0x19: function (src, dst) { return ~(src ^ dst); }, /* XNOR */
93 0x14: function (src, dst) { return ~src & dst; }, /* AND inverted source */
94 0x1D: function (src, dst) { return ~src | dst; }, /* OR inverted source */
95 0x12: function (src, dst) { return src & ~dst; }, /* AND inverted destination */
96 0x1B: function (src, dst) { return src | ~dst; } /* OR inverted destination */
101 var display = document.createElement("div");
102 display.style.position = "relative";
103 display.style.width = displayWidth + "px";
104 display.style.height = displayHeight + "px";
106 // Create default layer
107 var default_layer_container = new Guacamole.Client.LayerContainer(displayWidth, displayHeight);
109 // Position default layer
110 var default_layer_container_element = default_layer_container.getElement();
111 default_layer_container_element.style.position = "absolute";
112 default_layer_container_element.style.left = "0px";
113 default_layer_container_element.style.top = "0px";
115 // Create cursor layer
116 var cursor = new Guacamole.Client.LayerContainer(0, 0);
117 cursor.getLayer().setChannelMask(Guacamole.Layer.SRC);
119 // Position cursor layer
120 var cursor_element = cursor.getElement();
121 cursor_element.style.position = "absolute";
122 cursor_element.style.left = "0px";
123 cursor_element.style.top = "0px";
125 // Add default layer and cursor to display
126 display.appendChild(default_layer_container.getElement());
127 display.appendChild(cursor.getElement());
129 // Initially, only default layer exists
130 var layers = [default_layer_container];
132 // No initial buffers
135 tunnel.onerror = function(message) {
136 if (guac_client.onerror)
137 guac_client.onerror(message);
140 function setState(state) {
141 if (state != currentState) {
142 currentState = state;
143 if (guac_client.onstatechange)
144 guac_client.onstatechange(currentState);
148 function isConnected() {
149 return currentState == STATE_CONNECTED
150 || currentState == STATE_WAITING;
153 var cursorHotspotX = 0;
154 var cursorHotspotY = 0;
159 function moveCursor(x, y) {
161 var element = cursor.getElement();
164 element.style.left = (x - cursorHotspotX) + "px";
165 element.style.top = (y - cursorHotspotY) + "px";
167 // Update stored position
173 guac_client.getDisplay = function() {
177 guac_client.sendKeyEvent = function(pressed, keysym) {
178 // Do not send requests if not connected
182 tunnel.sendMessage("key", keysym, pressed);
185 guac_client.sendMouseState = function(mouseState) {
187 // Do not send requests if not connected
191 // Update client-side cursor
199 if (mouseState.left) buttonMask |= 1;
200 if (mouseState.middle) buttonMask |= 2;
201 if (mouseState.right) buttonMask |= 4;
202 if (mouseState.up) buttonMask |= 8;
203 if (mouseState.down) buttonMask |= 16;
206 tunnel.sendMessage("mouse", mouseState.x, mouseState.y, buttonMask);
209 guac_client.setClipboard = function(data) {
211 // Do not send requests if not connected
215 tunnel.sendMessage("clipboard", data);
219 guac_client.onstatechange = null;
220 guac_client.onname = null;
221 guac_client.onerror = null;
222 guac_client.onclipboard = null;
225 function getBufferLayer(index) {
228 var buffer = buffers[index];
230 // Create buffer if necessary
231 if (buffer == null) {
232 buffer = new Guacamole.Layer(0, 0);
234 buffers[index] = buffer;
241 function getLayerContainer(index) {
243 var layer = layers[index];
247 layer = new Guacamole.Client.LayerContainer(displayWidth, displayHeight);
248 layers[index] = layer;
250 // Get and position layer
251 var layer_element = layer.getElement();
252 layer_element.style.position = "absolute";
253 layer_element.style.left = "0px";
254 layer_element.style.top = "0px";
256 // Add to default layer container
257 default_layer_container.getElement().appendChild(layer_element);
265 function getLayer(index) {
267 // If buffer, just get layer
269 return getBufferLayer(index);
271 // Otherwise, retrieve layer from layer container
272 return getLayerContainer(index).getLayer();
276 var instructionHandlers = {
278 "error": function(parameters) {
279 if (guac_client.onerror) guac_client.onerror(parameters[0]);
280 guac_client.disconnect();
283 "name": function(parameters) {
284 if (guac_client.onname) guac_client.onname(parameters[0]);
287 "clipboard": function(parameters) {
288 if (guac_client.onclipboard) guac_client.onclipboard(parameters[0]);
291 "size": function(parameters) {
293 var layer_index = parseInt(parameters[0]);
294 var width = parseInt(parameters[1]);
295 var height = parseInt(parameters[2]);
297 // Only valid for layers (buffers auto-resize)
298 if (layer_index >= 0) {
301 var layer_container = getLayerContainer(layer_index);
302 layer_container.resize(width, height);
304 // If layer is default, resize display
305 if (layer_index == 0) {
307 displayWidth = width;
308 displayHeight = height;
310 // Update (set) display size
311 display.style.width = displayWidth + "px";
312 display.style.height = displayHeight + "px";
316 } // end if layer (not buffer)
320 "move": function(parameters) {
322 var layer_index = parseInt(parameters[0]);
323 var parent_index = parseInt(parameters[1]);
324 var x = parseInt(parameters[2]);
325 var y = parseInt(parameters[3]);
326 var z = parseInt(parameters[4]);
328 // Only valid for non-default layers
329 if (layer_index > 0 && parent_index >= 0) {
331 // Get container element
332 var layer_container = getLayerContainer(layer_index).getElement();
333 var parent = getLayerContainer(parent_index).getElement();
335 // Set parent if necessary
336 if (!(layer_container.parentNode === parent))
337 parent.appendChild(layer_container);
340 layer_container.style.left = x + "px";
341 layer_container.style.top = y + "px";
342 layer_container.style.zIndex = z;
348 "dispose": function(parameters) {
350 var layer_index = parseInt(parameters[0]);
352 // If visible layer, remove from parent
353 if (layer_index > 0) {
355 // Get container element
356 var layer_container = getLayerContainer(layer_index).getElement();
358 // Remove from parent
359 layer_container.parentNode.removeChild(layer_container);
362 delete layers[layer_index];
366 // If buffer, just delete reference
367 else if (layer_index < 0)
368 delete buffers[-1 - layer_index];
370 // Attempting to dispose the root layer currently has no effect.
374 "png": function(parameters) {
376 var channelMask = parseInt(parameters[0]);
377 var layer = getLayer(parseInt(parameters[1]));
378 var x = parseInt(parameters[2]);
379 var y = parseInt(parameters[3]);
380 var data = parameters[4];
382 layer.setChannelMask(channelMask);
387 "data:image/png;base64," + data
390 // If received first update, no longer waiting.
391 if (currentState == STATE_WAITING)
392 setState(STATE_CONNECTED);
396 "copy": function(parameters) {
398 var srcL = getLayer(parseInt(parameters[0]));
399 var srcX = parseInt(parameters[1]);
400 var srcY = parseInt(parameters[2]);
401 var srcWidth = parseInt(parameters[3]);
402 var srcHeight = parseInt(parameters[4]);
403 var channelMask = parseInt(parameters[5]);
404 var dstL = getLayer(parseInt(parameters[6]));
405 var dstX = parseInt(parameters[7]);
406 var dstY = parseInt(parameters[8]);
408 dstL.setChannelMask(channelMask);
422 "transfer": function(parameters) {
424 var srcL = getLayer(parseInt(parameters[0]));
425 var srcX = parseInt(parameters[1]);
426 var srcY = parseInt(parameters[2]);
427 var srcWidth = parseInt(parameters[3]);
428 var srcHeight = parseInt(parameters[4]);
429 var transferFunction = transferFunctions[parameters[5]];
430 var dstL = getLayer(parseInt(parameters[6]));
431 var dstX = parseInt(parameters[7]);
432 var dstY = parseInt(parameters[8]);
447 "rect": function(parameters) {
449 var channelMask = parseInt(parameters[0]);
450 var layer = getLayer(parseInt(parameters[1]));
451 var x = parseInt(parameters[2]);
452 var y = parseInt(parameters[3]);
453 var w = parseInt(parameters[4]);
454 var h = parseInt(parameters[5]);
455 var r = parseInt(parameters[6]);
456 var g = parseInt(parameters[7]);
457 var b = parseInt(parameters[8]);
458 var a = parseInt(parameters[9]);
460 layer.setChannelMask(channelMask);
469 "clip": function(parameters) {
471 var layer = getLayer(parseInt(parameters[0]));
472 var x = parseInt(parameters[1]);
473 var y = parseInt(parameters[2]);
474 var w = parseInt(parameters[3]);
475 var h = parseInt(parameters[4]);
477 layer.clipRect(x, y, w, h);
481 "cursor": function(parameters) {
483 cursorHotspotX = parseInt(parameters[0]);
484 cursorHotspotY = parseInt(parameters[1]);
485 var srcL = getLayer(parseInt(parameters[2]));
486 var srcX = parseInt(parameters[3]);
487 var srcY = parseInt(parameters[4]);
488 var srcWidth = parseInt(parameters[5]);
489 var srcHeight = parseInt(parameters[6]);
492 cursor.resize(srcWidth, srcHeight);
494 // Draw cursor to cursor layer
495 cursor.getLayer().copyRect(
505 // Update cursor position (hotspot may have changed)
506 moveCursor(cursorX, cursorY);
510 "sync": function(parameters) {
512 var timestamp = parameters[0];
514 // When all layers have finished rendering all instructions
515 // UP TO THIS POINT IN TIME, send sync response.
517 var layersToSync = 0;
518 function syncLayer() {
522 // Send sync response when layers are finished
523 if (layersToSync == 0) {
524 if (timestamp != currentTimestamp) {
525 tunnel.sendMessage("sync", timestamp);
526 currentTimestamp = timestamp;
532 // Count active, not-ready layers and install sync tracking hooks
533 for (var i=0; i<layers.length; i++) {
535 var layer = layers[i].getLayer();
536 if (layer && !layer.isReady()) {
538 layer.sync(syncLayer);
543 // If all layers are ready, then we didn't install any hooks.
544 // Send sync message now,
545 if (layersToSync == 0) {
546 if (timestamp != currentTimestamp) {
547 tunnel.sendMessage("sync", timestamp);
548 currentTimestamp = timestamp;
557 tunnel.oninstruction = function(opcode, parameters) {
559 var handler = instructionHandlers[opcode];
566 guac_client.disconnect = function() {
568 // Only attempt disconnection not disconnected.
569 if (currentState != STATE_DISCONNECTED
570 && currentState != STATE_DISCONNECTING) {
572 setState(STATE_DISCONNECTING);
576 window.clearInterval(pingInterval);
578 // Send disconnect message and disconnect
579 tunnel.sendMessage("disconnect");
581 setState(STATE_DISCONNECTED);
587 guac_client.connect = function(data) {
589 setState(STATE_CONNECTING);
592 tunnel.connect(data);
595 setState(STATE_IDLE);
599 // Ping every 5 seconds (ensure connection alive)
600 pingInterval = window.setInterval(function() {
601 tunnel.sendMessage("sync", currentTimestamp);
604 setState(STATE_WAITING);
611 * Simple container for Guacamole.Layer, allowing layers to be easily
612 * repositioned and nested. This allows certain operations to be accelerated
613 * through DOM manipulation, rather than raster operations.
617 * @param {Number} width The width of the Layer, in pixels. The canvas element
618 * backing this Layer will be given this width.
620 * @param {Number} height The height of the Layer, in pixels. The canvas element
621 * backing this Layer will be given this height.
623 Guacamole.Client.LayerContainer = function(width, height) {
626 * Reference to this LayerContainer.
629 var layer_container = this;
631 // Create layer with given size
632 var layer = new Guacamole.Layer(width, height);
634 // Set layer position
635 var canvas = layer.getCanvas();
636 canvas.style.position = "absolute";
637 canvas.style.left = "0px";
638 canvas.style.top = "0px";
640 // Create div with given size
641 var div = document.createElement("div");
642 div.appendChild(canvas);
643 div.style.width = width + "px";
644 div.style.height = height + "px";
647 * Changes the size of this LayerContainer and the contained Layer to the
648 * given width and height.
650 * @param {Number} width The new width to assign to this Layer.
651 * @param {Number} height The new height to assign to this Layer.
653 layer_container.resize = function(width, height) {
656 layer.resize(width, height);
658 // Resize containing div
659 div.style.width = width + "px";
660 div.style.height = height + "px";
665 * Returns the Layer contained within this LayerContainer.
666 * @returns {Guacamole.Layer} The Layer contained within this LayerContainer.
668 layer_container.getLayer = function() {
673 * Returns the element containing the Layer within this LayerContainer.
674 * @returns {Element} The element containing the Layer within this LayerContainer.
676 layer_container.getElement = function() {