6afc25212d6534523bd40821b3accbed05ea58de
[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:" + tunnel.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(tunnel.unescapeGuacamoleString(parameters[0]));
221         },
222
223         "name": function(parameters) {
224             if (nameHandler) nameHandler(tunnel.unescapeGuacamoleString(parameters[0]));
225         },
226
227         "clipboard": function(parameters) {
228             if (clipboardHandler) clipboardHandler(tunnel.unescapeGuacamoleString(parameters[0]));
229         },
230
231         "size": function(parameters) {
232
233             displayWidth = parseInt(parameters[0]);
234             displayHeight = parseInt(parameters[1]);
235
236             // Update (set) display size
237             display.style.width = displayWidth + "px";
238             display.style.height = displayHeight + "px";
239
240             // Set cursor layer width/height
241             if (cursor != null)
242                 cursor.resize(displayWidth, displayHeight);
243
244         },
245
246         "png": function(parameters) {
247
248             var layer = parseInt(parameters[0]);
249             var x = parseInt(parameters[1]);
250             var y = parseInt(parameters[2]);
251             var data = parameters[3];
252
253             getLayer(layer).draw(
254                 x,
255                 y,
256                 "data:image/png;base64," + data
257             );
258
259             // If received first update, no longer waiting.
260             if (currentState == STATE_WAITING)
261                 setState(STATE_CONNECTED);
262
263         },
264
265         "copy": function(parameters) {
266
267             var srcL = parseInt(parameters[0]);
268             var srcX = parseInt(parameters[1]);
269             var srcY = parseInt(parameters[2]);
270             var srcWidth = parseInt(parameters[3]);
271             var srcHeight = parseInt(parameters[4]);
272             var dstL = parseInt(parameters[5]);
273             var dstX = parseInt(parameters[6]);
274             var dstY = parseInt(parameters[7]);
275
276             getLayer(dstL).copyRect(
277                 getLayer(srcL),
278                 srcX,
279                 srcY,
280                 srcWidth, 
281                 srcHeight, 
282                 dstX,
283                 dstY 
284             );
285
286         },
287
288         "cursor": function(parameters) {
289
290             var x = parseInt(parameters[0]);
291             var y = parseInt(parameters[1]);
292             var data = parameters[2];
293
294             if (cursor == null) {
295                 cursor = new Layer(displayWidth, displayHeight);
296                 display.appendChild(cursor);
297             }
298
299             // Start cursor image load
300             var image = new Image();
301             image.onload = function() {
302                 cursorImage = image;
303                 cursorHotspotX = x;
304                 cursorHotspotY = y;
305                 redrawCursor(cursorRectX, cursorRectY);
306             };
307             image.src = "data:image/png;base64," + data
308
309         }
310       
311     };
312
313
314     function doInstruction(opcode, parameters) {
315
316         var handler = instructionHandlers[opcode];
317         if (handler)
318             handler(parameters);
319
320     }
321         
322
323     this.connect = function() {
324
325         setState(STATE_CONNECTING);
326         tunnel.connect();
327         setState(STATE_WAITING);
328
329     };
330
331     
332     function disconnect() {
333
334         // Only attempt disconnection not disconnected.
335         if (currentState != STATE_DISCONNECTED
336                 && currentState != STATE_DISCONNECTING) {
337
338             setState(STATE_DISCONNECTING);
339             tunnel.sendMessage("disconnect;");
340             setState(STATE_DISCONNECTED);
341         }
342
343     }
344
345     this.disconnect = disconnect;
346
347 }