Fixed issue with moving cursor when icon updates.
[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(
313                     cursorRectX + cursorHotspotX,
314                     cursorRectY + cursorHotspotY
315                 );
316             };
317             image.src = "data:image/png;base64," + data
318
319         },
320
321         "sync": function(parameters) {
322
323             var timestamp = parameters[0];
324
325             // When all layers have finished rendering all instructions
326             // UP TO THIS POINT IN TIME, send sync response.
327
328             var layersToSync = 0;
329             function syncLayer() {
330
331                 layersToSync--;
332
333                 // Send sync response when layers are finished
334                 if (layersToSync == 0)
335                     tunnel.sendMessage("sync:" + timestamp + ";");
336
337             }
338
339             // Count active, not-ready layers and install sync tracking hooks
340             for (var i=0; i<layers.length; i++) {
341
342                 var layer = layers[i];
343                 if (layer && !layer.isReady()) {
344                     layersToSync++;
345                     layer.sync(syncLayer);
346                 }
347
348             }
349
350             // If all layers are ready, then we didn't install any hooks.
351             // Send sync message now,
352             if (layersToSync == 0)
353                 tunnel.sendMessage("sync:" + timestamp + ";");
354
355         },
356       
357     };
358
359
360     function doInstruction(opcode, parameters) {
361
362         var handler = instructionHandlers[opcode];
363         if (handler)
364             handler(parameters);
365
366     }
367
368
369     function disconnect() {
370
371         // Only attempt disconnection not disconnected.
372         if (currentState != STATE_DISCONNECTED
373                 && currentState != STATE_DISCONNECTING) {
374
375             setState(STATE_DISCONNECTING);
376             tunnel.sendMessage("disconnect;");
377             tunnel.disconnect();
378             setState(STATE_DISCONNECTED);
379         }
380
381     }
382
383     function escapeGuacamoleString(str) {
384
385         var escapedString = "";
386
387         for (var i=0; i<str.length; i++) {
388
389             var c = str.charAt(i);
390             if (c == ",")
391                 escapedString += "\\c";
392             else if (c == ";")
393                 escapedString += "\\s";
394             else if (c == "\\")
395                 escapedString += "\\\\";
396             else
397                 escapedString += c;
398
399         }
400
401         return escapedString;
402
403     }
404
405     function unescapeGuacamoleString(str) {
406
407         var unescapedString = "";
408
409         for (var i=0; i<str.length; i++) {
410
411             var c = str.charAt(i);
412             if (c == "\\" && i<str.length-1) {
413
414                 var escapeChar = str.charAt(++i);
415                 if (escapeChar == "c")
416                     unescapedString += ",";
417                 else if (escapeChar == "s")
418                     unescapedString += ";";
419                 else if (escapeChar == "\\")
420                     unescapedString += "\\";
421                 else
422                     unescapedString += "\\" + escapeChar;
423
424             }
425             else
426                 unescapedString += c;
427
428         }
429
430         return unescapedString;
431
432     }
433
434     this.disconnect = disconnect;
435     this.connect = function(data) {
436
437         setState(STATE_CONNECTING);
438
439         try {
440             tunnel.connect(data);
441         }
442         catch (e) {
443             setState(STATE_IDLE);
444             throw e;
445         }
446
447         setState(STATE_WAITING);
448     };
449
450     this.escapeGuacamoleString   = escapeGuacamoleString;
451     this.unescapeGuacamoleString = unescapeGuacamoleString;
452
453 }