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.
25 * Alternatively, the contents of this file may be used under the terms of
26 * either the GNU General Public License Version 2 or later (the "GPL"), or
27 * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
28 * in which case the provisions of the GPL or the LGPL are applicable instead
29 * of those above. If you wish to allow use of your version of this file only
30 * under the terms of either the GPL or the LGPL, and not to allow others to
31 * use your version of this file under the terms of the MPL, indicate your
32 * decision by deleting the provisions above and replace them with the notice
33 * and other provisions required by the GPL or the LGPL. If you do not delete
34 * the provisions above, a recipient may use your version of this file under
35 * the terms of any one of the MPL, the GPL or the LGPL.
37 * ***** END LICENSE BLOCK ***** */
39 // Guacamole namespace
40 var Guacamole = Guacamole || {};
44 * Guacamole protocol client. Given a display element and {@link Guacamole.Tunnel},
45 * automatically handles incoming and outgoing Guacamole instructions via the
46 * provided tunnel, updating the display using one or more canvas elements.
49 * @param {Guacamole.Tunnel} tunnel The tunnel to use to send and receive
50 * Guacamole instructions.
52 Guacamole.Client = function(tunnel) {
54 var guac_client = this;
57 var STATE_CONNECTING = 1;
58 var STATE_WAITING = 2;
59 var STATE_CONNECTED = 3;
60 var STATE_DISCONNECTING = 4;
61 var STATE_DISCONNECTED = 5;
63 var currentState = STATE_IDLE;
65 var currentTimestamp = 0;
66 var pingInterval = null;
69 var displayHeight = 0;
72 * Translation from Guacamole protocol line caps to Layer line caps.
81 * Translation from Guacamole protocol line caps to Layer line caps.
90 var display = document.createElement("div");
91 display.style.position = "relative";
92 display.style.width = displayWidth + "px";
93 display.style.height = displayHeight + "px";
95 // Create default layer
96 var default_layer_container = new Guacamole.Client.LayerContainer(displayWidth, displayHeight);
98 // Position default layer
99 var default_layer_container_element = default_layer_container.getElement();
100 default_layer_container_element.style.position = "absolute";
101 default_layer_container_element.style.left = "0px";
102 default_layer_container_element.style.top = "0px";
103 default_layer_container_element.style.overflow = "hidden";
105 // Create cursor layer
106 var cursor = new Guacamole.Client.LayerContainer(0, 0);
107 cursor.getLayer().setChannelMask(Guacamole.Layer.SRC);
109 // Position cursor layer
110 var cursor_element = cursor.getElement();
111 cursor_element.style.position = "absolute";
112 cursor_element.style.left = "0px";
113 cursor_element.style.top = "0px";
115 // Add default layer and cursor to display
116 display.appendChild(default_layer_container.getElement());
117 display.appendChild(cursor.getElement());
119 // Initially, only default layer exists
120 var layers = [default_layer_container];
122 // No initial buffers
125 tunnel.onerror = function(message) {
126 if (guac_client.onerror)
127 guac_client.onerror(message);
130 function setState(state) {
131 if (state != currentState) {
132 currentState = state;
133 if (guac_client.onstatechange)
134 guac_client.onstatechange(currentState);
138 function isConnected() {
139 return currentState == STATE_CONNECTED
140 || currentState == STATE_WAITING;
143 var cursorHotspotX = 0;
144 var cursorHotspotY = 0;
149 function moveCursor(x, y) {
151 var element = cursor.getElement();
154 element.style.left = (x - cursorHotspotX) + "px";
155 element.style.top = (y - cursorHotspotY) + "px";
157 // Update stored position
163 guac_client.getDisplay = function() {
167 guac_client.sendKeyEvent = function(pressed, keysym) {
168 // Do not send requests if not connected
172 tunnel.sendMessage("key", keysym, pressed);
175 guac_client.sendMouseState = function(mouseState) {
177 // Do not send requests if not connected
181 // Update client-side cursor
189 if (mouseState.left) buttonMask |= 1;
190 if (mouseState.middle) buttonMask |= 2;
191 if (mouseState.right) buttonMask |= 4;
192 if (mouseState.up) buttonMask |= 8;
193 if (mouseState.down) buttonMask |= 16;
196 tunnel.sendMessage("mouse", mouseState.x, mouseState.y, buttonMask);
199 guac_client.setClipboard = function(data) {
201 // Do not send requests if not connected
205 tunnel.sendMessage("clipboard", data);
209 guac_client.onstatechange = null;
210 guac_client.onname = null;
211 guac_client.onerror = null;
212 guac_client.onclipboard = null;
215 function getBufferLayer(index) {
218 var buffer = buffers[index];
220 // Create buffer if necessary
221 if (buffer == null) {
222 buffer = new Guacamole.Layer(0, 0);
224 buffers[index] = buffer;
231 function getLayerContainer(index) {
233 var layer = layers[index];
237 layer = new Guacamole.Client.LayerContainer(displayWidth, displayHeight);
238 layers[index] = layer;
240 // Get and position layer
241 var layer_element = layer.getElement();
242 layer_element.style.position = "absolute";
243 layer_element.style.left = "0px";
244 layer_element.style.top = "0px";
245 layer_element.style.overflow = "hidden";
247 // Add to default layer container
248 default_layer_container.getElement().appendChild(layer_element);
256 function getLayer(index) {
258 // If buffer, just get layer
260 return getBufferLayer(index);
262 // Otherwise, retrieve layer from layer container
263 return getLayerContainer(index).getLayer();
268 * Handlers for all defined layer properties.
270 var layerPropertyHandlers = {
272 "miter-limit": function(layer, value) {
273 layer.setMiterLimit(parseFloat(value));
279 * Handlers for all instruction opcodes receivable by a Guacamole protocol
282 var instructionHandlers = {
284 "arc": function(parameters) {
286 var layer = getLayer(parseInt(parameters[0]));
287 var x = parseInt(parameters[1]);
288 var y = parseInt(parameters[2]);
289 var radius = parseInt(parameters[3]);
290 var startAngle = parseFloat(parameters[4]);
291 var endAngle = parseFloat(parameters[5]);
292 var negative = parseInt(parameters[6]);
294 layer.arc(x, y, radius, startAngle, endAngle, negative != 0);
298 "cfill": function(parameters) {
300 var channelMask = parseInt(parameters[0]);
301 var layer = getLayer(parseInt(parameters[1]));
302 var r = parseInt(parameters[2]);
303 var g = parseInt(parameters[3]);
304 var b = parseInt(parameters[4]);
305 var a = parseInt(parameters[5]);
307 layer.setChannelMask(channelMask);
309 layer.fillColor(r, g, b, a);
313 "clip": function(parameters) {
315 var layer = getLayer(parseInt(parameters[0]));
321 "clipboard": function(parameters) {
322 if (guac_client.onclipboard) guac_client.onclipboard(parameters[0]);
325 "close": function(parameters) {
327 var layer = getLayer(parseInt(parameters[0]));
333 "copy": function(parameters) {
335 var srcL = getLayer(parseInt(parameters[0]));
336 var srcX = parseInt(parameters[1]);
337 var srcY = parseInt(parameters[2]);
338 var srcWidth = parseInt(parameters[3]);
339 var srcHeight = parseInt(parameters[4]);
340 var channelMask = parseInt(parameters[5]);
341 var dstL = getLayer(parseInt(parameters[6]));
342 var dstX = parseInt(parameters[7]);
343 var dstY = parseInt(parameters[8]);
345 dstL.setChannelMask(channelMask);
359 "cstroke": function(parameters) {
361 var channelMask = parseInt(parameters[0]);
362 var layer = getLayer(parseInt(parameters[1]));
363 var cap = lineCap[parseInt(parameters[2])];
364 var join = lineJoin[parseInt(parameters[3])];
365 var thickness = parseInt(parameters[4]);
366 var r = parseInt(parameters[5]);
367 var g = parseInt(parameters[6]);
368 var b = parseInt(parameters[7]);
369 var a = parseInt(parameters[8]);
371 layer.setChannelMask(channelMask);
373 layer.strokeColor(cap, join, thickness, r, g, b, a);
377 "cursor": function(parameters) {
379 cursorHotspotX = parseInt(parameters[0]);
380 cursorHotspotY = parseInt(parameters[1]);
381 var srcL = getLayer(parseInt(parameters[2]));
382 var srcX = parseInt(parameters[3]);
383 var srcY = parseInt(parameters[4]);
384 var srcWidth = parseInt(parameters[5]);
385 var srcHeight = parseInt(parameters[6]);
388 cursor.resize(srcWidth, srcHeight);
390 // Draw cursor to cursor layer
391 cursor.getLayer().copy(
401 // Update cursor position (hotspot may have changed)
402 moveCursor(cursorX, cursorY);
406 "curve": function(parameters) {
408 var layer = getLayer(parseInt(parameters[0]));
409 var cp1x = parseInt(parameters[1]);
410 var cp1y = parseInt(parameters[2]);
411 var cp2x = parseInt(parameters[3]);
412 var cp2y = parseInt(parameters[4]);
413 var x = parseInt(parameters[5]);
414 var y = parseInt(parameters[6]);
416 layer.curveTo(cp1x, cp1y, cp2x, cp2y, x, y);
420 "dispose": function(parameters) {
422 var layer_index = parseInt(parameters[0]);
424 // If visible layer, remove from parent
425 if (layer_index > 0) {
427 // Get container element
428 var layer_container = getLayerContainer(layer_index).getElement();
430 // Remove from parent
431 layer_container.parentNode.removeChild(layer_container);
434 delete layers[layer_index];
438 // If buffer, just delete reference
439 else if (layer_index < 0)
440 delete buffers[-1 - layer_index];
442 // Attempting to dispose the root layer currently has no effect.
446 "distort": function(parameters) {
448 var layer_index = parseInt(parameters[0]);
449 var a = parseFloat(parameters[1]);
450 var b = parseFloat(parameters[2]);
451 var c = parseFloat(parameters[3]);
452 var d = parseFloat(parameters[4]);
453 var e = parseFloat(parameters[5]);
454 var f = parseFloat(parameters[6]);
456 // Only valid for visible layers (not buffers)
457 if (layer_index >= 0) {
459 // Get container element
460 var layer_container = getLayerContainer(layer_index).getElement();
462 // Set layer transform
463 layer_container.style.transform =
464 layer_container.style.WebkitTransform =
465 layer_container.style.MozTransform =
466 layer_container.style.OTransform =
467 layer_container.style.msTransform =
474 "matrix(" + a + "," + b + "," + c + "," + d + "," + e + "," + f + ")";
480 "error": function(parameters) {
481 if (guac_client.onerror) guac_client.onerror(parameters[0]);
482 guac_client.disconnect();
485 "identity": function(parameters) {
487 var layer = getLayer(parseInt(parameters[0]));
489 layer.setTransform(1, 0, 0, 1, 0, 0);
493 "lfill": function(parameters) {
495 var channelMask = parseInt(parameters[0]);
496 var layer = getLayer(parseInt(parameters[1]));
497 var srcLayer = getLayer(parseInt(parameters[2]));
499 layer.setChannelMask(channelMask);
501 layer.fillLayer(srcLayer);
505 "line": function(parameters) {
507 var layer = getLayer(parseInt(parameters[0]));
508 var x = parseInt(parameters[1]);
509 var y = parseInt(parameters[2]);
515 "lstroke": function(parameters) {
517 var channelMask = parseInt(parameters[0]);
518 var layer = getLayer(parseInt(parameters[1]));
519 var srcLayer = getLayer(parseInt(parameters[2]));
521 layer.setChannelMask(channelMask);
523 layer.strokeLayer(srcLayer);
527 "move": function(parameters) {
529 var layer_index = parseInt(parameters[0]);
530 var parent_index = parseInt(parameters[1]);
531 var x = parseInt(parameters[2]);
532 var y = parseInt(parameters[3]);
533 var z = parseInt(parameters[4]);
535 // Only valid for non-default layers
536 if (layer_index > 0 && parent_index >= 0) {
538 // Get container element
539 var layer_container = getLayerContainer(layer_index).getElement();
540 var parent = getLayerContainer(parent_index).getElement();
542 // Set parent if necessary
543 if (!(layer_container.parentNode === parent))
544 parent.appendChild(layer_container);
547 layer_container.style.left = x + "px";
548 layer_container.style.top = y + "px";
549 layer_container.style.zIndex = z;
555 "name": function(parameters) {
556 if (guac_client.onname) guac_client.onname(parameters[0]);
559 "png": function(parameters) {
561 var channelMask = parseInt(parameters[0]);
562 var layer = getLayer(parseInt(parameters[1]));
563 var x = parseInt(parameters[2]);
564 var y = parseInt(parameters[3]);
565 var data = parameters[4];
567 layer.setChannelMask(channelMask);
572 "data:image/png;base64," + data
575 // If received first update, no longer waiting.
576 if (currentState == STATE_WAITING)
577 setState(STATE_CONNECTED);
581 "pop": function(parameters) {
583 var layer = getLayer(parseInt(parameters[0]));
589 "push": function(parameters) {
591 var layer = getLayer(parseInt(parameters[0]));
597 "rect": function(parameters) {
599 var layer = getLayer(parseInt(parameters[0]));
600 var x = parseInt(parameters[1]);
601 var y = parseInt(parameters[2]);
602 var w = parseInt(parameters[3]);
603 var h = parseInt(parameters[4]);
605 layer.rect(x, y, w, h);
609 "reset": function(parameters) {
611 var layer = getLayer(parseInt(parameters[0]));
617 "set": function(parameters) {
619 var layer = getLayer(parseInt(parameters[0]));
620 var name = parameters[1];
621 var value = parameters[2];
623 // Call property handler if defined
624 var handler = layerPropertyHandlers[name];
626 handler(layer, value);
630 "shade": function(parameters) {
632 var layer_index = parseInt(parameters[0]);
633 var a = parseInt(parameters[1]);
635 // Only valid for visible layers (not buffers)
636 if (layer_index >= 0) {
638 // Get container element
639 var layer_container = getLayerContainer(layer_index).getElement();
642 layer_container.style.opacity = a/255.0;
648 "size": function(parameters) {
650 var layer_index = parseInt(parameters[0]);
651 var width = parseInt(parameters[1]);
652 var height = parseInt(parameters[2]);
655 var layer_container = getLayerContainer(layer_index);
656 layer_container.resize(width, height);
658 // If layer is default, resize display
659 if (layer_index == 0) {
661 displayWidth = width;
662 displayHeight = height;
664 // Update (set) display size
665 display.style.width = displayWidth + "px";
666 display.style.height = displayHeight + "px";
672 "start": function(parameters) {
674 var layer = getLayer(parseInt(parameters[0]));
675 var x = parseInt(parameters[1]);
676 var y = parseInt(parameters[2]);
682 "sync": function(parameters) {
684 var timestamp = parameters[0];
686 // When all layers have finished rendering all instructions
687 // UP TO THIS POINT IN TIME, send sync response.
689 var layersToSync = 0;
690 function syncLayer() {
694 // Send sync response when layers are finished
695 if (layersToSync == 0) {
696 if (timestamp != currentTimestamp) {
697 tunnel.sendMessage("sync", timestamp);
698 currentTimestamp = timestamp;
704 // Count active, not-ready layers and install sync tracking hooks
705 for (var i=0; i<layers.length; i++) {
707 var layer = layers[i].getLayer();
708 if (layer && !layer.isReady()) {
710 layer.sync(syncLayer);
715 // If all layers are ready, then we didn't install any hooks.
716 // Send sync message now,
717 if (layersToSync == 0) {
718 if (timestamp != currentTimestamp) {
719 tunnel.sendMessage("sync", timestamp);
720 currentTimestamp = timestamp;
726 "transfer": function(parameters) {
728 var srcL = getLayer(parseInt(parameters[0]));
729 var srcX = parseInt(parameters[1]);
730 var srcY = parseInt(parameters[2]);
731 var srcWidth = parseInt(parameters[3]);
732 var srcHeight = parseInt(parameters[4]);
733 var transferFunction = Guacamole.Client.DefaultTransferFunction[parameters[5]];
734 var dstL = getLayer(parseInt(parameters[6]));
735 var dstX = parseInt(parameters[7]);
736 var dstY = parseInt(parameters[8]);
751 "transform": function(parameters) {
753 var layer = getLayer(parseInt(parameters[0]));
754 var a = parseFloat(parameters[1]);
755 var b = parseFloat(parameters[2]);
756 var c = parseFloat(parameters[3]);
757 var d = parseFloat(parameters[4]);
758 var e = parseFloat(parameters[5]);
759 var f = parseFloat(parameters[6]);
761 layer.transform(a, b, c, d, e, f);
768 tunnel.oninstruction = function(opcode, parameters) {
770 var handler = instructionHandlers[opcode];
777 guac_client.disconnect = function() {
779 // Only attempt disconnection not disconnected.
780 if (currentState != STATE_DISCONNECTED
781 && currentState != STATE_DISCONNECTING) {
783 setState(STATE_DISCONNECTING);
787 window.clearInterval(pingInterval);
789 // Send disconnect message and disconnect
790 tunnel.sendMessage("disconnect");
792 setState(STATE_DISCONNECTED);
798 guac_client.connect = function(data) {
800 setState(STATE_CONNECTING);
803 tunnel.connect(data);
806 setState(STATE_IDLE);
810 // Ping every 5 seconds (ensure connection alive)
811 pingInterval = window.setInterval(function() {
812 tunnel.sendMessage("sync", currentTimestamp);
815 setState(STATE_WAITING);
822 * Simple container for Guacamole.Layer, allowing layers to be easily
823 * repositioned and nested. This allows certain operations to be accelerated
824 * through DOM manipulation, rather than raster operations.
828 * @param {Number} width The width of the Layer, in pixels. The canvas element
829 * backing this Layer will be given this width.
831 * @param {Number} height The height of the Layer, in pixels. The canvas element
832 * backing this Layer will be given this height.
834 Guacamole.Client.LayerContainer = function(width, height) {
837 * Reference to this LayerContainer.
840 var layer_container = this;
842 // Create layer with given size
843 var layer = new Guacamole.Layer(width, height);
845 // Set layer position
846 var canvas = layer.getCanvas();
847 canvas.style.position = "absolute";
848 canvas.style.left = "0px";
849 canvas.style.top = "0px";
851 // Create div with given size
852 var div = document.createElement("div");
853 div.appendChild(canvas);
854 div.style.width = width + "px";
855 div.style.height = height + "px";
858 * Changes the size of this LayerContainer and the contained Layer to the
859 * given width and height.
861 * @param {Number} width The new width to assign to this Layer.
862 * @param {Number} height The new height to assign to this Layer.
864 layer_container.resize = function(width, height) {
867 layer.resize(width, height);
869 // Resize containing div
870 div.style.width = width + "px";
871 div.style.height = height + "px";
876 * Returns the Layer contained within this LayerContainer.
877 * @returns {Guacamole.Layer} The Layer contained within this LayerContainer.
879 layer_container.getLayer = function() {
884 * Returns the element containing the Layer within this LayerContainer.
885 * @returns {Element} The element containing the Layer within this LayerContainer.
887 layer_container.getElement = function() {
894 * Map of all Guacamole binary raster operations to transfer functions.
897 Guacamole.Client.DefaultTransferFunction = {
900 0x0: function (src, dst) {
901 dst.red = dst.green = dst.blue = 0x00;
905 0xF: function (src, dst) {
906 dst.red = dst.green = dst.blue = 0xFF;
910 0x3: function (src, dst) {
912 dst.green = src.green;
914 dst.alpha = src.alpha;
918 0x5: function (src, dst) {
923 0xC: function (src, dst) {
924 dst.red = 0xFF & ~src.red;
925 dst.green = 0xFF & ~src.green;
926 dst.blue = 0xFF & ~src.blue;
927 dst.alpha = src.alpha;
931 0xA: function (src, dst) {
932 dst.red = 0xFF & ~dst.red;
933 dst.green = 0xFF & ~dst.green;
934 dst.blue = 0xFF & ~dst.blue;
938 0x1: function (src, dst) {
939 dst.red = ( src.red & dst.red);
940 dst.green = ( src.green & dst.green);
941 dst.blue = ( src.blue & dst.blue);
945 0xE: function (src, dst) {
946 dst.red = 0xFF & ~( src.red & dst.red);
947 dst.green = 0xFF & ~( src.green & dst.green);
948 dst.blue = 0xFF & ~( src.blue & dst.blue);
952 0x7: function (src, dst) {
953 dst.red = ( src.red | dst.red);
954 dst.green = ( src.green | dst.green);
955 dst.blue = ( src.blue | dst.blue);
959 0x8: function (src, dst) {
960 dst.red = 0xFF & ~( src.red | dst.red);
961 dst.green = 0xFF & ~( src.green | dst.green);
962 dst.blue = 0xFF & ~( src.blue | dst.blue);
966 0x6: function (src, dst) {
967 dst.red = ( src.red ^ dst.red);
968 dst.green = ( src.green ^ dst.green);
969 dst.blue = ( src.blue ^ dst.blue);
973 0x9: function (src, dst) {
974 dst.red = 0xFF & ~( src.red ^ dst.red);
975 dst.green = 0xFF & ~( src.green ^ dst.green);
976 dst.blue = 0xFF & ~( src.blue ^ dst.blue);
979 /* AND inverted source */
980 0x4: function (src, dst) {
981 dst.red = 0xFF & (~src.red & dst.red);
982 dst.green = 0xFF & (~src.green & dst.green);
983 dst.blue = 0xFF & (~src.blue & dst.blue);
986 /* OR inverted source */
987 0xD: function (src, dst) {
988 dst.red = 0xFF & (~src.red | dst.red);
989 dst.green = 0xFF & (~src.green | dst.green);
990 dst.blue = 0xFF & (~src.blue | dst.blue);
993 /* AND inverted destination */
994 0x2: function (src, dst) {
995 dst.red = 0xFF & ( src.red & ~dst.red);
996 dst.green = 0xFF & ( src.green & ~dst.green);
997 dst.blue = 0xFF & ( src.blue & ~dst.blue);
1000 /* OR inverted destination */
1001 0xB: function (src, dst) {
1002 dst.red = 0xFF & ( src.red | ~dst.red);
1003 dst.green = 0xFF & ( src.green | ~dst.green);
1004 dst.blue = 0xFF & ( src.blue | ~dst.blue);