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