Added missing semicolons, improved state handling
[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 layer = parseInt(parameters[0]);
250             var x = parseInt(parameters[1]);
251             var y = parseInt(parameters[2]);
252             var data = parameters[3];
253
254             getLayer(layer).draw(
255                 x,
256                 y,
257                 "data:image/png;base64," + data
258             );
259
260             // If received first update, no longer waiting.
261             if (currentState == STATE_WAITING)
262                 setState(STATE_CONNECTED);
263
264         },
265
266         "copy": function(parameters) {
267
268             var srcL = parseInt(parameters[0]);
269             var srcX = parseInt(parameters[1]);
270             var srcY = parseInt(parameters[2]);
271             var srcWidth = parseInt(parameters[3]);
272             var srcHeight = parseInt(parameters[4]);
273             var dstL = parseInt(parameters[5]);
274             var dstX = parseInt(parameters[6]);
275             var dstY = parseInt(parameters[7]);
276
277             getLayer(dstL).copyRect(
278                 getLayer(srcL),
279                 srcX,
280                 srcY,
281                 srcWidth, 
282                 srcHeight, 
283                 dstX,
284                 dstY 
285             );
286
287         },
288
289         "cursor": function(parameters) {
290
291             var x = parseInt(parameters[0]);
292             var y = parseInt(parameters[1]);
293             var data = parameters[2];
294
295             if (cursor == null) {
296                 cursor = new Layer(displayWidth, displayHeight);
297                 display.appendChild(cursor);
298             }
299
300             // Start cursor image load
301             var image = new Image();
302             image.onload = function() {
303                 cursorImage = image;
304                 cursorHotspotX = x;
305                 cursorHotspotY = y;
306                 redrawCursor(cursorRectX, cursorRectY);
307             };
308             image.src = "data:image/png;base64," + data
309
310         }
311       
312     };
313
314
315     function doInstruction(opcode, parameters) {
316
317         var handler = instructionHandlers[opcode];
318         if (handler)
319             handler(parameters);
320
321     }
322
323
324     function disconnect() {
325
326         // Only attempt disconnection not disconnected.
327         if (currentState != STATE_DISCONNECTED
328                 && currentState != STATE_DISCONNECTING) {
329
330             setState(STATE_DISCONNECTING);
331             tunnel.sendMessage("disconnect;");
332             tunnel.disconnect();
333             setState(STATE_DISCONNECTED);
334         }
335
336     }
337
338     function escapeGuacamoleString(str) {
339
340         var escapedString = "";
341
342         for (var i=0; i<str.length; i++) {
343
344             var c = str.charAt(i);
345             if (c == ",")
346                 escapedString += "\\c";
347             else if (c == ";")
348                 escapedString += "\\s";
349             else if (c == "\\")
350                 escapedString += "\\\\";
351             else
352                 escapedString += c;
353
354         }
355
356         return escapedString;
357
358     }
359
360     function unescapeGuacamoleString(str) {
361
362         var unescapedString = "";
363
364         for (var i=0; i<str.length; i++) {
365
366             var c = str.charAt(i);
367             if (c == "\\" && i<str.length-1) {
368
369                 var escapeChar = str.charAt(++i);
370                 if (escapeChar == "c")
371                     unescapedString += ",";
372                 else if (escapeChar == "s")
373                     unescapedString += ";";
374                 else if (escapeChar == "\\")
375                     unescapedString += "\\";
376                 else
377                     unescapedString += "\\" + escapeChar;
378
379             }
380             else
381                 unescapedString += c;
382
383         }
384
385         return unescapedString;
386
387     }
388
389     this.disconnect = disconnect;
390     this.connect = function() {
391
392         setState(STATE_CONNECTING);
393         tunnel.connect();
394         setState(STATE_WAITING);
395
396     };
397
398     this.escapeGuacamoleString   = escapeGuacamoleString;
399     this.unescapeGuacamoleString = unescapeGuacamoleString;
400
401 }