Prompt when logging out from the button in the yellow bar (users may think this will...
[guacamole.git] / src / main / webapp / scripts / interface.js
1
2 // UI Definition
3 var GuacamoleUI = {
4
5     "LOGOUT_PROMPT" :   "Logging out will disconnect all of your active "
6                       + "Guacamole sessions. Are you sure you wish to log out?",
7
8     /* Detection Constants */
9     
10     "LONG_PRESS_DETECT_TIMEOUT"     : 800, /* milliseconds */
11     "LONG_PRESS_MOVEMENT_THRESHOLD" : 10,  /* pixels */
12     "MENU_CLOSE_DETECT_TIMEOUT"     : 500, /* milliseconds */
13     "MENU_OPEN_DETECT_TIMEOUT"      : 325, /* milliseconds */
14     "KEYBOARD_AUTO_RESIZE_INTERVAL" : 30,  /* milliseconds */
15
16     /* Animation Constants */
17
18     "MENU_SHADE_STEPS"    : 10, /* frames */
19     "MENU_SHADE_INTERVAL" : 30, /* milliseconds */
20     "MENU_SHOW_STEPS"     : 5,  /* frames */
21     "MENU_SHOW_INTERVAL"  : 30, /* milliseconds */
22
23     /* OSK Mode Constants */
24     "OSK_MODE_NATIVE" : 1, /* "Show Keyboard" will show the platform's native OSK */
25     "OSK_MODE_GUAC"   : 2, /* "Show Keyboard" will show Guac's built-in OSK */
26
27     /* UI Elements */
28
29     "viewport"    : document.getElementById("viewportClone"),
30     "display"     : document.getElementById("display"),
31     "menu"        : document.getElementById("menu"),
32     "menuControl" : document.getElementById("menuControl"),
33     "touchMenu"   : document.getElementById("touchMenu"),
34     "logo"        : document.getElementById("status-logo"),
35     "eventTarget" : document.getElementById("eventTarget"),
36
37     "buttons": {
38
39         "showClipboard": document.getElementById("showClipboard"),
40         "showKeyboard" : document.getElementById("showKeyboard"),
41         "ctrlAltDelete": document.getElementById("ctrlAltDelete"),
42         "reconnect"    : document.getElementById("reconnect"),
43         "logout"       : document.getElementById("logout"),
44
45         "touchShowClipboard" : document.getElementById("touchShowClipboard"),
46         "touchShowKeyboard"  : document.getElementById("touchShowKeyboard"),
47         "touchLogout"        : document.getElementById("touchLogout")
48
49     },
50
51     "containers": {
52         "state"    : document.getElementById("statusDialog"),
53         "clipboard": document.getElementById("clipboardDiv"),
54         "keyboard" : document.getElementById("keyboardContainer")
55     },
56     
57     "state"     : document.getElementById("statusText"),
58     "clipboard" : document.getElementById("clipboard")
59
60 };
61
62 // Constant UI initialization and behavior
63 (function() {
64
65     var menu_shaded = false;
66
67     var shade_interval = null;
68     var show_interval = null;
69
70     // Cache error image (might not be available when error occurs)
71     var guacErrorImage = new Image();
72     guacErrorImage.src = "images/noguacamole-logo-24.png";
73
74     // Function for adding a class to an element
75     var addClass;
76
77     // Function for removing a class from an element
78     var removeClass;
79
80     // If Node.classList is supported, implement addClass/removeClass using that
81     if (Node.classList) {
82
83         addClass = function(element, classname) {
84             element.classList.add(classname);
85         };
86         
87         removeClass = function(element, classname) {
88             element.classList.remove(classname);
89         };
90         
91     }
92
93     // Otherwise, implement own
94     else {
95
96         addClass = function(element, classname) {
97
98             // Simply add new class
99             element.className += " " + classname;
100
101         };
102         
103         removeClass = function(element, classname) {
104
105             // Filter out classes with given name
106             element.className = element.className.replace(/([^ ]+)[ ]*/g,
107                 function(match, testClassname, spaces, offset, string) {
108
109                     // If same class, remove
110                     if (testClassname == classname)
111                         return "";
112
113                     // Otherwise, allow
114                     return match;
115                     
116                 }
117             );
118
119         };
120         
121     }
122
123
124     GuacamoleUI.hideStatus = function() {
125         removeClass(document.body, "guac-error");
126         GuacamoleUI.containers.state.style.visibility = "hidden";
127         GuacamoleUI.display.style.opacity = "1";
128     };
129     
130     GuacamoleUI.showStatus = function(text) {
131         removeClass(document.body, "guac-error");
132         GuacamoleUI.containers.state.style.visibility = "visible";
133         GuacamoleUI.state.textContent = text;
134         GuacamoleUI.display.style.opacity = "1";
135     };
136     
137     GuacamoleUI.showError = function(error) {
138         addClass(document.body, "guac-error");
139         GuacamoleUI.state.textContent = error;
140         GuacamoleUI.display.style.opacity = "0.1";
141     };
142
143     GuacamoleUI.hideTouchMenu = function() {
144         GuacamoleUI.touchMenu.style.visibility = "hidden";
145     };
146     
147     GuacamoleUI.showTouchMenu = function() {
148         
149         GuacamoleUI.touchMenu.style.left =
150             ((GuacamoleUI.viewport.offsetWidth - GuacamoleUI.touchMenu.offsetWidth) / 2
151             + window.pageXOffset)
152             + "px";
153
154         GuacamoleUI.touchMenu.style.top =
155             ((GuacamoleUI.viewport.offsetHeight - GuacamoleUI.touchMenu.offsetHeight) / 2
156             + window.pageYOffset)
157             + "px";
158
159         GuacamoleUI.touchMenu.style.visibility = "visible";
160         
161     };
162
163     GuacamoleUI.shadeMenu = function() {
164
165         if (!menu_shaded) {
166
167             var step = Math.floor(GuacamoleUI.menu.offsetHeight / GuacamoleUI.MENU_SHADE_STEPS) + 1;
168             var offset = 0;
169             menu_shaded = true;
170
171             window.clearInterval(show_interval);
172             shade_interval = window.setInterval(function() {
173
174                 offset -= step;
175
176                 GuacamoleUI.menu.style.transform =
177                 GuacamoleUI.menu.style.WebkitTransform =
178                 GuacamoleUI.menu.style.MozTransform =
179                 GuacamoleUI.menu.style.OTransform =
180                 GuacamoleUI.menu.style.msTransform =
181
182                     "translateY(" + offset + "px)";
183
184                 if (offset <= -GuacamoleUI.menu.offsetHeight) {
185                     window.clearInterval(shade_interval);
186                     GuacamoleUI.menu.style.visiblity = "hidden";
187                 }
188
189             }, GuacamoleUI.MENU_SHADE_INTERVAL);
190         }
191
192     };
193
194     GuacamoleUI.showMenu = function() {
195
196         if (menu_shaded) {
197
198             var step = Math.floor(GuacamoleUI.menu.offsetHeight / GuacamoleUI.MENU_SHOW_STEPS) + 1;
199             var offset = -GuacamoleUI.menu.offsetHeight;
200             menu_shaded = false;
201             GuacamoleUI.menu.style.visiblity = "";
202
203             window.clearInterval(shade_interval);
204             show_interval = window.setInterval(function() {
205
206                 offset += step;
207
208                 if (offset >= 0) {
209                     offset = 0;
210                     window.clearInterval(show_interval);
211                 }
212
213                 GuacamoleUI.menu.style.transform =
214                 GuacamoleUI.menu.style.WebkitTransform =
215                 GuacamoleUI.menu.style.MozTransform =
216                 GuacamoleUI.menu.style.OTransform =
217                 GuacamoleUI.menu.style.msTransform =
218
219                     "translateY(" + offset + "px)";
220
221             }, GuacamoleUI.MENU_SHOW_INTERVAL);
222         }
223
224     };
225
226     // Show/Hide clipboard
227     GuacamoleUI.buttons.showClipboard.onclick = function() {
228
229         var displayed = GuacamoleUI.containers.clipboard.style.display;
230         if (displayed != "block") {
231             GuacamoleUI.containers.clipboard.style.display = "block";
232             GuacamoleUI.buttons.showClipboard.innerHTML = "Hide Clipboard";
233         }
234         else {
235             GuacamoleUI.containers.clipboard.style.display = "none";
236             GuacamoleUI.buttons.showClipboard.innerHTML = "Show Clipboard";
237             GuacamoleUI.clipboard.onchange();
238         }
239
240     };
241
242     GuacamoleUI.buttons.touchShowClipboard.onclick = function() {
243         // FIXME: Implement
244         alert("Not yet implemented... Sorry.");
245     };
246
247     // Show/Hide keyboard
248     var keyboardResizeInterval = null;
249     GuacamoleUI.buttons.showKeyboard.onclick = function() {
250
251         // If Guac OSK shown, hide it.
252         var displayed = GuacamoleUI.containers.keyboard.style.display;
253         if (displayed == "block") {
254             GuacamoleUI.containers.keyboard.style.display = "none";
255             GuacamoleUI.buttons.showKeyboard.textContent = "Show Keyboard";
256
257             window.onresize = null;
258             window.clearInterval(keyboardResizeInterval);
259         }
260
261         // Otherwise, show it
262         else {
263
264             // Ensure event target is NOT focused if we are using the Guac OSK.
265             GuacamoleUI.eventTarget.blur();
266
267             GuacamoleUI.containers.keyboard.style.display = "block";
268             GuacamoleUI.buttons.showKeyboard.textContent = "Hide Keyboard";
269
270             // Automatically update size
271             window.onresize = updateKeyboardSize;
272             keyboardResizeInterval = window.setInterval(updateKeyboardSize,
273                 GuacamoleUI.KEYBOARD_AUTO_RESIZE_INTERVAL);
274
275             updateKeyboardSize();
276
277         }
278
279     };
280
281     // Touch-specific keyboard show
282     GuacamoleUI.buttons.touchShowKeyboard.onclick = 
283         function(e) {
284
285             // Center event target in case browser automatically centers
286             // input fields on focus.
287             GuacamoleUI.eventTarget.style.left =
288                 (window.pageXOffset + GuacamoleUI.viewport.offsetWidth / 2) + "px";
289
290             GuacamoleUI.eventTarget.style.top =
291                 (window.pageYOffset + GuacamoleUI.viewport.offsetHeight / 2) + "px";
292
293             GuacamoleUI.eventTarget.focus();
294             GuacamoleUI.hideTouchMenu();
295
296         };
297
298     // Logout
299     GuacamoleUI.buttons.logout.onclick =
300     GuacamoleUI.buttons.touchLogout.onclick =
301         function() {
302
303             // Logout after warning user about session disconnect
304             if (confirm(GuacamoleUI.LOGOUT_PROMPT)) {
305                 window.location.href = "logout";
306                 GuacamoleUI.hideTouchMenu();
307             }
308             
309         };
310
311     // Timeouts for detecting if users wants menu to open or close
312     var detectMenuOpenTimeout = null;
313     var detectMenuCloseTimeout = null;
314
315     // Clear detection timeouts
316     GuacamoleUI.resetMenuDetect = function() {
317
318         if (detectMenuOpenTimeout != null) {
319             window.clearTimeout(detectMenuOpenTimeout);
320             detectMenuOpenTimeout = null;
321         }
322
323         if (detectMenuCloseTimeout != null) {
324             window.clearTimeout(detectMenuCloseTimeout);
325             detectMenuCloseTimeout = null;
326         }
327
328     };
329
330     // Initiate detection of menu open action. If not canceled through some
331     // user event, menu will open.
332     GuacamoleUI.startMenuOpenDetect = function() {
333
334         if (!detectMenuOpenTimeout) {
335
336             // Clear detection state
337             GuacamoleUI.resetMenuDetect();
338
339             // Wait and then show menu
340             detectMenuOpenTimeout = window.setTimeout(function() {
341
342                 // If menu opened via mouse, do not show native OSK
343                 GuacamoleUI.oskMode = GuacamoleUI.OSK_MODE_GUAC;
344
345                 GuacamoleUI.showMenu();
346                 detectMenuOpenTimeout = null;
347             }, GuacamoleUI.MENU_OPEN_DETECT_TIMEOUT);
348
349         }
350
351     };
352
353     // Initiate detection of menu close action. If not canceled through some
354     // user mouse event, menu will close.
355     GuacamoleUI.startMenuCloseDetect = function() {
356
357         if (!detectMenuCloseTimeout) {
358
359             // Clear detection state
360             GuacamoleUI.resetMenuDetect();
361
362             // Wait and then shade menu
363             detectMenuCloseTimeout = window.setTimeout(function() {
364                 GuacamoleUI.shadeMenu();
365                 detectMenuCloseTimeout = null;
366             }, GuacamoleUI.MENU_CLOSE_DETECT_TIMEOUT);
367
368         }
369
370     };
371
372     // Show menu if mouseover any part of menu
373     GuacamoleUI.menu.addEventListener('mouseover', GuacamoleUI.showMenu, true);
374
375     // Stop detecting menu state change intents if mouse is over menu
376     GuacamoleUI.menu.addEventListener('mouseover', GuacamoleUI.resetMenuDetect, true);
377
378     // When mouse hovers over top of screen, start detection of intent to open menu
379     GuacamoleUI.menuControl.addEventListener('mousemove', GuacamoleUI.startMenuOpenDetect, true);
380
381     var long_press_start_x = 0;
382     var long_press_start_y = 0;
383     var menuShowLongPressTimeout = null;
384
385     GuacamoleUI.startLongPressDetect = function() {
386
387         if (!menuShowLongPressTimeout) {
388
389             menuShowLongPressTimeout = window.setTimeout(function() {
390                 
391                 menuShowLongPressTimeout = null;
392
393                 // Assume native OSK if menu shown via long-press
394                 GuacamoleUI.oskMode = GuacamoleUI.OSK_MODE_NATIVE;
395                 GuacamoleUI.showTouchMenu();
396
397             }, GuacamoleUI.LONG_PRESS_DETECT_TIMEOUT);
398
399         }
400     };
401
402     GuacamoleUI.stopLongPressDetect = function() {
403         window.clearTimeout(menuShowLongPressTimeout);
404         menuShowLongPressTimeout = null;
405     };
406
407     // Detect long-press at bottom of screen
408     GuacamoleUI.display.addEventListener('touchstart', function(e) {
409         
410         // Close menu if shown
411         GuacamoleUI.shadeMenu();
412         GuacamoleUI.hideTouchMenu();
413         
414         // Record touch location
415         if (e.touches.length == 1) {
416             var touch = e.touches[0];
417             long_press_start_x = touch.screenX;
418             long_press_start_y = touch.screenY;
419         }
420         
421         // Start detection
422         GuacamoleUI.startLongPressDetect();
423         
424     }, true);
425
426     // Stop detection if touch moves significantly
427     GuacamoleUI.display.addEventListener('touchmove', function(e) {
428         
429         // If touch distance from start exceeds threshold, cancel long press
430         var touch = e.touches[0];
431         if (Math.abs(touch.screenX - long_press_start_x) >= GuacamoleUI.LONG_PRESS_MOVEMENT_THRESHOLD
432             || Math.abs(touch.screenY - long_press_start_y) >= GuacamoleUI.LONG_PRESS_MOVEMENT_THRESHOLD)
433             GuacamoleUI.stopLongPressDetect();
434         
435     }, true);
436
437     // Stop detection if press stops
438     GuacamoleUI.display.addEventListener('touchend', GuacamoleUI.stopLongPressDetect, true);
439
440     // Close menu on mouse movement
441     GuacamoleUI.display.addEventListener('mousemove', GuacamoleUI.startMenuCloseDetect, true);
442     GuacamoleUI.display.addEventListener('mousedown', GuacamoleUI.startMenuCloseDetect, true);
443
444     // Reconnect button
445     GuacamoleUI.buttons.reconnect.onclick = function() {
446         window.location.reload();
447     };
448
449     // On-screen keyboard
450     GuacamoleUI.keyboard = new Guacamole.OnScreenKeyboard("layouts/en-us-qwerty-mobile.xml");
451     GuacamoleUI.containers.keyboard.appendChild(GuacamoleUI.keyboard.getElement());
452
453     // Function for automatically updating keyboard size
454     var lastKeyboardWidth;
455     function updateKeyboardSize() {
456         var currentSize = GuacamoleUI.keyboard.getElement().offsetWidth;
457         if (lastKeyboardWidth != currentSize) {
458             GuacamoleUI.keyboard.resize(currentSize);
459             lastKeyboardWidth = currentSize;
460         }
461     };
462
463     // Turn off autocorrect and autocapitalization on eventTarget
464     GuacamoleUI.eventTarget.setAttribute("autocorrect", "off");
465     GuacamoleUI.eventTarget.setAttribute("autocapitalize", "off");
466
467     // Automatically reposition event target on scroll
468     window.addEventListener("scroll", function() {
469         GuacamoleUI.eventTarget.style.left = window.pageXOffset + "px";
470         GuacamoleUI.eventTarget.style.top = window.pageYOffset + "px";
471     });
472
473 })();
474
475 // Tie UI events / behavior to a specific Guacamole client
476 GuacamoleUI.attach = function(guac) {
477
478     var title_prefix = null;
479     var connection_name = "Guacamole"; 
480     
481     var guac_display = guac.getDisplay();
482
483     // Set document title appropriately, based on prefix and connection name
484     function updateTitle() {
485
486         // Use title prefix if present
487         if (title_prefix) {
488             
489             document.title = title_prefix;
490
491             // Include connection name, if present
492             if (connection_name)
493                 document.title += " " + connection_name;
494
495         }
496
497         // Otherwise, just set to connection name
498         else if (connection_name)
499             document.title = connection_name;
500
501     }
502
503     // When mouse enters display, start detection of intent to close menu
504     guac_display.addEventListener('mouseover', GuacamoleUI.startMenuCloseDetect, true);
505
506     guac_display.onclick = function(e) {
507         e.preventDefault();
508         return false;
509     };
510
511     // Mouse
512     var mouse = new Guacamole.Mouse(guac_display);
513     mouse.onmousedown = mouse.onmouseup = mouse.onmousemove =
514         function(mouseState) {
515        
516             // Determine mouse position within view
517             var mouse_view_x = mouseState.x + guac_display.offsetLeft - window.pageXOffset;
518             var mouse_view_y = mouseState.y + guac_display.offsetTop  - window.pageYOffset;
519
520             // Determine viewport dimensioins
521             var view_width  = GuacamoleUI.viewport.offsetWidth;
522             var view_height = GuacamoleUI.viewport.offsetHeight;
523
524             // Determine scroll amounts based on mouse position relative to document
525
526             var scroll_amount_x;
527             if (mouse_view_x > view_width)
528                 scroll_amount_x = mouse_view_x - view_width;
529             else if (mouse_view_x < 0)
530                 scroll_amount_x = mouse_view_x;
531             else
532                 scroll_amount_x = 0;
533
534             var scroll_amount_y;
535             if (mouse_view_y > view_height)
536                 scroll_amount_y = mouse_view_y - view_height;
537             else if (mouse_view_y < 0)
538                 scroll_amount_y = mouse_view_y;
539             else
540                 scroll_amount_y = 0;
541
542             // Scroll (if necessary) to keep mouse on screen.
543             window.scrollBy(scroll_amount_x, scroll_amount_y);
544        
545             // Send mouse event
546             guac.sendMouseState(mouseState);
547             
548         };
549
550     // Keyboard
551     var keyboard = new Guacamole.Keyboard(document);
552
553     // Monitor whether the event target is focused
554     var eventTargetFocused = false;
555
556     // Save length for calculation of changed value
557     var currentLength = GuacamoleUI.eventTarget.value.length;
558
559     GuacamoleUI.eventTarget.onfocus = function() {
560         eventTargetFocused = true;
561         GuacamoleUI.eventTarget.value = "";
562         currentLength = 0;
563     };
564
565     GuacamoleUI.eventTarget.onblur = function() {
566         eventTargetFocused = false;
567     };
568
569     // If text is input directly into event target without typing (as with
570     // voice input, for example), type automatically.
571     GuacamoleUI.eventTarget.oninput = function(e) {
572
573         // Calculate current length and change in length
574         var oldLength = currentLength;
575         currentLength = GuacamoleUI.eventTarget.value.length;
576         
577         // If deleted or replaced text, ignore
578         if (currentLength <= oldLength)
579             return;
580
581         // Get changed text
582         var text = GuacamoleUI.eventTarget.value.substring(oldLength);
583
584         // Send each character
585         for (var i=0; i<text.length; i++) {
586
587             // Get char code
588             var charCode = text.charCodeAt(i);
589
590             // Convert to keysym
591             var keysym = 0x003F; // Default to a question mark
592             if (charCode >= 0x0000 && charCode <= 0x00FF)
593                 keysym = charCode;
594             else if (charCode >= 0x0100 && charCode <= 0x10FFFF)
595                 keysym = 0x01000000 | charCode;
596
597             // Send keysym only if not already pressed
598             if (!keyboard.pressed[keysym]) {
599
600                 // Press and release key
601                 guac.sendKeyEvent(1, keysym);
602                 guac.sendKeyEvent(0, keysym);
603
604             }
605
606         }
607
608     }
609
610     function isTypableCharacter(keysym) {
611         return (keysym & 0xFFFF00) != 0xFF00;
612     }
613
614     function disableKeyboard() {
615         keyboard.onkeydown = null;
616         keyboard.onkeyup = null;
617     }
618
619     function enableKeyboard() {
620
621         keyboard.onkeydown = function (keysym) {
622             guac.sendKeyEvent(1, keysym);
623             return eventTargetFocused && isTypableCharacter(keysym);
624         };
625
626         keyboard.onkeyup = function (keysym) {
627             guac.sendKeyEvent(0, keysym);
628             return eventTargetFocused && isTypableCharacter(keysym);
629         };
630
631     }
632
633     // Enable keyboard by default
634     enableKeyboard();
635
636     // Handle client state change
637     guac.onstatechange = function(clientState) {
638
639         switch (clientState) {
640
641             // Idle
642             case 0:
643                 GuacamoleUI.showStatus("Idle.");
644                 title_prefix = "[Idle]";
645                 break;
646
647             // Connecting
648             case 1:
649                 GuacamoleUI.shadeMenu();
650                 GuacamoleUI.showStatus("Connecting...");
651                 title_prefix = "[Connecting...]";
652                 break;
653
654             // Connected + waiting
655             case 2:
656                 GuacamoleUI.showStatus("Connected, waiting for first update...");
657                 title_prefix = "[Waiting...]";
658                 break;
659
660             // Connected
661             case 3:
662                 GuacamoleUI.hideStatus();
663                 title_prefix = null;
664                 break;
665
666             // Disconnecting
667             case 4:
668                 GuacamoleUI.showStatus("Disconnecting...");
669                 title_prefix = "[Disconnecting...]";
670                 break;
671
672             // Disconnected
673             case 5:
674                 GuacamoleUI.showStatus("Disconnected.");
675                 title_prefix = "[Disconnected]";
676                 break;
677
678             // Unknown status code
679             default:
680                 GuacamoleUI.showStatus("[UNKNOWN STATUS]");
681
682         }
683
684         updateTitle();
685     };
686
687     // Name instruction handler
688     guac.onname = function(name) {
689         connection_name = name;
690         updateTitle();
691     };
692
693     // Error handler
694     guac.onerror = function(error) {
695
696         // Disconnect, if connected
697         guac.disconnect();
698
699         // Display error message
700         GuacamoleUI.showError(error);
701         
702     };
703
704     // Disconnect on close
705     window.onunload = function() {
706         guac.disconnect();
707     };
708
709     // Handle clipboard events
710     GuacamoleUI.clipboard.onchange = function() {
711
712         var text = GuacamoleUI.clipboard.value;
713         guac.setClipboard(text);
714
715     };
716
717     // Ignore keypresses when clipboard is focused
718     GuacamoleUI.clipboard.onfocus = function() {
719         disableKeyboard();
720     };
721
722     // Capture keypresses when clipboard is not focused
723     GuacamoleUI.clipboard.onblur = function() {
724         enableKeyboard();
725     };
726
727     // Server copy handler
728     guac.onclipboard = function(data) {
729         GuacamoleUI.clipboard.value = data;
730     };
731
732     GuacamoleUI.keyboard.onkeydown = function(keysym) {
733         guac.sendKeyEvent(1, keysym);
734     };
735
736     GuacamoleUI.keyboard.onkeyup = function(keysym) {
737         guac.sendKeyEvent(0, keysym);
738     };
739
740     // Send Ctrl-Alt-Delete
741     GuacamoleUI.buttons.ctrlAltDelete.onclick = function() {
742
743         var KEYSYM_CTRL   = 0xFFE3;
744         var KEYSYM_ALT    = 0xFFE9;
745         var KEYSYM_DELETE = 0xFFFF;
746
747         guac.sendKeyEvent(1, KEYSYM_CTRL);
748         guac.sendKeyEvent(1, KEYSYM_ALT);
749         guac.sendKeyEvent(1, KEYSYM_DELETE);
750         guac.sendKeyEvent(0, KEYSYM_DELETE);
751         guac.sendKeyEvent(0, KEYSYM_ALT);
752         guac.sendKeyEvent(0, KEYSYM_CTRL);
753     };
754
755 };