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