Added busy handlers for layer, implemented ready instruction handling in client
[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     var readyCallback = null;
147     var busyLayers = 0;
148
149     function layerBusy()  { busyLayers++; }
150     function layerReady() {
151         busyLayers--;
152         if (readyCallback != null)
153             readyCallback();
154     }
155
156     function sendReady() {
157
158        // If ready, send ready message
159        if (busyLayers == 0) {
160            tunnel.sendMessage("ready;");
161        }
162
163        // If not ready, queue message for when ready
164        else if (readyCallback == null) {
165            readyCallback = function() {
166                tunnel.sendMessage("ready;");
167                readyCallback = null;
168            }
169        }
170
171     }
172
173     function getTrackedLayer(w, h) {
174         var layer = new Layer(w, h);
175
176         layer.setBusyHandler(layerBusy);
177         layer.setReadyHandler(layerReady);
178
179         return layer;
180     }
181
182     // Layers
183     var displayWidth = 0;
184     var displayHeight = 0;
185
186     var layers = new Array();
187     var buffers = new Array();
188     var cursor = null;
189
190     this.getLayers = function() {
191         return layers;
192     }
193
194     function getLayer(index) {
195
196         // If negative index, use buffer
197         if (index < 0) {
198
199             index = -1 - index;
200             var buffer = buffers[index];
201
202             // Create buffer if necessary
203             if (buffer == null) {
204                 buffer = new Layer(0, 0); // Untracked
205                 buffer.setAutosize(1);
206                 buffers[index] = buffer;
207             }
208
209             return buffer;
210         }
211
212         // If non-negative, use visible layer
213         else {
214
215             var layer = layers[index];
216             if (layer == null) {
217
218                 // Add new layer
219                 layer = getTrackedLayer(displayWidth, displayHeight);
220                 layers[index] = layer;
221
222                 // (Re)-add existing layers in order
223                 for (var i=0; i<layers.length; i++) {
224                     if (layers[i]) {
225
226                         // If already present, remove
227                         if (layers[i].parentNode === display)
228                             display.removeChild(layers[i]);
229
230                         // Add to end
231                         display.appendChild(layers[i]);
232                     }
233                 }
234
235                 // Add cursor layer last
236                 if (cursor != null) {
237                     if (cursor.parentNode === display)
238                         display.removeChild(cursor);
239                     display.appendChild(cursor);
240                 }
241
242             }
243             else {
244                 // Reset size
245                 layer.resize(displayWidth, displayHeight);
246             }
247
248             return layer;
249         }
250
251     }
252
253     var instructionHandlers = {
254
255         "error": function(parameters) {
256             if (errorHandler) errorHandler(tunnel.unescapeGuacamoleString(parameters[0]));
257         },
258
259         "ready": function(parameters) {
260             sendReady();
261         },
262
263         "name": function(parameters) {
264             if (nameHandler) nameHandler(tunnel.unescapeGuacamoleString(parameters[0]));
265         },
266
267         "clipboard": function(parameters) {
268             if (clipboardHandler) clipboardHandler(tunnel.unescapeGuacamoleString(parameters[0]));
269         },
270
271         "size": function(parameters) {
272
273             displayWidth = parseInt(parameters[0]);
274             displayHeight = parseInt(parameters[1]);
275
276             // Update (set) display size
277             display.style.width = displayWidth + "px";
278             display.style.height = displayHeight + "px";
279
280             // Set cursor layer width/height
281             if (cursor != null)
282                 cursor.resize(displayWidth, displayHeight);
283
284         },
285
286         "png": function(parameters) {
287
288             var layer = parseInt(parameters[0]);
289             var x = parseInt(parameters[1]);
290             var y = parseInt(parameters[2]);
291             var data = parameters[3];
292
293             getLayer(layer).draw(
294                 x,
295                 y,
296                 "data:image/png;base64," + data
297             );
298
299             // If received first update, no longer waiting.
300             if (currentState == STATE_WAITING)
301                 setState(STATE_CONNECTED);
302
303         },
304
305         "copy": function(parameters) {
306
307             var srcL = parseInt(parameters[0]);
308             var srcX = parseInt(parameters[1]);
309             var srcY = parseInt(parameters[2]);
310             var srcWidth = parseInt(parameters[3]);
311             var srcHeight = parseInt(parameters[4]);
312             var dstL = parseInt(parameters[5]);
313             var dstX = parseInt(parameters[6]);
314             var dstY = parseInt(parameters[7]);
315
316             getLayer(dstL).copyRect(
317                 getLayer(srcL),
318                 srcX,
319                 srcY,
320                 srcWidth, 
321                 srcHeight, 
322                 dstX,
323                 dstY 
324             );
325
326         },
327
328         "cursor": function(parameters) {
329
330             var x = parseInt(parameters[0]);
331             var y = parseInt(parameters[1]);
332             var data = parameters[2];
333
334             if (cursor == null) {
335                 cursor = getTrackedLayer(displayWidth, displayHeight);
336                 display.appendChild(cursor);
337             }
338
339             // Start cursor image load
340             var image = new Image();
341             image.onload = function() {
342                 cursorImage = image;
343                 cursorHotspotX = x;
344                 cursorHotspotY = y;
345                 redrawCursor(cursorRectX, cursorRectY);
346             };
347             image.src = "data:image/png;base64," + data
348
349         }
350       
351     };
352
353
354     function doInstruction(opcode, parameters) {
355
356         var handler = instructionHandlers[opcode];
357         if (handler)
358             handler(parameters);
359
360     }
361         
362
363     this.connect = function() {
364
365         setState(STATE_CONNECTING);
366         tunnel.connect();
367         setState(STATE_WAITING);
368
369     };
370
371     
372     function disconnect() {
373
374         // Only attempt disconnection not disconnected.
375         if (currentState != STATE_DISCONNECTED
376                 && currentState != STATE_DISCONNECTING) {
377
378             setState(STATE_DISCONNECTING);
379             tunnel.sendMessage("disconnect;");
380             setState(STATE_DISCONNECTED);
381         }
382
383     }
384
385     this.disconnect = disconnect;
386
387 }