535f9b29aac5f53e47d7653884e14e86b2e256f4
[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             displayWidth = parseInt(parameters[0]);
265             displayHeight = parseInt(parameters[1]);
266
267             // Update (set) display size
268             display.style.width = displayWidth + "px";
269             display.style.height = displayHeight + "px";
270
271             // Set cursor layer width/height
272             if (cursor != null)
273                 cursor.resize(displayWidth, displayHeight);
274
275         },
276
277         "png": function(parameters) {
278
279             var channelMask = parseInt(parameters[0]);
280             var layer = getLayer(parseInt(parameters[1]));
281             var x = parseInt(parameters[2]);
282             var y = parseInt(parameters[3]);
283             var data = parameters[4];
284
285             layer.setChannelMask(channelMask);
286
287             layer.draw(
288                 x,
289                 y,
290                 "data:image/png;base64," + data
291             );
292
293             // If received first update, no longer waiting.
294             if (currentState == STATE_WAITING)
295                 setState(STATE_CONNECTED);
296
297         },
298
299         "copy": function(parameters) {
300
301             var srcL = getLayer(parseInt(parameters[0]));
302             var srcX = parseInt(parameters[1]);
303             var srcY = parseInt(parameters[2]);
304             var srcWidth = parseInt(parameters[3]);
305             var srcHeight = parseInt(parameters[4]);
306             var channelMask = parseInt(parameters[5]);
307             var dstL = getLayer(parseInt(parameters[6]));
308             var dstX = parseInt(parameters[7]);
309             var dstY = parseInt(parameters[8]);
310
311             dstL.setChannelMask(channelMask);
312
313             dstL.copyRect(
314                 srcL,
315                 srcX,
316                 srcY,
317                 srcWidth, 
318                 srcHeight, 
319                 dstX,
320                 dstY 
321             );
322
323         },
324
325         "rect": function(parameters) {
326
327             var channelMask = parseInt(parameters[0]);
328             var layer = getLayer(parseInt(parameters[1]));
329             var x = parseInt(parameters[2]);
330             var y = parseInt(parameters[3]);
331             var w = parseInt(parameters[4]);
332             var h = parseInt(parameters[5]);
333             var r = parseInt(parameters[6]);
334             var g = parseInt(parameters[7]);
335             var b = parseInt(parameters[8]);
336             var a = parseInt(parameters[9]);
337
338             layer.setChannelMask(channelMask);
339
340             layer.drawRect(
341                 x, y, w, h,
342                 r, g, b, a
343             );
344
345         },
346
347         "clip": function(parameters) {
348
349             var layer = getLayer(parseInt(parameters[0]));
350             var x = parseInt(parameters[1]);
351             var y = parseInt(parameters[2]);
352             var w = parseInt(parameters[3]);
353             var h = parseInt(parameters[4]);
354
355             layer.clipRect(x, y, w, h);
356
357         },
358
359         "cursor": function(parameters) {
360
361             var x = parseInt(parameters[0]);
362             var y = parseInt(parameters[1]);
363             var data = parameters[2];
364
365             if (cursor == null) {
366                 cursor = new Guacamole.Layer(displayWidth, displayHeight);
367                 
368                 var canvas = cursor.getCanvas();
369                 canvas.style.position = "absolute";
370                 canvas.style.left = "0px";
371                 canvas.style.top = "0px";
372
373                 display.appendChild(canvas);
374             }
375
376             // Start cursor image load
377             var image = new Image();
378             image.onload = function() {
379                 cursorImage = image;
380
381                 var cursorX = cursorRectX + cursorHotspotX;
382                 var cursorY = cursorRectY + cursorHotspotY;
383
384                 cursorHotspotX = x;
385                 cursorHotspotY = y;
386
387                 redrawCursor(cursorX, cursorY);
388             };
389             image.src = "data:image/png;base64," + data
390
391         },
392
393         "sync": function(parameters) {
394
395             var timestamp = parameters[0];
396
397             // When all layers have finished rendering all instructions
398             // UP TO THIS POINT IN TIME, send sync response.
399
400             var layersToSync = 0;
401             function syncLayer() {
402
403                 layersToSync--;
404
405                 // Send sync response when layers are finished
406                 if (layersToSync == 0) {
407                     tunnel.sendMessage("sync", timestamp);
408                     currentTimestamp = timestamp;
409                 }
410
411             }
412
413             // Count active, not-ready layers and install sync tracking hooks
414             for (var i=0; i<layers.length; i++) {
415
416                 var layer = layers[i];
417                 if (layer && !layer.isReady()) {
418                     layersToSync++;
419                     layer.sync(syncLayer);
420                 }
421
422             }
423
424             // If all layers are ready, then we didn't install any hooks.
425             // Send sync message now,
426             if (layersToSync == 0) {
427                 tunnel.sendMessage("sync", timestamp);
428                 currentTimestamp = timestamp;
429             }
430
431         }
432       
433     };
434
435
436     tunnel.oninstruction = function(opcode, parameters) {
437
438         var handler = instructionHandlers[opcode];
439         if (handler)
440             handler(parameters);
441
442     };
443
444
445     guac_client.disconnect = function() {
446
447         // Only attempt disconnection not disconnected.
448         if (currentState != STATE_DISCONNECTED
449                 && currentState != STATE_DISCONNECTING) {
450
451             setState(STATE_DISCONNECTING);
452
453             // Stop ping
454             if (pingInterval)
455                 window.clearInterval(pingInterval);
456
457             // Send disconnect message and disconnect
458             tunnel.sendMessage("disconnect");
459             tunnel.disconnect();
460             setState(STATE_DISCONNECTED);
461
462         }
463
464     };
465     
466     guac_client.connect = function(data) {
467
468         setState(STATE_CONNECTING);
469
470         try {
471             tunnel.connect(data);
472         }
473         catch (e) {
474             setState(STATE_IDLE);
475             throw e;
476         }
477
478         // Ping every 5 seconds (ensure connection alive)
479         pingInterval = window.setInterval(function() {
480             tunnel.sendMessage("sync", currentTimestamp);
481         }, 5000);
482
483         setState(STATE_WAITING);
484     };
485
486 };
487