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