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