b7328bad6b248fa658c09bcf1bc6fc590569b56a
[guacamole-common-js.git] / src / main / resources / guacamole.js
1
2 /* ***** BEGIN LICENSE BLOCK *****
3  * Version: MPL 1.1/GPL 2.0/LGPL 2.1
4  *
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/
9  *
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
13  * License.
14  *
15  * The Original Code is guacamole-common-js.
16  *
17  * The Initial Developer of the Original Code is
18  * Michael Jumper.
19  * Portions created by the Initial Developer are Copyright (C) 2010
20  * the Initial Developer. All Rights Reserved.
21  *
22  * Contributor(s):
23  * Matt Hortman
24  *
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.
36  *
37  * ***** END LICENSE BLOCK ***** */
38
39 // Guacamole namespace
40 var Guacamole = Guacamole || {};
41
42
43 /**
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.
47  * 
48  * @constructor
49  * @param {Guacamole.Tunnel} tunnel The tunnel to use to send and receive
50  *                                  Guacamole instructions.
51  */
52 Guacamole.Client = function(tunnel) {
53
54     var guac_client = this;
55
56     var STATE_IDLE          = 0;
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;
62
63     var currentState = STATE_IDLE;
64     
65     var currentTimestamp = 0;
66     var pingInterval = null;
67
68     var displayWidth = 0;
69     var displayHeight = 0;
70
71     /**
72      * Translation from Guacamole protocol line caps to Layer line caps.
73      */
74     var lineCap = {
75         0: "butt",
76         1: "round",
77         2: "square"
78     };
79
80     /**
81      * Translation from Guacamole protocol line caps to Layer line caps.
82      */
83     var lineJoin = {
84         0: "bevel",
85         1: "miter",
86         2: "round"
87     };
88
89     // Create display
90     var display = document.createElement("div");
91     display.style.position = "relative";
92     display.style.width = displayWidth + "px";
93     display.style.height = displayHeight + "px";
94
95     // Create default layer
96     var default_layer_container = new Guacamole.Client.LayerContainer(displayWidth, displayHeight);
97
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
104     // Create cursor layer
105     var cursor = new Guacamole.Client.LayerContainer(0, 0);
106     cursor.getLayer().setChannelMask(Guacamole.Layer.SRC);
107
108     // Position cursor layer
109     var cursor_element = cursor.getElement();
110     cursor_element.style.position = "absolute";
111     cursor_element.style.left = "0px";
112     cursor_element.style.top  = "0px";
113
114     // Add default layer and cursor to display
115     display.appendChild(default_layer_container.getElement());
116     display.appendChild(cursor.getElement());
117
118     // Initially, only default layer exists
119     var layers =  [default_layer_container];
120
121     // No initial buffers
122     var buffers = [];
123
124     tunnel.onerror = function(message) {
125         if (guac_client.onerror)
126             guac_client.onerror(message);
127     };
128
129     function setState(state) {
130         if (state != currentState) {
131             currentState = state;
132             if (guac_client.onstatechange)
133                 guac_client.onstatechange(currentState);
134         }
135     }
136
137     function isConnected() {
138         return currentState == STATE_CONNECTED
139             || currentState == STATE_WAITING;
140     }
141
142     var cursorHotspotX = 0;
143     var cursorHotspotY = 0;
144
145     var cursorX = 0;
146     var cursorY = 0;
147
148     function moveCursor(x, y) {
149
150         var element = cursor.getElement();
151
152         // Update rect
153         element.style.left = (x - cursorHotspotX) + "px";
154         element.style.top  = (y - cursorHotspotY) + "px";
155
156         // Update stored position
157         cursorX = x;
158         cursorY = y;
159
160     }
161
162     guac_client.getDisplay = function() {
163         return display;
164     };
165
166     guac_client.sendKeyEvent = function(pressed, keysym) {
167         // Do not send requests if not connected
168         if (!isConnected())
169             return;
170
171         tunnel.sendMessage("key", keysym, pressed);
172     };
173
174     guac_client.sendMouseState = function(mouseState) {
175
176         // Do not send requests if not connected
177         if (!isConnected())
178             return;
179
180         // Update client-side cursor
181         moveCursor(
182             mouseState.x,
183             mouseState.y
184         );
185
186         // Build mask
187         var buttonMask = 0;
188         if (mouseState.left)   buttonMask |= 1;
189         if (mouseState.middle) buttonMask |= 2;
190         if (mouseState.right)  buttonMask |= 4;
191         if (mouseState.up)     buttonMask |= 8;
192         if (mouseState.down)   buttonMask |= 16;
193
194         // Send message
195         tunnel.sendMessage("mouse", mouseState.x, mouseState.y, buttonMask);
196     };
197
198     guac_client.setClipboard = function(data) {
199
200         // Do not send requests if not connected
201         if (!isConnected())
202             return;
203
204         tunnel.sendMessage("clipboard", data);
205     };
206
207     // Handlers
208     guac_client.onstatechange = null;
209     guac_client.onname = null;
210     guac_client.onerror = null;
211     guac_client.onclipboard = null;
212
213     // Layers
214     function getBufferLayer(index) {
215
216         index = -1 - index;
217         var buffer = buffers[index];
218
219         // Create buffer if necessary
220         if (buffer == null) {
221             buffer = new Guacamole.Layer(0, 0);
222             buffer.autosize = 1;
223             buffers[index] = buffer;
224         }
225
226         return buffer;
227
228     }
229
230     function getLayerContainer(index) {
231
232         var layer = layers[index];
233         if (layer == null) {
234
235             // Add new layer
236             layer = new Guacamole.Client.LayerContainer(displayWidth, displayHeight);
237             layers[index] = layer;
238
239             // Get and position layer
240             var layer_element = layer.getElement();
241             layer_element.style.position = "absolute";
242             layer_element.style.left = "0px";
243             layer_element.style.top = "0px";
244
245             // Add to default layer container
246             default_layer_container.getElement().appendChild(layer_element);
247
248         }
249
250         return layer;
251
252     }
253
254     function getLayer(index) {
255        
256         // If buffer, just get layer
257         if (index < 0)
258             return getBufferLayer(index);
259
260         // Otherwise, retrieve layer from layer container
261         return getLayerContainer(index).getLayer();
262
263     }
264
265     var instructionHandlers = {
266
267         "cfill": function(parameters) {
268
269             var channelMask = parseInt(parameters[0]);
270             var layer = getLayer(parseInt(parameters[1]));
271             var r = parseInt(parameters[2]);
272             var g = parseInt(parameters[3]);
273             var b = parseInt(parameters[4]);
274             var a = parseInt(parameters[5]);
275
276             layer.setChannelMask(channelMask);
277
278             layer.fillColor(r, g, b, a);
279
280         },
281
282         "clip": function(parameters) {
283
284             var layer = getLayer(parseInt(parameters[0]));
285
286             layer.clip();
287
288         },
289
290         "clipboard": function(parameters) {
291             if (guac_client.onclipboard) guac_client.onclipboard(parameters[0]);
292         },
293
294         "copy": function(parameters) {
295
296             var srcL = getLayer(parseInt(parameters[0]));
297             var srcX = parseInt(parameters[1]);
298             var srcY = parseInt(parameters[2]);
299             var srcWidth = parseInt(parameters[3]);
300             var srcHeight = parseInt(parameters[4]);
301             var channelMask = parseInt(parameters[5]);
302             var dstL = getLayer(parseInt(parameters[6]));
303             var dstX = parseInt(parameters[7]);
304             var dstY = parseInt(parameters[8]);
305
306             dstL.setChannelMask(channelMask);
307
308             dstL.copy(
309                 srcL,
310                 srcX,
311                 srcY,
312                 srcWidth, 
313                 srcHeight, 
314                 dstX,
315                 dstY 
316             );
317
318         },
319
320         "cstroke": function(parameters) {
321
322             var channelMask = parseInt(parameters[0]);
323             var layer = getLayer(parseInt(parameters[1]));
324             var cap = lineCap[parseInt(parameters[2])];
325             var join = lineJoin[parseInt(parameters[3])];
326             var thickness = parseInt(parameters[4]);
327             var r = parseInt(parameters[5]);
328             var g = parseInt(parameters[6]);
329             var b = parseInt(parameters[7]);
330             var a = parseInt(parameters[8]);
331
332             layer.setChannelMask(channelMask);
333
334             layer.strokeColor(cap, join, thickness, r, g, b, a);
335
336         },
337
338         "cursor": function(parameters) {
339
340             cursorHotspotX = parseInt(parameters[0]);
341             cursorHotspotY = parseInt(parameters[1]);
342             var srcL = getLayer(parseInt(parameters[2]));
343             var srcX = parseInt(parameters[3]);
344             var srcY = parseInt(parameters[4]);
345             var srcWidth = parseInt(parameters[5]);
346             var srcHeight = parseInt(parameters[6]);
347
348             // Reset cursor size
349             cursor.resize(srcWidth, srcHeight);
350
351             // Draw cursor to cursor layer
352             cursor.getLayer().copy(
353                 srcL,
354                 srcX,
355                 srcY,
356                 srcWidth, 
357                 srcHeight, 
358                 0,
359                 0 
360             );
361
362             // Update cursor position (hotspot may have changed)
363             moveCursor(cursorX, cursorY);
364
365         },
366
367         "dispose": function(parameters) {
368             
369             var layer_index = parseInt(parameters[0]);
370
371             // If visible layer, remove from parent
372             if (layer_index > 0) {
373
374                 // Get container element
375                 var layer_container = getLayerContainer(layer_index).getElement();
376
377                 // Remove from parent
378                 layer_container.parentNode.removeChild(layer_container);
379
380                 // Delete reference
381                 delete layers[layer_index];
382
383             }
384
385             // If buffer, just delete reference
386             else if (layer_index < 0)
387                 delete buffers[-1 - layer_index];
388
389             // Attempting to dispose the root layer currently has no effect.
390
391         },
392
393         "error": function(parameters) {
394             if (guac_client.onerror) guac_client.onerror(parameters[0]);
395             guac_client.disconnect();
396         },
397
398         "line": function(parameters) {
399
400             var layer = getLayer(parseInt(parameters[0]));
401             var x = parseInt(parameters[1]);
402             var y = parseInt(parameters[2]);
403
404             layer.lineTo(x, y);
405
406         },
407
408         "move": function(parameters) {
409             
410             var layer_index = parseInt(parameters[0]);
411             var parent_index = parseInt(parameters[1]);
412             var x = parseInt(parameters[2]);
413             var y = parseInt(parameters[3]);
414             var z = parseInt(parameters[4]);
415
416             // Only valid for non-default layers
417             if (layer_index > 0 && parent_index >= 0) {
418
419                 // Get container element
420                 var layer_container = getLayerContainer(layer_index).getElement();
421                 var parent = getLayerContainer(parent_index).getElement();
422
423                 // Set parent if necessary
424                 if (!(layer_container.parentNode === parent))
425                     parent.appendChild(layer_container);
426
427                 // Move layer
428                 layer_container.style.left   = x + "px";
429                 layer_container.style.top    = y + "px";
430                 layer_container.style.zIndex = z;
431
432             }
433
434         },
435
436         "name": function(parameters) {
437             if (guac_client.onname) guac_client.onname(parameters[0]);
438         },
439
440         "png": function(parameters) {
441
442             var channelMask = parseInt(parameters[0]);
443             var layer = getLayer(parseInt(parameters[1]));
444             var x = parseInt(parameters[2]);
445             var y = parseInt(parameters[3]);
446             var data = parameters[4];
447
448             layer.setChannelMask(channelMask);
449
450             layer.draw(
451                 x,
452                 y,
453                 "data:image/png;base64," + data
454             );
455
456             // If received first update, no longer waiting.
457             if (currentState == STATE_WAITING)
458                 setState(STATE_CONNECTED);
459
460         },
461
462         "rect": function(parameters) {
463
464             var layer = getLayer(parseInt(parameters[0]));
465             var x = parseInt(parameters[1]);
466             var y = parseInt(parameters[2]);
467             var w = parseInt(parameters[3]);
468             var h = parseInt(parameters[4]);
469
470             layer.rect(x, y, w, h);
471
472         },
473         
474         "reset": function(parameters) {
475
476             var layer = getLayer(parseInt(parameters[0]));
477
478             layer.reset();
479
480         },
481  
482         "size": function(parameters) {
483
484             var layer_index = parseInt(parameters[0]);
485             var width = parseInt(parameters[1]);
486             var height = parseInt(parameters[2]);
487
488             // Resize layer
489             var layer_container = getLayerContainer(layer_index);
490             layer_container.resize(width, height);
491
492             // If layer is default, resize display
493             if (layer_index == 0) {
494
495                 displayWidth = width;
496                 displayHeight = height;
497
498                 // Update (set) display size
499                 display.style.width = displayWidth + "px";
500                 display.style.height = displayHeight + "px";
501
502             }
503
504         },
505         
506         "start": function(parameters) {
507
508             var layer = getLayer(parseInt(parameters[0]));
509             var x = parseInt(parameters[1]);
510             var y = parseInt(parameters[2]);
511
512             layer.moveTo(x, y);
513
514         },
515
516         "sync": function(parameters) {
517
518             var timestamp = parameters[0];
519
520             // When all layers have finished rendering all instructions
521             // UP TO THIS POINT IN TIME, send sync response.
522
523             var layersToSync = 0;
524             function syncLayer() {
525
526                 layersToSync--;
527
528                 // Send sync response when layers are finished
529                 if (layersToSync == 0) {
530                     if (timestamp != currentTimestamp) {
531                         tunnel.sendMessage("sync", timestamp);
532                         currentTimestamp = timestamp;
533                     }
534                 }
535
536             }
537
538             // Count active, not-ready layers and install sync tracking hooks
539             for (var i=0; i<layers.length; i++) {
540
541                 var layer = layers[i].getLayer();
542                 if (layer && !layer.isReady()) {
543                     layersToSync++;
544                     layer.sync(syncLayer);
545                 }
546
547             }
548
549             // If all layers are ready, then we didn't install any hooks.
550             // Send sync message now,
551             if (layersToSync == 0) {
552                 if (timestamp != currentTimestamp) {
553                     tunnel.sendMessage("sync", timestamp);
554                     currentTimestamp = timestamp;
555                 }
556             }
557
558         },
559
560         "transfer": function(parameters) {
561
562             var srcL = getLayer(parseInt(parameters[0]));
563             var srcX = parseInt(parameters[1]);
564             var srcY = parseInt(parameters[2]);
565             var srcWidth = parseInt(parameters[3]);
566             var srcHeight = parseInt(parameters[4]);
567             var transferFunction = Guacamole.Client.DefaultTransferFunction[parameters[5]];
568             var dstL = getLayer(parseInt(parameters[6]));
569             var dstX = parseInt(parameters[7]);
570             var dstY = parseInt(parameters[8]);
571
572             dstL.transfer(
573                 srcL,
574                 srcX,
575                 srcY,
576                 srcWidth, 
577                 srcHeight, 
578                 dstX,
579                 dstY,
580                 transferFunction
581             );
582
583         }
584       
585     };
586
587
588     tunnel.oninstruction = function(opcode, parameters) {
589
590         var handler = instructionHandlers[opcode];
591         if (handler)
592             handler(parameters);
593
594     };
595
596
597     guac_client.disconnect = function() {
598
599         // Only attempt disconnection not disconnected.
600         if (currentState != STATE_DISCONNECTED
601                 && currentState != STATE_DISCONNECTING) {
602
603             setState(STATE_DISCONNECTING);
604
605             // Stop ping
606             if (pingInterval)
607                 window.clearInterval(pingInterval);
608
609             // Send disconnect message and disconnect
610             tunnel.sendMessage("disconnect");
611             tunnel.disconnect();
612             setState(STATE_DISCONNECTED);
613
614         }
615
616     };
617     
618     guac_client.connect = function(data) {
619
620         setState(STATE_CONNECTING);
621
622         try {
623             tunnel.connect(data);
624         }
625         catch (e) {
626             setState(STATE_IDLE);
627             throw e;
628         }
629
630         // Ping every 5 seconds (ensure connection alive)
631         pingInterval = window.setInterval(function() {
632             tunnel.sendMessage("sync", currentTimestamp);
633         }, 5000);
634
635         setState(STATE_WAITING);
636     };
637
638 };
639
640
641 /**
642  * Simple container for Guacamole.Layer, allowing layers to be easily
643  * repositioned and nested. This allows certain operations to be accelerated
644  * through DOM manipulation, rather than raster operations.
645  * 
646  * @constructor
647  * 
648  * @param {Number} width The width of the Layer, in pixels. The canvas element
649  *                       backing this Layer will be given this width.
650  *                       
651  * @param {Number} height The height of the Layer, in pixels. The canvas element
652  *                        backing this Layer will be given this height.
653  */
654 Guacamole.Client.LayerContainer = function(width, height) {
655
656     /**
657      * Reference to this LayerContainer.
658      * @private
659      */
660     var layer_container = this;
661
662     // Create layer with given size
663     var layer = new Guacamole.Layer(width, height);
664
665     // Set layer position
666     var canvas = layer.getCanvas();
667     canvas.style.position = "absolute";
668     canvas.style.left = "0px";
669     canvas.style.top = "0px";
670
671     // Create div with given size
672     var div = document.createElement("div");
673     div.appendChild(canvas);
674     div.style.width = width + "px";
675     div.style.height = height + "px";
676
677     /**
678      * Changes the size of this LayerContainer and the contained Layer to the
679      * given width and height.
680      * 
681      * @param {Number} width The new width to assign to this Layer.
682      * @param {Number} height The new height to assign to this Layer.
683      */
684     layer_container.resize = function(width, height) {
685
686         // Resize layer
687         layer.resize(width, height);
688
689         // Resize containing div
690         div.style.width = width + "px";
691         div.style.height = height + "px";
692
693     };
694   
695     /**
696      * Returns the Layer contained within this LayerContainer.
697      * @returns {Guacamole.Layer} The Layer contained within this LayerContainer.
698      */
699     layer_container.getLayer = function() {
700         return layer;
701     };
702
703     /**
704      * Returns the element containing the Layer within this LayerContainer.
705      * @returns {Element} The element containing the Layer within this LayerContainer.
706      */
707     layer_container.getElement = function() {
708         return div;
709     };
710
711 };
712
713 /**
714  * Map of all Guacamole binary raster operations to transfer functions.
715  * @private
716  */
717 Guacamole.Client.DefaultTransferFunction = {
718
719     /* BLACK */
720     0x0: function (src, dst) {
721         dst.red = dst.green = dst.blue = 0x00;
722     },
723
724     /* WHITE */
725     0xF: function (src, dst) {
726         dst.red = dst.green = dst.blue = 0xFF;
727     },
728
729     /* SRC */
730     0x3: function (src, dst) {
731         dst.red   = src.red;
732         dst.green = src.green;
733         dst.blue  = src.blue;
734         dst.alpha = src.alpha;
735     },
736
737     /* DEST (no-op) */
738     0x5: function (src, dst) {
739         // Do nothing
740     },
741
742     /* Invert SRC */
743     0xC: function (src, dst) {
744         dst.red   = 0xFF & ~src.red;
745         dst.green = 0xFF & ~src.green;
746         dst.blue  = 0xFF & ~src.blue;
747         dst.alpha =  src.alpha;
748     },
749     
750     /* Invert DEST */
751     0xA: function (src, dst) {
752         dst.red   = 0xFF & ~dst.red;
753         dst.green = 0xFF & ~dst.green;
754         dst.blue  = 0xFF & ~dst.blue;
755     },
756
757     /* AND */
758     0x1: function (src, dst) {
759         dst.red   =  ( src.red   &  dst.red);
760         dst.green =  ( src.green &  dst.green);
761         dst.blue  =  ( src.blue  &  dst.blue);
762     },
763
764     /* NAND */
765     0xE: function (src, dst) {
766         dst.red   = 0xFF & ~( src.red   &  dst.red);
767         dst.green = 0xFF & ~( src.green &  dst.green);
768         dst.blue  = 0xFF & ~( src.blue  &  dst.blue);
769     },
770
771     /* OR */
772     0x7: function (src, dst) {
773         dst.red   =  ( src.red   |  dst.red);
774         dst.green =  ( src.green |  dst.green);
775         dst.blue  =  ( src.blue  |  dst.blue);
776     },
777
778     /* NOR */
779     0x8: function (src, dst) {
780         dst.red   = 0xFF & ~( src.red   |  dst.red);
781         dst.green = 0xFF & ~( src.green |  dst.green);
782         dst.blue  = 0xFF & ~( src.blue  |  dst.blue);
783     },
784
785     /* XOR */
786     0x6: function (src, dst) {
787         dst.red   =  ( src.red   ^  dst.red);
788         dst.green =  ( src.green ^  dst.green);
789         dst.blue  =  ( src.blue  ^  dst.blue);
790     },
791
792     /* XNOR */
793     0x9: function (src, dst) {
794         dst.red   = 0xFF & ~( src.red   ^  dst.red);
795         dst.green = 0xFF & ~( src.green ^  dst.green);
796         dst.blue  = 0xFF & ~( src.blue  ^  dst.blue);
797     },
798
799     /* AND inverted source */
800     0x4: function (src, dst) {
801         dst.red   =  0xFF & (~src.red   &  dst.red);
802         dst.green =  0xFF & (~src.green &  dst.green);
803         dst.blue  =  0xFF & (~src.blue  &  dst.blue);
804     },
805
806     /* OR inverted source */
807     0xD: function (src, dst) {
808         dst.red   =  0xFF & (~src.red   |  dst.red);
809         dst.green =  0xFF & (~src.green |  dst.green);
810         dst.blue  =  0xFF & (~src.blue  |  dst.blue);
811     },
812
813     /* AND inverted destination */
814     0x2: function (src, dst) {
815         dst.red   =  0xFF & ( src.red   & ~dst.red);
816         dst.green =  0xFF & ( src.green & ~dst.green);
817         dst.blue  =  0xFF & ( src.blue  & ~dst.blue);
818     },
819
820     /* OR inverted destination */
821     0xB: function (src, dst) {
822         dst.red   =  0xFF & ( src.red   | ~dst.red);
823         dst.green =  0xFF & ( src.green | ~dst.green);
824         dst.blue  =  0xFF & ( src.blue  | ~dst.blue);
825     }
826
827 };