Fixed potential layer sync issue (if multiple layers are used, and multiple layers...
[guacamole-common-js.git] / src / main / resources / guacamole.js
1
2 /*
3  *  Guacamole - Clientless Remote Desktop
4  *  Copyright (C) 2010  Michael Jumper
5  *
6  *  This program is free software: you can redistribute it and/or modify
7  *  it under the terms of the GNU Affero General Public License as published by
8  *  the Free Software Foundation, either version 3 of the License, or
9  *  (at your option) any later version.
10  *
11  *  This program is distributed in the hope that it will be useful,
12  *  but WITHOUT ANY WARRANTY; without even the implied warranty of
13  *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14  *  GNU Affero General Public License for more details.
15  *
16  *  You should have received a copy of the GNU Affero General Public License
17  */
18
19 function GuacamoleClient(display, tunnel) {
20
21     var STATE_IDLE          = 0;
22     var STATE_CONNECTING    = 1;
23     var STATE_WAITING       = 2;
24     var STATE_CONNECTED     = 3;
25     var STATE_DISCONNECTING = 4;
26     var STATE_DISCONNECTED  = 5;
27
28     var currentState = STATE_IDLE;
29     var stateChangeHandler = null;
30
31     tunnel.setInstructionHandler(doInstruction);
32
33     // Display must be relatively positioned for mouse to be handled properly
34     display.style.position = "relative";
35
36     function setState(state) {
37         if (state != currentState) {
38             currentState = state;
39             if (stateChangeHandler)
40                 stateChangeHandler(currentState);
41         }
42     }
43
44     this.setOnStateChangeHandler = function(handler) {
45         stateChangeHandler = handler;
46     };
47
48     function isConnected() {
49         return currentState == STATE_CONNECTED
50             || currentState == STATE_WAITING;
51     }
52
53     var cursorImage = null;
54     var cursorHotspotX = 0;
55     var cursorHotspotY = 0;
56
57     // FIXME: Make object. Clean up.
58     var cursorRectX = 0;
59     var cursorRectY = 0;
60     var cursorRectW = 0;
61     var cursorRectH = 0;
62
63     var cursorHidden = 0;
64
65     function redrawCursor(x, y) {
66
67         // Hide hardware cursor
68         if (cursorHidden == 0) {
69             display.className += " guac-hide-cursor";
70             cursorHidden = 1;
71         }
72
73         // Erase old cursor
74         cursor.clearRect(cursorRectX, cursorRectY, cursorRectW, cursorRectH);
75
76         // Update rect
77         cursorRectX = x - cursorHotspotX;
78         cursorRectY = y - cursorHotspotY;
79         cursorRectW = cursorImage.width;
80         cursorRectH = cursorImage.height;
81
82         // Draw new cursor
83         cursor.drawImage(cursorRectX, cursorRectY, cursorImage);
84     }
85
86     this.sendKeyEvent = function(pressed, keysym) {
87         // Do not send requests if not connected
88         if (!isConnected())
89             return;
90
91         tunnel.sendMessage("key:" +  keysym + "," + pressed + ";");
92     };
93
94     this.sendMouseState = function(mouseState) {
95
96         // Do not send requests if not connected
97         if (!isConnected())
98             return;
99
100         // Draw client-side cursor
101         if (cursorImage != null) {
102             redrawCursor(
103                 mouseState.getX(),
104                 mouseState.getY()
105             );
106         }
107
108         // Build mask
109         var buttonMask = 0;
110         if (mouseState.getLeft())   buttonMask |= 1;
111         if (mouseState.getMiddle()) buttonMask |= 2;
112         if (mouseState.getRight())  buttonMask |= 4;
113         if (mouseState.getUp())     buttonMask |= 8;
114         if (mouseState.getDown())   buttonMask |= 16;
115
116         // Send message
117         tunnel.sendMessage("mouse:" + mouseState.getX() + "," + mouseState.getY() + "," + buttonMask + ";");
118     };
119
120     this.setClipboard = function(data) {
121
122         // Do not send requests if not connected
123         if (!isConnected())
124             return;
125
126         tunnel.sendMessage("clipboard:" + escapeGuacamoleString(data) + ";");
127     };
128
129     // Handlers
130
131     var nameHandler = null;
132     this.setNameHandler = function(handler) {
133         nameHandler = handler;
134     }
135
136     var errorHandler = null;
137     this.setErrorHandler = function(handler) {
138         errorHandler = handler;
139     };
140
141     var clipboardHandler = null;
142     this.setClipboardHandler = function(handler) {
143         clipboardHandler = handler;
144     };
145
146     // Layers
147     var displayWidth = 0;
148     var displayHeight = 0;
149
150     var layers = new Array();
151     var buffers = new Array();
152     var cursor = null;
153
154     this.getLayers = function() {
155         return layers;
156     };
157
158     function getLayer(index) {
159
160         // If negative index, use buffer
161         if (index < 0) {
162
163             index = -1 - index;
164             var buffer = buffers[index];
165
166             // Create buffer if necessary
167             if (buffer == null) {
168                 buffer = new Layer(0, 0);
169                 buffer.setAutosize(1);
170                 buffers[index] = buffer;
171             }
172
173             return buffer;
174         }
175
176         // If non-negative, use visible layer
177         else {
178
179             var layer = layers[index];
180             if (layer == null) {
181
182                 // Add new layer
183                 layer = new Layer(displayWidth, displayHeight);
184                 layers[index] = layer;
185
186                 // (Re)-add existing layers in order
187                 for (var i=0; i<layers.length; i++) {
188                     if (layers[i]) {
189
190                         // If already present, remove
191                         if (layers[i].parentNode === display)
192                             display.removeChild(layers[i]);
193
194                         // Add to end
195                         display.appendChild(layers[i]);
196                     }
197                 }
198
199                 // Add cursor layer last
200                 if (cursor != null) {
201                     if (cursor.parentNode === display)
202                         display.removeChild(cursor);
203                     display.appendChild(cursor);
204                 }
205
206             }
207             else {
208                 // Reset size
209                 layer.resize(displayWidth, displayHeight);
210             }
211
212             return layer;
213         }
214
215     }
216
217     var instructionHandlers = {
218
219         "error": function(parameters) {
220             if (errorHandler) errorHandler(unescapeGuacamoleString(parameters[0]));
221             disconnect();
222         },
223
224         "name": function(parameters) {
225             if (nameHandler) nameHandler(unescapeGuacamoleString(parameters[0]));
226         },
227
228         "clipboard": function(parameters) {
229             if (clipboardHandler) clipboardHandler(unescapeGuacamoleString(parameters[0]));
230         },
231
232         "size": function(parameters) {
233
234             displayWidth = parseInt(parameters[0]);
235             displayHeight = parseInt(parameters[1]);
236
237             // Update (set) display size
238             display.style.width = displayWidth + "px";
239             display.style.height = displayHeight + "px";
240
241             // Set cursor layer width/height
242             if (cursor != null)
243                 cursor.resize(displayWidth, displayHeight);
244
245         },
246
247         "png": function(parameters) {
248
249             var channelMask = parseInt(parameters[0]);
250             var layer = getLayer(parseInt(parameters[1]));
251             var x = parseInt(parameters[2]);
252             var y = parseInt(parameters[3]);
253             var data = parameters[4];
254
255             layer.setChannelMask(channelMask);
256
257             layer.draw(
258                 x,
259                 y,
260                 "data:image/png;base64," + data
261             );
262
263             // If received first update, no longer waiting.
264             if (currentState == STATE_WAITING)
265                 setState(STATE_CONNECTED);
266
267         },
268
269         "copy": function(parameters) {
270
271             var srcL = getLayer(parseInt(parameters[0]));
272             var srcX = parseInt(parameters[1]);
273             var srcY = parseInt(parameters[2]);
274             var srcWidth = parseInt(parameters[3]);
275             var srcHeight = parseInt(parameters[4]);
276             var channelMask = parseInt(parameters[5]);
277             var dstL = getLayer(parseInt(parameters[6]));
278             var dstX = parseInt(parameters[7]);
279             var dstY = parseInt(parameters[8]);
280
281             dstL.setChannelMask(channelMask);
282
283             dstL.copyRect(
284                 srcL,
285                 srcX,
286                 srcY,
287                 srcWidth, 
288                 srcHeight, 
289                 dstX,
290                 dstY 
291             );
292
293         },
294
295         "cursor": function(parameters) {
296
297             var x = parseInt(parameters[0]);
298             var y = parseInt(parameters[1]);
299             var data = parameters[2];
300
301             if (cursor == null) {
302                 cursor = new Layer(displayWidth, displayHeight);
303                 display.appendChild(cursor);
304             }
305
306             // Start cursor image load
307             var image = new Image();
308             image.onload = function() {
309                 cursorImage = image;
310                 cursorHotspotX = x;
311                 cursorHotspotY = y;
312                 redrawCursor(cursorRectX, cursorRectY);
313             };
314             image.src = "data:image/png;base64," + data
315
316         },
317
318         "sync": function(parameters) {
319
320             var timestamp = parameters[0];
321
322             // When all layers have finished rendering all instructions
323             // UP TO THIS POINT IN TIME, send sync response.
324
325             var layersToSync = 0;
326             function syncLayer() {
327
328                 layersToSync--;
329
330                 // Send sync response when layers are finished
331                 if (layersToSync == 0)
332                     tunnel.sendMessage("sync:" + timestamp + ";");
333
334             }
335
336             // Count active, not-ready layers and install sync tracking hooks
337             for (var i=0; i<layers.length; i++) {
338
339                 var layer = layers[i];
340                 if (layer && !layer.isReady()) {
341                     layersToSync++;
342                     layer.sync(syncLayer);
343                 }
344
345             }
346
347             // If all layers are ready, then we didn't install any hooks.
348             // Send sync message now,
349             if (layersToSync == 0)
350                 tunnel.sendMessage("sync:" + timestamp + ";");
351
352         },
353       
354     };
355
356
357     function doInstruction(opcode, parameters) {
358
359         var handler = instructionHandlers[opcode];
360         if (handler)
361             handler(parameters);
362
363     }
364
365
366     function disconnect() {
367
368         // Only attempt disconnection not disconnected.
369         if (currentState != STATE_DISCONNECTED
370                 && currentState != STATE_DISCONNECTING) {
371
372             setState(STATE_DISCONNECTING);
373             tunnel.sendMessage("disconnect;");
374             tunnel.disconnect();
375             setState(STATE_DISCONNECTED);
376         }
377
378     }
379
380     function escapeGuacamoleString(str) {
381
382         var escapedString = "";
383
384         for (var i=0; i<str.length; i++) {
385
386             var c = str.charAt(i);
387             if (c == ",")
388                 escapedString += "\\c";
389             else if (c == ";")
390                 escapedString += "\\s";
391             else if (c == "\\")
392                 escapedString += "\\\\";
393             else
394                 escapedString += c;
395
396         }
397
398         return escapedString;
399
400     }
401
402     function unescapeGuacamoleString(str) {
403
404         var unescapedString = "";
405
406         for (var i=0; i<str.length; i++) {
407
408             var c = str.charAt(i);
409             if (c == "\\" && i<str.length-1) {
410
411                 var escapeChar = str.charAt(++i);
412                 if (escapeChar == "c")
413                     unescapedString += ",";
414                 else if (escapeChar == "s")
415                     unescapedString += ";";
416                 else if (escapeChar == "\\")
417                     unescapedString += "\\";
418                 else
419                     unescapedString += "\\" + escapeChar;
420
421             }
422             else
423                 unescapedString += c;
424
425         }
426
427         return unescapedString;
428
429     }
430
431     this.disconnect = disconnect;
432     this.connect = function() {
433
434         setState(STATE_CONNECTING);
435         tunnel.connect();
436         setState(STATE_WAITING);
437
438     };
439
440     this.escapeGuacamoleString   = escapeGuacamoleString;
441     this.unescapeGuacamoleString = unescapeGuacamoleString;
442
443 }