129d469ada6a6e6f186698f00c4d18e9b4a0fced
[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  * Guacamole protocol client. Given a display element and {@link Guacamole.Tunnel},
43  * automatically handles incoming and outgoing Guacamole instructions via the
44  * provided tunnel, updating the display using one or more canvas elements.
45  * 
46  * @constructor
47  * @param {Element} display The display element to add canvas elements to.
48  * @param {Guacamole.Tunnel} tunnel The tunnel to use to send and receive
49  *                                  Guacamole instructions.
50  */
51 Guacamole.Client = function(display, 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     tunnel.onerror = function(message) {
68         if (guac_client.onerror)
69             guac_client.onerror(message);
70     };
71
72     // Display must be relatively positioned for mouse to be handled properly
73     display.style.position = "relative";
74
75     function setState(state) {
76         if (state != currentState) {
77             currentState = state;
78             if (guac_client.onstatechange)
79                 guac_client.onstatechange(currentState);
80         }
81     }
82
83     function isConnected() {
84         return currentState == STATE_CONNECTED
85             || currentState == STATE_WAITING;
86     }
87
88     var cursorImage = null;
89     var cursorHotspotX = 0;
90     var cursorHotspotY = 0;
91
92     var cursorRectX = 0;
93     var cursorRectY = 0;
94     var cursorRectW = 0;
95     var cursorRectH = 0;
96
97     var cursorHidden = 0;
98
99     function redrawCursor(x, y) {
100
101         // Hide hardware cursor
102         if (cursorHidden == 0) {
103             display.className += " guac-hide-cursor";
104             cursorHidden = 1;
105         }
106
107         // Erase old cursor
108         cursor.clearRect(cursorRectX, cursorRectY, cursorRectW, cursorRectH);
109
110         // Update rect
111         cursorRectX = x - cursorHotspotX;
112         cursorRectY = y - cursorHotspotY;
113         cursorRectW = cursorImage.width;
114         cursorRectH = cursorImage.height;
115
116         // Draw new cursor
117         cursor.drawImage(cursorRectX, cursorRectY, cursorImage);
118     }
119
120     guac_client.sendKeyEvent = function(pressed, keysym) {
121         // Do not send requests if not connected
122         if (!isConnected())
123             return;
124
125         tunnel.sendMessage("key", keysym, pressed);
126     };
127
128     guac_client.sendMouseState = function(mouseState) {
129
130         // Do not send requests if not connected
131         if (!isConnected())
132             return;
133
134         // Draw client-side cursor
135         if (cursorImage != null) {
136             redrawCursor(
137                 mouseState.x,
138                 mouseState.y
139             );
140         }
141
142         // Build mask
143         var buttonMask = 0;
144         if (mouseState.left)   buttonMask |= 1;
145         if (mouseState.middle) buttonMask |= 2;
146         if (mouseState.right)  buttonMask |= 4;
147         if (mouseState.up)     buttonMask |= 8;
148         if (mouseState.down)   buttonMask |= 16;
149
150         // Send message
151         tunnel.sendMessage("mouse", mouseState.x, mouseState.y, buttonMask);
152     };
153
154     guac_client.setClipboard = function(data) {
155
156         // Do not send requests if not connected
157         if (!isConnected())
158             return;
159
160         tunnel.sendMessage("clipboard", data);
161     };
162
163     // Handlers
164     guac_client.onstatechange = null;
165     guac_client.onname = null;
166     guac_client.onerror = null;
167     guac_client.onclipboard = null;
168
169     // Layers
170     var displayWidth = 0;
171     var displayHeight = 0;
172
173     var layers = new Array();
174     var buffers = new Array();
175     var cursor = null;
176
177     guac_client.getLayers = function() {
178         return layers;
179     };
180
181     function getLayer(index) {
182
183         // If negative index, use buffer
184         if (index < 0) {
185
186             index = -1 - index;
187             var buffer = buffers[index];
188
189             // Create buffer if necessary
190             if (buffer == null) {
191                 buffer = new Guacamole.Layer(0, 0);
192                 buffer.autosize = 1;
193                 buffers[index] = buffer;
194             }
195
196             return buffer;
197         }
198
199         // If non-negative, use visible layer
200         else {
201
202             var layer = layers[index];
203             if (layer == null) {
204
205                 // Add new layer
206                 layer = new Guacamole.Layer(displayWidth, displayHeight);
207                 
208                 // Set layer position
209                 var canvas = layer.getCanvas();
210                 canvas.style.position = "absolute";
211                 canvas.style.left = "0px";
212                 canvas.style.top = "0px";
213
214                 layers[index] = layer;
215
216                 // (Re)-add existing layers in order
217                 for (var i=0; i<layers.length; i++) {
218                     if (layers[i]) {
219
220                         // If already present, remove
221                         if (layers[i].parentNode === display)
222                             display.removeChild(layers[i].getCanvas());
223
224                         // Add to end
225                         display.appendChild(layers[i].getCanvas());
226                     }
227                 }
228
229                 // Add cursor layer last
230                 if (cursor != null) {
231                     if (cursor.parentNode === display)
232                         display.removeChild(cursor.getCanvas());
233                     display.appendChild(cursor.getCanvas());
234                 }
235
236             }
237             else {
238                 // Reset size
239                 layer.resize(displayWidth, displayHeight);
240             }
241
242             return layer;
243         }
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 = getLayer(parseInt(parameters[0])); /* FIXME: Implement */
265             displayWidth = parseInt(parameters[1]);
266             displayHeight = parseInt(parameters[2]);
267
268             // Update (set) display size
269             display.style.width = displayWidth + "px";
270             display.style.height = displayHeight + "px";
271
272             // Set cursor layer width/height
273             if (cursor != null)
274                 cursor.resize(displayWidth, displayHeight);
275
276         },
277
278         "png": function(parameters) {
279
280             var channelMask = parseInt(parameters[0]);
281             var layer = getLayer(parseInt(parameters[1]));
282             var x = parseInt(parameters[2]);
283             var y = parseInt(parameters[3]);
284             var data = parameters[4];
285
286             layer.setChannelMask(channelMask);
287
288             layer.draw(
289                 x,
290                 y,
291                 "data:image/png;base64," + data
292             );
293
294             // If received first update, no longer waiting.
295             if (currentState == STATE_WAITING)
296                 setState(STATE_CONNECTED);
297
298         },
299
300         "copy": function(parameters) {
301
302             var srcL = getLayer(parseInt(parameters[0]));
303             var srcX = parseInt(parameters[1]);
304             var srcY = parseInt(parameters[2]);
305             var srcWidth = parseInt(parameters[3]);
306             var srcHeight = parseInt(parameters[4]);
307             var channelMask = parseInt(parameters[5]);
308             var dstL = getLayer(parseInt(parameters[6]));
309             var dstX = parseInt(parameters[7]);
310             var dstY = parseInt(parameters[8]);
311
312             dstL.setChannelMask(channelMask);
313
314             dstL.copyRect(
315                 srcL,
316                 srcX,
317                 srcY,
318                 srcWidth, 
319                 srcHeight, 
320                 dstX,
321                 dstY 
322             );
323
324         },
325
326         "rect": function(parameters) {
327
328             var channelMask = parseInt(parameters[0]);
329             var layer = getLayer(parseInt(parameters[1]));
330             var x = parseInt(parameters[2]);
331             var y = parseInt(parameters[3]);
332             var w = parseInt(parameters[4]);
333             var h = parseInt(parameters[5]);
334             var r = parseInt(parameters[6]);
335             var g = parseInt(parameters[7]);
336             var b = parseInt(parameters[8]);
337             var a = parseInt(parameters[9]);
338
339             layer.setChannelMask(channelMask);
340
341             layer.drawRect(
342                 x, y, w, h,
343                 r, g, b, a
344             );
345
346         },
347
348         "clip": function(parameters) {
349
350             var layer = getLayer(parseInt(parameters[0]));
351             var x = parseInt(parameters[1]);
352             var y = parseInt(parameters[2]);
353             var w = parseInt(parameters[3]);
354             var h = parseInt(parameters[4]);
355
356             layer.clipRect(x, y, w, h);
357
358         },
359
360         "cursor": function(parameters) {
361
362             var x = parseInt(parameters[0]);
363             var y = parseInt(parameters[1]);
364             var data = parameters[2];
365
366             if (cursor == null) {
367                 cursor = new Guacamole.Layer(displayWidth, displayHeight);
368                 
369                 var canvas = cursor.getCanvas();
370                 canvas.style.position = "absolute";
371                 canvas.style.left = "0px";
372                 canvas.style.top = "0px";
373
374                 display.appendChild(canvas);
375             }
376
377             // Start cursor image load
378             var image = new Image();
379             image.onload = function() {
380                 cursorImage = image;
381
382                 var cursorX = cursorRectX + cursorHotspotX;
383                 var cursorY = cursorRectY + cursorHotspotY;
384
385                 cursorHotspotX = x;
386                 cursorHotspotY = y;
387
388                 redrawCursor(cursorX, cursorY);
389             };
390             image.src = "data:image/png;base64," + data
391
392         },
393
394         "sync": function(parameters) {
395
396             var timestamp = parameters[0];
397
398             // When all layers have finished rendering all instructions
399             // UP TO THIS POINT IN TIME, send sync response.
400
401             var layersToSync = 0;
402             function syncLayer() {
403
404                 layersToSync--;
405
406                 // Send sync response when layers are finished
407                 if (layersToSync == 0) {
408                     if (timestamp != currentTimestamp) {
409                         tunnel.sendMessage("sync", timestamp);
410                         currentTimestamp = timestamp;
411                     }
412                 }
413
414             }
415
416             // Count active, not-ready layers and install sync tracking hooks
417             for (var i=0; i<layers.length; i++) {
418
419                 var layer = layers[i];
420                 if (layer && !layer.isReady()) {
421                     layersToSync++;
422                     layer.sync(syncLayer);
423                 }
424
425             }
426
427             // If all layers are ready, then we didn't install any hooks.
428             // Send sync message now,
429             if (layersToSync == 0) {
430                 if (timestamp != currentTimestamp) {
431                     tunnel.sendMessage("sync", timestamp);
432                     currentTimestamp = timestamp;
433                 }
434             }
435
436         }
437       
438     };
439
440
441     tunnel.oninstruction = function(opcode, parameters) {
442
443         var handler = instructionHandlers[opcode];
444         if (handler)
445             handler(parameters);
446
447     };
448
449
450     guac_client.disconnect = function() {
451
452         // Only attempt disconnection not disconnected.
453         if (currentState != STATE_DISCONNECTED
454                 && currentState != STATE_DISCONNECTING) {
455
456             setState(STATE_DISCONNECTING);
457
458             // Stop ping
459             if (pingInterval)
460                 window.clearInterval(pingInterval);
461
462             // Send disconnect message and disconnect
463             tunnel.sendMessage("disconnect");
464             tunnel.disconnect();
465             setState(STATE_DISCONNECTED);
466
467         }
468
469     };
470     
471     guac_client.connect = function(data) {
472
473         setState(STATE_CONNECTING);
474
475         try {
476             tunnel.connect(data);
477         }
478         catch (e) {
479             setState(STATE_IDLE);
480             throw e;
481         }
482
483         // Ping every 5 seconds (ensure connection alive)
484         pingInterval = window.setInterval(function() {
485             tunnel.sendMessage("sync", currentTimestamp);
486         }, 5000);
487
488         setState(STATE_WAITING);
489     };
490
491 };
492