e126348fe318254b43a6c0dc3da5034cac64e620
[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     // Create cursor layer
80     var cursor = new Guacamole.Client.LayerContainer(displayWidth, displayHeight);
81
82     // Add default layer and cursor to display
83     display.appendChild(default_layer_container.getElement());
84     display.appendChild(cursor.getElement());
85
86     // Initially, only default layer exists
87     var layers =  [default_layer_container];
88
89     // No initial buffers
90     var buffers = [];
91
92     tunnel.onerror = function(message) {
93         if (guac_client.onerror)
94             guac_client.onerror(message);
95     };
96
97     function setState(state) {
98         if (state != currentState) {
99             currentState = state;
100             if (guac_client.onstatechange)
101                 guac_client.onstatechange(currentState);
102         }
103     }
104
105     function isConnected() {
106         return currentState == STATE_CONNECTED
107             || currentState == STATE_WAITING;
108     }
109
110     var cursorImage = null;
111     var cursorHotspotX = 0;
112     var cursorHotspotY = 0;
113
114     var cursorRectX = 0;
115     var cursorRectY = 0;
116     var cursorRectW = 0;
117     var cursorRectH = 0;
118
119     var cursorHidden = 0;
120
121     function redrawCursor(x, y) {
122
123         // Hide hardware cursor
124         if (cursorHidden == 0) {
125             display.className += " guac-hide-cursor";
126             cursorHidden = 1;
127         }
128
129         // Erase old cursor
130         cursor.clearRect(cursorRectX, cursorRectY, cursorRectW, cursorRectH);
131
132         // Update rect
133         cursorRectX = x - cursorHotspotX;
134         cursorRectY = y - cursorHotspotY;
135         cursorRectW = cursorImage.width;
136         cursorRectH = cursorImage.height;
137
138         // Draw new cursor
139         cursor.drawImage(cursorRectX, cursorRectY, cursorImage);
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         // Draw client-side cursor
162         if (cursorImage != null) {
163             redrawCursor(
164                 mouseState.x,
165                 mouseState.y
166             );
167         }
168
169         // Build mask
170         var buttonMask = 0;
171         if (mouseState.left)   buttonMask |= 1;
172         if (mouseState.middle) buttonMask |= 2;
173         if (mouseState.right)  buttonMask |= 4;
174         if (mouseState.up)     buttonMask |= 8;
175         if (mouseState.down)   buttonMask |= 16;
176
177         // Send message
178         tunnel.sendMessage("mouse", mouseState.x, mouseState.y, buttonMask);
179     };
180
181     guac_client.setClipboard = function(data) {
182
183         // Do not send requests if not connected
184         if (!isConnected())
185             return;
186
187         tunnel.sendMessage("clipboard", data);
188     };
189
190     // Handlers
191     guac_client.onstatechange = null;
192     guac_client.onname = null;
193     guac_client.onerror = null;
194     guac_client.onclipboard = null;
195
196     // Layers
197     function getBufferLayer(index) {
198
199         index = -1 - index;
200         var buffer = buffers[index];
201
202         // Create buffer if necessary
203         if (buffer == null) {
204             buffer = new Guacamole.Layer(0, 0);
205             buffer.autosize = 1;
206             buffers[index] = buffer;
207         }
208
209         return buffer;
210
211     }
212
213     function getLayerContainer(index) {
214
215         var layer = layers[index];
216         if (layer == null) {
217
218             // Add new layer
219             layer = new Guacamole.Client.LayerContainer(displayWidth, displayHeight);
220             layers[index] = layer;
221
222             // Get and position layer
223             var layer_element = layer.getElement();
224             layer_element.style.position = "absolute";
225             layer_element.style.left = "0px";
226             layer_element.style.top = "0px";
227
228             // Add to default layer container
229             default_layer_container.getElement().appendChild(layer_element);
230
231         }
232
233         return layer;
234
235     }
236
237     function getLayer(index) {
238        
239         // If buffer, just get layer
240         if (index < 0)
241             return getBufferLayer(index);
242
243         // Otherwise, retrieve layer from layer container
244         return getLayerContainer(index).getLayer();
245
246     }
247
248     var instructionHandlers = {
249
250         "error": function(parameters) {
251             if (guac_client.onerror) guac_client.onerror(parameters[0]);
252             guac_client.disconnect();
253         },
254
255         "name": function(parameters) {
256             if (guac_client.onname) guac_client.onname(parameters[0]);
257         },
258
259         "clipboard": function(parameters) {
260             if (guac_client.onclipboard) guac_client.onclipboard(parameters[0]);
261         },
262
263         "size": function(parameters) {
264
265             var layer_index = parseInt(parameters[0]);
266             var width = parseInt(parameters[1]);
267             var height = parseInt(parameters[2]);
268
269             // Only valid for layers (buffers auto-resize)
270             if (layer_index >= 0) {
271
272                 // Resize layer
273                 var layer_container = getLayerContainer(layer_index);
274                 layer_container.resize(width, height);
275
276                 // If layer is default, resize display
277                 if (layer_index == 0) {
278
279                     displayWidth = width;
280                     displayHeight = height;
281
282                     // Update (set) display size
283                     display.style.width = displayWidth + "px";
284                     display.style.height = displayHeight + "px";
285
286                     // Set cursor layer width/height
287                     if (cursor != null)
288                         cursor.resize(displayWidth, displayHeight);
289
290                 }
291
292             } // end if layer (not buffer)
293
294         },
295
296         "move": function(parameters) {
297             
298             var layer_index = parseInt(parameters[0]);
299             var parent_index = parseInt(parameters[1]);
300             var x = parseInt(parameters[2]);
301             var y = parseInt(parameters[3]);
302             var z = parseInt(parameters[4]);
303
304             // Only valid for non-default layers
305             if (layer_index > 0 && parent_index >= 0) {
306
307                 // Get container element
308                 var layer_container = getLayerContainer(layer_index).getElement();
309                 var parent = getLayerContainer(parent_index).getElement();
310
311                 // Set parent if necessary
312                 if (!(layer_container.parentNode === parent))
313                     parent.appendChild(layer_container);
314
315                 // Move layer
316                 layer_container.style.left   = x + "px";
317                 layer_container.style.top    = y + "px";
318                 layer_container.style.zIndex = z;
319
320             }
321
322         },
323
324         "png": function(parameters) {
325
326             var channelMask = parseInt(parameters[0]);
327             var layer = getLayer(parseInt(parameters[1]));
328             var x = parseInt(parameters[2]);
329             var y = parseInt(parameters[3]);
330             var data = parameters[4];
331
332             layer.setChannelMask(channelMask);
333
334             layer.draw(
335                 x,
336                 y,
337                 "data:image/png;base64," + data
338             );
339
340             // If received first update, no longer waiting.
341             if (currentState == STATE_WAITING)
342                 setState(STATE_CONNECTED);
343
344         },
345
346         "copy": function(parameters) {
347
348             var srcL = getLayer(parseInt(parameters[0]));
349             var srcX = parseInt(parameters[1]);
350             var srcY = parseInt(parameters[2]);
351             var srcWidth = parseInt(parameters[3]);
352             var srcHeight = parseInt(parameters[4]);
353             var channelMask = parseInt(parameters[5]);
354             var dstL = getLayer(parseInt(parameters[6]));
355             var dstX = parseInt(parameters[7]);
356             var dstY = parseInt(parameters[8]);
357
358             dstL.setChannelMask(channelMask);
359
360             dstL.copyRect(
361                 srcL,
362                 srcX,
363                 srcY,
364                 srcWidth, 
365                 srcHeight, 
366                 dstX,
367                 dstY 
368             );
369
370         },
371
372         "rect": function(parameters) {
373
374             var channelMask = parseInt(parameters[0]);
375             var layer = getLayer(parseInt(parameters[1]));
376             var x = parseInt(parameters[2]);
377             var y = parseInt(parameters[3]);
378             var w = parseInt(parameters[4]);
379             var h = parseInt(parameters[5]);
380             var r = parseInt(parameters[6]);
381             var g = parseInt(parameters[7]);
382             var b = parseInt(parameters[8]);
383             var a = parseInt(parameters[9]);
384
385             layer.setChannelMask(channelMask);
386
387             layer.drawRect(
388                 x, y, w, h,
389                 r, g, b, a
390             );
391
392         },
393
394         "clip": function(parameters) {
395
396             var layer = getLayer(parseInt(parameters[0]));
397             var x = parseInt(parameters[1]);
398             var y = parseInt(parameters[2]);
399             var w = parseInt(parameters[3]);
400             var h = parseInt(parameters[4]);
401
402             layer.clipRect(x, y, w, h);
403
404         },
405
406         "cursor": function(parameters) {
407
408             var x = parseInt(parameters[0]);
409             var y = parseInt(parameters[1]);
410             var data = parameters[2];
411
412             // Start cursor image load
413             var image = new Image();
414             image.onload = function() {
415                 cursorImage = image;
416
417                 var cursorX = cursorRectX + cursorHotspotX;
418                 var cursorY = cursorRectY + cursorHotspotY;
419
420                 cursorHotspotX = x;
421                 cursorHotspotY = y;
422
423                 redrawCursor(cursorX, cursorY);
424             };
425             image.src = "data:image/png;base64," + data
426
427         },
428
429         "sync": function(parameters) {
430
431             var timestamp = parameters[0];
432
433             // When all layers have finished rendering all instructions
434             // UP TO THIS POINT IN TIME, send sync response.
435
436             var layersToSync = 0;
437             function syncLayer() {
438
439                 layersToSync--;
440
441                 // Send sync response when layers are finished
442                 if (layersToSync == 0) {
443                     if (timestamp != currentTimestamp) {
444                         tunnel.sendMessage("sync", timestamp);
445                         currentTimestamp = timestamp;
446                     }
447                 }
448
449             }
450
451             // Count active, not-ready layers and install sync tracking hooks
452             for (var i=0; i<layers.length; i++) {
453
454                 var layer = layers[i].getLayer();
455                 if (layer && !layer.isReady()) {
456                     layersToSync++;
457                     layer.sync(syncLayer);
458                 }
459
460             }
461
462             // If all layers are ready, then we didn't install any hooks.
463             // Send sync message now,
464             if (layersToSync == 0) {
465                 if (timestamp != currentTimestamp) {
466                     tunnel.sendMessage("sync", timestamp);
467                     currentTimestamp = timestamp;
468                 }
469             }
470
471         }
472       
473     };
474
475
476     tunnel.oninstruction = function(opcode, parameters) {
477
478         var handler = instructionHandlers[opcode];
479         if (handler)
480             handler(parameters);
481
482     };
483
484
485     guac_client.disconnect = function() {
486
487         // Only attempt disconnection not disconnected.
488         if (currentState != STATE_DISCONNECTED
489                 && currentState != STATE_DISCONNECTING) {
490
491             setState(STATE_DISCONNECTING);
492
493             // Stop ping
494             if (pingInterval)
495                 window.clearInterval(pingInterval);
496
497             // Send disconnect message and disconnect
498             tunnel.sendMessage("disconnect");
499             tunnel.disconnect();
500             setState(STATE_DISCONNECTED);
501
502         }
503
504     };
505     
506     guac_client.connect = function(data) {
507
508         setState(STATE_CONNECTING);
509
510         try {
511             tunnel.connect(data);
512         }
513         catch (e) {
514             setState(STATE_IDLE);
515             throw e;
516         }
517
518         // Ping every 5 seconds (ensure connection alive)
519         pingInterval = window.setInterval(function() {
520             tunnel.sendMessage("sync", currentTimestamp);
521         }, 5000);
522
523         setState(STATE_WAITING);
524     };
525
526 };
527
528
529 /**
530  * Simple container for Guacamole.Layer, allowing layers to be easily
531  * repositioned and nested. This allows certain operations to be accelerated
532  * through DOM manipulation, rather than raster operations.
533  * 
534  * @constructor
535  * 
536  * @param {Number} width The width of the Layer, in pixels. The canvas element
537  *                       backing this Layer will be given this width.
538  *                       
539  * @param {Number} height The height of the Layer, in pixels. The canvas element
540  *                        backing this Layer will be given this height.
541  */
542 Guacamole.Client.LayerContainer = function(width, height) {
543
544     /**
545      * Reference to this LayerContainer.
546      * @private
547      */
548     var layer_container = this;
549
550     // Create layer with given size
551     var layer = new Guacamole.Layer(width, height);
552
553     // Set layer position
554     var canvas = layer.getCanvas();
555     canvas.style.position = "absolute";
556     canvas.style.left = "0px";
557     canvas.style.top = "0px";
558
559     // Create div with given size
560     var div = document.createElement("div");
561     div.appendChild(canvas);
562     div.style.width = width + "px";
563     div.style.height = height + "px";
564
565     /**
566      * Changes the size of this LayerContainer and the contained Layer to the
567      * given width and height.
568      * 
569      * @param {Number} width The new width to assign to this Layer.
570      * @param {Number} height The new height to assign to this Layer.
571      */
572     layer_container.resize = function(width, height) {
573
574         // Resize layer
575         layer.resize(width, height);
576
577         // Resize containing div
578         div.style.width = width + "px";
579         div.style.height = height + "px";
580
581     };
582   
583     /**
584      * Returns the Layer contained within this LayerContainer.
585      * @returns {Guacamole.Layer} The Layer contained within this LayerContainer.
586      */
587     layer_container.getLayer = function() {
588         return layer;
589     };
590
591     /**
592      * Returns the element containing the Layer within this LayerContainer.
593      * @returns {Element} The element containing the Layer within this LayerContainer.
594      */
595     layer_container.getElement = function() {
596         return div;
597     };
598
599 };