2 /* ***** BEGIN LICENSE BLOCK *****
3 * Version: MPL 1.1/GPL 2.0/LGPL 2.1
5 * The contents of this file are subject to the Mozilla Public License Version
6 * 1.1 (the "License"); you may not use this file except in compliance with
7 * the License. You may obtain a copy of the License at
8 * http://www.mozilla.org/MPL/
10 * Software distributed under the License is distributed on an "AS IS" basis,
11 * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
12 * for the specific language governing rights and limitations under the
15 * The Original Code is guacamole-common-js.
17 * The Initial Developer of the Original Code is
19 * Portions created by the Initial Developer are Copyright (C) 2010
20 * the Initial Developer. All Rights Reserved.
24 * Alternatively, the contents of this file may be used under the terms of
25 * either the GNU General Public License Version 2 or later (the "GPL"), or
26 * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
27 * in which case the provisions of the GPL or the LGPL are applicable instead
28 * of those above. If you wish to allow use of your version of this file only
29 * under the terms of either the GPL or the LGPL, and not to allow others to
30 * use your version of this file under the terms of the MPL, indicate your
31 * decision by deleting the provisions above and replace them with the notice
32 * and other provisions required by the GPL or the LGPL. If you do not delete
33 * the provisions above, a recipient may use your version of this file under
34 * the terms of any one of the MPL, the GPL or the LGPL.
36 * ***** END LICENSE BLOCK ***** */
38 // Guacamole namespace
39 var Guacamole = Guacamole || {};
42 * Guacamole protocol client. Given a display element and {@link Guacamole.Tunnel},
43 * automatically handles incoming and outgoing Guacamole instructions via the
44 * provided tunnel, updating the display using one or more canvas elements.
47 * @param {Element} display The display element to add canvas elements to.
48 * @param {Guacamole.Tunnel} tunnel The tunnel to use to send and receive
49 * Guacamole instructions.
51 Guacamole.Client = function(display, tunnel) {
53 var guac_client = this;
56 var STATE_CONNECTING = 1;
57 var STATE_WAITING = 2;
58 var STATE_CONNECTED = 3;
59 var STATE_DISCONNECTING = 4;
60 var STATE_DISCONNECTED = 5;
62 var currentState = STATE_IDLE;
64 tunnel.onerror = function(message) {
65 if (guac_client.onerror)
66 guac_client.onerror(message);
69 // Display must be relatively positioned for mouse to be handled properly
70 display.style.position = "relative";
72 function setState(state) {
73 if (state != currentState) {
75 if (guac_client.onstatechange)
76 guac_client.onstatechange(currentState);
80 function isConnected() {
81 return currentState == STATE_CONNECTED
82 || currentState == STATE_WAITING;
85 var cursorImage = null;
86 var cursorHotspotX = 0;
87 var cursorHotspotY = 0;
96 function redrawCursor(x, y) {
98 // Hide hardware cursor
99 if (cursorHidden == 0) {
100 display.className += " guac-hide-cursor";
105 cursor.clearRect(cursorRectX, cursorRectY, cursorRectW, cursorRectH);
108 cursorRectX = x - cursorHotspotX;
109 cursorRectY = y - cursorHotspotY;
110 cursorRectW = cursorImage.width;
111 cursorRectH = cursorImage.height;
114 cursor.drawImage(cursorRectX, cursorRectY, cursorImage);
117 guac_client.sendKeyEvent = function(pressed, keysym) {
118 // Do not send requests if not connected
122 tunnel.sendMessage("key", keysym, pressed);
125 guac_client.sendMouseState = function(mouseState) {
127 // Do not send requests if not connected
131 // Draw client-side cursor
132 if (cursorImage != null) {
141 if (mouseState.left) buttonMask |= 1;
142 if (mouseState.middle) buttonMask |= 2;
143 if (mouseState.right) buttonMask |= 4;
144 if (mouseState.up) buttonMask |= 8;
145 if (mouseState.down) buttonMask |= 16;
148 tunnel.sendMessage("mouse", mouseState.x, mouseState.y, buttonMask);
151 guac_client.setClipboard = function(data) {
153 // Do not send requests if not connected
157 tunnel.sendMessage("clipboard", data);
161 guac_client.onstatechange = null;
162 guac_client.onname = null;
163 guac_client.onerror = null;
164 guac_client.onclipboard = null;
167 var displayWidth = 0;
168 var displayHeight = 0;
170 var layers = new Array();
171 var buffers = new Array();
174 guac_client.getLayers = function() {
178 function getLayer(index) {
180 // If negative index, use buffer
184 var buffer = buffers[index];
186 // Create buffer if necessary
187 if (buffer == null) {
188 buffer = new Guacamole.Layer(0, 0);
190 buffers[index] = buffer;
196 // If non-negative, use visible layer
199 var layer = layers[index];
203 layer = new Guacamole.Layer(displayWidth, displayHeight);
205 // Set layer position
206 var canvas = layer.getCanvas();
207 canvas.style.position = "absolute";
208 canvas.style.left = "0px";
209 canvas.style.top = "0px";
211 layers[index] = layer;
213 // (Re)-add existing layers in order
214 for (var i=0; i<layers.length; i++) {
217 // If already present, remove
218 if (layers[i].parentNode === display)
219 display.removeChild(layers[i].getCanvas());
222 display.appendChild(layers[i].getCanvas());
226 // Add cursor layer last
227 if (cursor != null) {
228 if (cursor.parentNode === display)
229 display.removeChild(cursor.getCanvas());
230 display.appendChild(cursor.getCanvas());
236 layer.resize(displayWidth, displayHeight);
244 var instructionHandlers = {
246 "error": function(parameters) {
247 if (guac_client.onerror) guac_client.onerror(parameters[0]);
248 guac_client.disconnect();
251 "name": function(parameters) {
252 if (guac_client.onname) guac_client.onname(parameters[0]);
255 "clipboard": function(parameters) {
256 if (guac_client.onclipboard) guac_client.onclipboard(parameters[0]);
259 "size": function(parameters) {
261 displayWidth = parseInt(parameters[0]);
262 displayHeight = parseInt(parameters[1]);
264 // Update (set) display size
265 display.style.width = displayWidth + "px";
266 display.style.height = displayHeight + "px";
268 // Set cursor layer width/height
270 cursor.resize(displayWidth, displayHeight);
274 "png": function(parameters) {
276 var channelMask = parseInt(parameters[0]);
277 var layer = getLayer(parseInt(parameters[1]));
278 var x = parseInt(parameters[2]);
279 var y = parseInt(parameters[3]);
280 var data = parameters[4];
282 layer.setChannelMask(channelMask);
287 "data:image/png;base64," + data
290 // If received first update, no longer waiting.
291 if (currentState == STATE_WAITING)
292 setState(STATE_CONNECTED);
296 "copy": function(parameters) {
298 var srcL = getLayer(parseInt(parameters[0]));
299 var srcX = parseInt(parameters[1]);
300 var srcY = parseInt(parameters[2]);
301 var srcWidth = parseInt(parameters[3]);
302 var srcHeight = parseInt(parameters[4]);
303 var channelMask = parseInt(parameters[5]);
304 var dstL = getLayer(parseInt(parameters[6]));
305 var dstX = parseInt(parameters[7]);
306 var dstY = parseInt(parameters[8]);
308 dstL.setChannelMask(channelMask);
322 "rect": function(parameters) {
324 var channelMask = parseInt(parameters[0]);
325 var layer = getLayer(parseInt(parameters[1]));
326 var x = parseInt(parameters[2]);
327 var y = parseInt(parameters[3]);
328 var w = parseInt(parameters[4]);
329 var h = parseInt(parameters[5]);
330 var r = parseInt(parameters[6]);
331 var g = parseInt(parameters[7]);
332 var b = parseInt(parameters[8]);
333 var a = parseInt(parameters[9]);
335 layer.setChannelMask(channelMask);
344 "clip": function(parameters) {
346 var layer = getLayer(parseInt(parameters[0]));
347 var x = parseInt(parameters[1]);
348 var y = parseInt(parameters[2]);
349 var w = parseInt(parameters[3]);
350 var h = parseInt(parameters[4]);
352 layer.clipRect(x, y, w, h);
356 "cursor": function(parameters) {
358 var x = parseInt(parameters[0]);
359 var y = parseInt(parameters[1]);
360 var data = parameters[2];
362 if (cursor == null) {
363 cursor = new Guacamole.Layer(displayWidth, displayHeight);
365 var canvas = cursor.getCanvas();
366 canvas.style.position = "absolute";
367 canvas.style.left = "0px";
368 canvas.style.top = "0px";
370 display.appendChild(canvas);
373 // Start cursor image load
374 var image = new Image();
375 image.onload = function() {
378 var cursorX = cursorRectX + cursorHotspotX;
379 var cursorY = cursorRectY + cursorHotspotY;
384 redrawCursor(cursorX, cursorY);
386 image.src = "data:image/png;base64," + data
390 "sync": function(parameters) {
392 var timestamp = parameters[0];
394 // When all layers have finished rendering all instructions
395 // UP TO THIS POINT IN TIME, send sync response.
397 var layersToSync = 0;
398 function syncLayer() {
402 // Send sync response when layers are finished
403 if (layersToSync == 0)
404 tunnel.sendMessage("sync", timestamp);
408 // Count active, not-ready layers and install sync tracking hooks
409 for (var i=0; i<layers.length; i++) {
411 var layer = layers[i];
412 if (layer && !layer.isReady()) {
414 layer.sync(syncLayer);
419 // If all layers are ready, then we didn't install any hooks.
420 // Send sync message now,
421 if (layersToSync == 0)
422 tunnel.sendMessage("sync", timestamp);
429 tunnel.oninstruction = function(opcode, parameters) {
431 var handler = instructionHandlers[opcode];
438 guac_client.disconnect = function() {
440 // Only attempt disconnection not disconnected.
441 if (currentState != STATE_DISCONNECTED
442 && currentState != STATE_DISCONNECTING) {
444 setState(STATE_DISCONNECTING);
445 tunnel.sendMessage("disconnect");
447 setState(STATE_DISCONNECTED);
452 guac_client.connect = function(data) {
454 setState(STATE_CONNECTING);
457 tunnel.connect(data);
460 setState(STATE_IDLE);
464 setState(STATE_WAITING);