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) {
152 cursor.translate(x - cursorHotspotX, y - cursorHotspotY);
154 // Update stored position
160 guac_client.getDisplay = function() {
164 guac_client.sendKeyEvent = function(pressed, keysym) {
165 // Do not send requests if not connected
169 tunnel.sendMessage("key", keysym, pressed);
172 guac_client.sendMouseState = function(mouseState) {
174 // Do not send requests if not connected
178 // Update client-side cursor
186 if (mouseState.left) buttonMask |= 1;
187 if (mouseState.middle) buttonMask |= 2;
188 if (mouseState.right) buttonMask |= 4;
189 if (mouseState.up) buttonMask |= 8;
190 if (mouseState.down) buttonMask |= 16;
193 tunnel.sendMessage("mouse", mouseState.x, mouseState.y, buttonMask);
196 guac_client.setClipboard = function(data) {
198 // Do not send requests if not connected
202 tunnel.sendMessage("clipboard", data);
206 guac_client.onstatechange = null;
207 guac_client.onname = null;
208 guac_client.onerror = null;
209 guac_client.onclipboard = null;
212 function getBufferLayer(index) {
215 var buffer = buffers[index];
217 // Create buffer if necessary
218 if (buffer == null) {
219 buffer = new Guacamole.Layer(0, 0);
221 buffers[index] = buffer;
228 function getLayerContainer(index) {
230 var layer = layers[index];
234 layer = new Guacamole.Client.LayerContainer(displayWidth, displayHeight);
235 layers[index] = layer;
237 // Get and position layer
238 var layer_element = layer.getElement();
239 layer_element.style.position = "absolute";
240 layer_element.style.left = "0px";
241 layer_element.style.top = "0px";
242 layer_element.style.overflow = "hidden";
244 // Add to default layer container
245 default_layer_container.getElement().appendChild(layer_element);
253 function getLayer(index) {
255 // If buffer, just get layer
257 return getBufferLayer(index);
259 // Otherwise, retrieve layer from layer container
260 return getLayerContainer(index).getLayer();
265 * Handlers for all defined layer properties.
267 var layerPropertyHandlers = {
269 "miter-limit": function(layer, value) {
270 layer.setMiterLimit(parseFloat(value));
276 * Handlers for all instruction opcodes receivable by a Guacamole protocol
279 var instructionHandlers = {
281 "arc": function(parameters) {
283 var layer = getLayer(parseInt(parameters[0]));
284 var x = parseInt(parameters[1]);
285 var y = parseInt(parameters[2]);
286 var radius = parseInt(parameters[3]);
287 var startAngle = parseFloat(parameters[4]);
288 var endAngle = parseFloat(parameters[5]);
289 var negative = parseInt(parameters[6]);
291 layer.arc(x, y, radius, startAngle, endAngle, negative != 0);
295 "cfill": function(parameters) {
297 var channelMask = parseInt(parameters[0]);
298 var layer = getLayer(parseInt(parameters[1]));
299 var r = parseInt(parameters[2]);
300 var g = parseInt(parameters[3]);
301 var b = parseInt(parameters[4]);
302 var a = parseInt(parameters[5]);
304 layer.setChannelMask(channelMask);
306 layer.fillColor(r, g, b, a);
310 "clip": function(parameters) {
312 var layer = getLayer(parseInt(parameters[0]));
318 "clipboard": function(parameters) {
319 if (guac_client.onclipboard) guac_client.onclipboard(parameters[0]);
322 "close": function(parameters) {
324 var layer = getLayer(parseInt(parameters[0]));
330 "copy": function(parameters) {
332 var srcL = getLayer(parseInt(parameters[0]));
333 var srcX = parseInt(parameters[1]);
334 var srcY = parseInt(parameters[2]);
335 var srcWidth = parseInt(parameters[3]);
336 var srcHeight = parseInt(parameters[4]);
337 var channelMask = parseInt(parameters[5]);
338 var dstL = getLayer(parseInt(parameters[6]));
339 var dstX = parseInt(parameters[7]);
340 var dstY = parseInt(parameters[8]);
342 dstL.setChannelMask(channelMask);
356 "cstroke": function(parameters) {
358 var channelMask = parseInt(parameters[0]);
359 var layer = getLayer(parseInt(parameters[1]));
360 var cap = lineCap[parseInt(parameters[2])];
361 var join = lineJoin[parseInt(parameters[3])];
362 var thickness = parseInt(parameters[4]);
363 var r = parseInt(parameters[5]);
364 var g = parseInt(parameters[6]);
365 var b = parseInt(parameters[7]);
366 var a = parseInt(parameters[8]);
368 layer.setChannelMask(channelMask);
370 layer.strokeColor(cap, join, thickness, r, g, b, a);
374 "cursor": function(parameters) {
376 cursorHotspotX = parseInt(parameters[0]);
377 cursorHotspotY = parseInt(parameters[1]);
378 var srcL = getLayer(parseInt(parameters[2]));
379 var srcX = parseInt(parameters[3]);
380 var srcY = parseInt(parameters[4]);
381 var srcWidth = parseInt(parameters[5]);
382 var srcHeight = parseInt(parameters[6]);
385 cursor.resize(srcWidth, srcHeight);
387 // Draw cursor to cursor layer
388 cursor.getLayer().copy(
398 // Update cursor position (hotspot may have changed)
399 moveCursor(cursorX, cursorY);
403 "curve": function(parameters) {
405 var layer = getLayer(parseInt(parameters[0]));
406 var cp1x = parseInt(parameters[1]);
407 var cp1y = parseInt(parameters[2]);
408 var cp2x = parseInt(parameters[3]);
409 var cp2y = parseInt(parameters[4]);
410 var x = parseInt(parameters[5]);
411 var y = parseInt(parameters[6]);
413 layer.curveTo(cp1x, cp1y, cp2x, cp2y, x, y);
417 "dispose": function(parameters) {
419 var layer_index = parseInt(parameters[0]);
421 // If visible layer, remove from parent
422 if (layer_index > 0) {
424 // Get container element
425 var layer_container = getLayerContainer(layer_index).getElement();
427 // Remove from parent
428 layer_container.parentNode.removeChild(layer_container);
431 delete layers[layer_index];
435 // If buffer, just delete reference
436 else if (layer_index < 0)
437 delete buffers[-1 - layer_index];
439 // Attempting to dispose the root layer currently has no effect.
443 "distort": function(parameters) {
445 var layer_index = parseInt(parameters[0]);
446 var a = parseFloat(parameters[1]);
447 var b = parseFloat(parameters[2]);
448 var c = parseFloat(parameters[3]);
449 var d = parseFloat(parameters[4]);
450 var e = parseFloat(parameters[5]);
451 var f = parseFloat(parameters[6]);
453 // Only valid for visible layers (not buffers)
454 if (layer_index >= 0) {
456 // Get container element
457 var layer_container = getLayerContainer(layer_index).getElement();
459 // Set layer transform
460 layer_container.transform(a, b, c, d, e, f);
466 "error": function(parameters) {
467 if (guac_client.onerror) guac_client.onerror(parameters[0]);
468 guac_client.disconnect();
471 "identity": function(parameters) {
473 var layer = getLayer(parseInt(parameters[0]));
475 layer.setTransform(1, 0, 0, 1, 0, 0);
479 "lfill": function(parameters) {
481 var channelMask = parseInt(parameters[0]);
482 var layer = getLayer(parseInt(parameters[1]));
483 var srcLayer = getLayer(parseInt(parameters[2]));
485 layer.setChannelMask(channelMask);
487 layer.fillLayer(srcLayer);
491 "line": function(parameters) {
493 var layer = getLayer(parseInt(parameters[0]));
494 var x = parseInt(parameters[1]);
495 var y = parseInt(parameters[2]);
501 "lstroke": function(parameters) {
503 var channelMask = parseInt(parameters[0]);
504 var layer = getLayer(parseInt(parameters[1]));
505 var srcLayer = getLayer(parseInt(parameters[2]));
507 layer.setChannelMask(channelMask);
509 layer.strokeLayer(srcLayer);
513 "move": function(parameters) {
515 var layer_index = parseInt(parameters[0]);
516 var parent_index = parseInt(parameters[1]);
517 var x = parseInt(parameters[2]);
518 var y = parseInt(parameters[3]);
519 var z = parseInt(parameters[4]);
521 // Only valid for non-default layers
522 if (layer_index > 0 && parent_index >= 0) {
524 // Get container element
525 var layer_container = getLayerContainer(layer_index);
526 var layer_container_element = layer_container.getElement();
527 var parent = getLayerContainer(parent_index).getElement();
529 // Set parent if necessary
530 if (!(layer_container_element.parentNode === parent))
531 parent.appendChild(layer_container_element);
534 layer_container.translate(x, y);
535 layer_container_element.style.zIndex = z;
541 "name": function(parameters) {
542 if (guac_client.onname) guac_client.onname(parameters[0]);
545 "png": function(parameters) {
547 var channelMask = parseInt(parameters[0]);
548 var layer = getLayer(parseInt(parameters[1]));
549 var x = parseInt(parameters[2]);
550 var y = parseInt(parameters[3]);
551 var data = parameters[4];
553 layer.setChannelMask(channelMask);
558 "data:image/png;base64," + data
561 // If received first update, no longer waiting.
562 if (currentState == STATE_WAITING)
563 setState(STATE_CONNECTED);
567 "pop": function(parameters) {
569 var layer = getLayer(parseInt(parameters[0]));
575 "push": function(parameters) {
577 var layer = getLayer(parseInt(parameters[0]));
583 "rect": function(parameters) {
585 var layer = getLayer(parseInt(parameters[0]));
586 var x = parseInt(parameters[1]);
587 var y = parseInt(parameters[2]);
588 var w = parseInt(parameters[3]);
589 var h = parseInt(parameters[4]);
591 layer.rect(x, y, w, h);
595 "reset": function(parameters) {
597 var layer = getLayer(parseInt(parameters[0]));
603 "set": function(parameters) {
605 var layer = getLayer(parseInt(parameters[0]));
606 var name = parameters[1];
607 var value = parameters[2];
609 // Call property handler if defined
610 var handler = layerPropertyHandlers[name];
612 handler(layer, value);
616 "shade": function(parameters) {
618 var layer_index = parseInt(parameters[0]);
619 var a = parseInt(parameters[1]);
621 // Only valid for visible layers (not buffers)
622 if (layer_index >= 0) {
624 // Get container element
625 var layer_container = getLayerContainer(layer_index).getElement();
628 layer_container.style.opacity = a/255.0;
634 "size": function(parameters) {
636 var layer_index = parseInt(parameters[0]);
637 var width = parseInt(parameters[1]);
638 var height = parseInt(parameters[2]);
641 var layer_container = getLayerContainer(layer_index);
642 layer_container.resize(width, height);
644 // If layer is default, resize display
645 if (layer_index == 0) {
647 displayWidth = width;
648 displayHeight = height;
650 // Update (set) display size
651 display.style.width = displayWidth + "px";
652 display.style.height = displayHeight + "px";
658 "start": function(parameters) {
660 var layer = getLayer(parseInt(parameters[0]));
661 var x = parseInt(parameters[1]);
662 var y = parseInt(parameters[2]);
668 "sync": function(parameters) {
670 var timestamp = parameters[0];
672 // When all layers have finished rendering all instructions
673 // UP TO THIS POINT IN TIME, send sync response.
675 var layersToSync = 0;
676 function syncLayer() {
680 // Send sync response when layers are finished
681 if (layersToSync == 0) {
682 if (timestamp != currentTimestamp) {
683 tunnel.sendMessage("sync", timestamp);
684 currentTimestamp = timestamp;
690 // Count active, not-ready layers and install sync tracking hooks
691 for (var i=0; i<layers.length; i++) {
693 var layer = layers[i].getLayer();
694 if (layer && !layer.isReady()) {
696 layer.sync(syncLayer);
701 // If all layers are ready, then we didn't install any hooks.
702 // Send sync message now,
703 if (layersToSync == 0) {
704 if (timestamp != currentTimestamp) {
705 tunnel.sendMessage("sync", timestamp);
706 currentTimestamp = timestamp;
712 "transfer": function(parameters) {
714 var srcL = getLayer(parseInt(parameters[0]));
715 var srcX = parseInt(parameters[1]);
716 var srcY = parseInt(parameters[2]);
717 var srcWidth = parseInt(parameters[3]);
718 var srcHeight = parseInt(parameters[4]);
719 var transferFunction = Guacamole.Client.DefaultTransferFunction[parameters[5]];
720 var dstL = getLayer(parseInt(parameters[6]));
721 var dstX = parseInt(parameters[7]);
722 var dstY = parseInt(parameters[8]);
737 "transform": function(parameters) {
739 var layer = getLayer(parseInt(parameters[0]));
740 var a = parseFloat(parameters[1]);
741 var b = parseFloat(parameters[2]);
742 var c = parseFloat(parameters[3]);
743 var d = parseFloat(parameters[4]);
744 var e = parseFloat(parameters[5]);
745 var f = parseFloat(parameters[6]);
747 layer.transform(a, b, c, d, e, f);
754 tunnel.oninstruction = function(opcode, parameters) {
756 var handler = instructionHandlers[opcode];
763 guac_client.disconnect = function() {
765 // Only attempt disconnection not disconnected.
766 if (currentState != STATE_DISCONNECTED
767 && currentState != STATE_DISCONNECTING) {
769 setState(STATE_DISCONNECTING);
773 window.clearInterval(pingInterval);
775 // Send disconnect message and disconnect
776 tunnel.sendMessage("disconnect");
778 setState(STATE_DISCONNECTED);
784 guac_client.connect = function(data) {
786 setState(STATE_CONNECTING);
789 tunnel.connect(data);
792 setState(STATE_IDLE);
796 // Ping every 5 seconds (ensure connection alive)
797 pingInterval = window.setInterval(function() {
798 tunnel.sendMessage("sync", currentTimestamp);
801 setState(STATE_WAITING);
808 * Simple container for Guacamole.Layer, allowing layers to be easily
809 * repositioned and nested. This allows certain operations to be accelerated
810 * through DOM manipulation, rather than raster operations.
814 * @param {Number} width The width of the Layer, in pixels. The canvas element
815 * backing this Layer will be given this width.
817 * @param {Number} height The height of the Layer, in pixels. The canvas element
818 * backing this Layer will be given this height.
820 Guacamole.Client.LayerContainer = function(width, height) {
823 * Reference to this LayerContainer.
826 var layer_container = this;
828 // Create layer with given size
829 var layer = new Guacamole.Layer(width, height);
831 // Set layer position
832 var canvas = layer.getCanvas();
833 canvas.style.position = "absolute";
834 canvas.style.left = "0px";
835 canvas.style.top = "0px";
837 // Create div with given size
838 var div = document.createElement("div");
839 div.appendChild(canvas);
840 div.style.width = width + "px";
841 div.style.height = height + "px";
844 * Changes the size of this LayerContainer and the contained Layer to the
845 * given width and height.
847 * @param {Number} width The new width to assign to this Layer.
848 * @param {Number} height The new height to assign to this Layer.
850 layer_container.resize = function(width, height) {
853 layer.resize(width, height);
855 // Resize containing div
856 div.style.width = width + "px";
857 div.style.height = height + "px";
862 * Returns the Layer contained within this LayerContainer.
863 * @returns {Guacamole.Layer} The Layer contained within this LayerContainer.
865 layer_container.getLayer = function() {
870 * Returns the element containing the Layer within this LayerContainer.
871 * @returns {Element} The element containing the Layer within this LayerContainer.
873 layer_container.getElement = function() {
878 * The translation component of this LayerContainer's transform.
880 var translate = "translate(0px, 0px)"; // (0, 0)
883 * The arbitrary matrix component of this LayerContainer's transform.
885 var matrix = "matrix(1, 0, 0, 1, 0, 0)"; // Identity
888 * Moves the upper-left corner of this LayerContainer to the given X and Y
891 * @param {Number} x The X coordinate to move to.
892 * @param {Number} y The Y coordinate to move to.
894 layer_container.translate = function(x, y) {
896 // Generate translation
897 translate = "translate("
901 // Set layer transform
902 div.style.transform =
903 div.style.WebkitTransform =
904 div.style.MozTransform =
905 div.style.OTransform =
906 div.style.msTransform =
908 translate + " " + matrix;
913 * Applies the given affine transform (defined with six values from the
914 * transform's matrix).
916 * @param {Number} a The first value in the affine transform's matrix.
917 * @param {Number} b The second value in the affine transform's matrix.
918 * @param {Number} c The third value in the affine transform's matrix.
919 * @param {Number} d The fourth value in the affine transform's matrix.
920 * @param {Number} e The fifth value in the affine transform's matrix.
921 * @param {Number} f The sixth value in the affine transform's matrix.
923 layer_container.transform = function(a, b, c, d, e, f) {
925 // Generate matrix transformation
933 "matrix(" + a + "," + b + "," + c + "," + d + "," + e + "," + f + ")";
935 // Set layer transform
936 div.style.transform =
937 div.style.WebkitTransform =
938 div.style.MozTransform =
939 div.style.OTransform =
940 div.style.msTransform =
942 translate + " " + matrix;
949 * Map of all Guacamole binary raster operations to transfer functions.
952 Guacamole.Client.DefaultTransferFunction = {
955 0x0: function (src, dst) {
956 dst.red = dst.green = dst.blue = 0x00;
960 0xF: function (src, dst) {
961 dst.red = dst.green = dst.blue = 0xFF;
965 0x3: function (src, dst) {
967 dst.green = src.green;
969 dst.alpha = src.alpha;
973 0x5: function (src, dst) {
978 0xC: function (src, dst) {
979 dst.red = 0xFF & ~src.red;
980 dst.green = 0xFF & ~src.green;
981 dst.blue = 0xFF & ~src.blue;
982 dst.alpha = src.alpha;
986 0xA: function (src, dst) {
987 dst.red = 0xFF & ~dst.red;
988 dst.green = 0xFF & ~dst.green;
989 dst.blue = 0xFF & ~dst.blue;
993 0x1: function (src, dst) {
994 dst.red = ( src.red & dst.red);
995 dst.green = ( src.green & dst.green);
996 dst.blue = ( src.blue & dst.blue);
1000 0xE: function (src, dst) {
1001 dst.red = 0xFF & ~( src.red & dst.red);
1002 dst.green = 0xFF & ~( src.green & dst.green);
1003 dst.blue = 0xFF & ~( src.blue & dst.blue);
1007 0x7: function (src, dst) {
1008 dst.red = ( src.red | dst.red);
1009 dst.green = ( src.green | dst.green);
1010 dst.blue = ( src.blue | dst.blue);
1014 0x8: function (src, dst) {
1015 dst.red = 0xFF & ~( src.red | dst.red);
1016 dst.green = 0xFF & ~( src.green | dst.green);
1017 dst.blue = 0xFF & ~( src.blue | dst.blue);
1021 0x6: function (src, dst) {
1022 dst.red = ( src.red ^ dst.red);
1023 dst.green = ( src.green ^ dst.green);
1024 dst.blue = ( src.blue ^ dst.blue);
1028 0x9: function (src, dst) {
1029 dst.red = 0xFF & ~( src.red ^ dst.red);
1030 dst.green = 0xFF & ~( src.green ^ dst.green);
1031 dst.blue = 0xFF & ~( src.blue ^ dst.blue);
1034 /* AND inverted source */
1035 0x4: function (src, dst) {
1036 dst.red = 0xFF & (~src.red & dst.red);
1037 dst.green = 0xFF & (~src.green & dst.green);
1038 dst.blue = 0xFF & (~src.blue & dst.blue);
1041 /* OR inverted source */
1042 0xD: function (src, dst) {
1043 dst.red = 0xFF & (~src.red | dst.red);
1044 dst.green = 0xFF & (~src.green | dst.green);
1045 dst.blue = 0xFF & (~src.blue | dst.blue);
1048 /* AND inverted destination */
1049 0x2: function (src, dst) {
1050 dst.red = 0xFF & ( src.red & ~dst.red);
1051 dst.green = 0xFF & ( src.green & ~dst.green);
1052 dst.blue = 0xFF & ( src.blue & ~dst.blue);
1055 /* OR inverted destination */
1056 0xB: function (src, dst) {
1057 dst.red = 0xFF & ( src.red | ~dst.red);
1058 dst.green = 0xFF & ( src.green | ~dst.green);
1059 dst.blue = 0xFF & ( src.blue | ~dst.blue);