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