Rename assumeNativeOSK to nativeOSK, automatically reset nativeOSK to false if menu...
[guacamole.git] / src / main / webapp / scripts / interface.js
1
2 // UI Definition
3 var GuacamoleUI = {
4
5     "viewport"    : document.getElementById("viewportClone"),
6     "display"     : document.getElementById("display"),
7     "menu"        : document.getElementById("menu"),
8     "menuControl" : document.getElementById("menuControl"),
9     "touchMenu"   : document.getElementById("touchMenu"),
10     "logo"        : document.getElementById("status-logo"),
11     "eventTarget" : document.getElementById("eventTarget"),
12
13     "buttons": {
14
15         "showClipboard": document.getElementById("showClipboard"),
16         "showKeyboard" : document.getElementById("showKeyboard"),
17         "ctrlAltDelete": document.getElementById("ctrlAltDelete"),
18         "reconnect"    : document.getElementById("reconnect"),
19         "logout"       : document.getElementById("logout")
20
21     },
22
23     "containers": {
24         "state"    : document.getElementById("statusDialog"),
25         "clipboard": document.getElementById("clipboardDiv"),
26         "keyboard" : document.getElementById("keyboardContainer")
27     },
28     
29     "state"     : document.getElementById("statusText"),
30     "clipboard" : document.getElementById("clipboard")
31
32 };
33
34 // Constant UI initialization and behavior
35 (function() {
36
37     var menu_shaded = false;
38
39     var shade_interval = null;
40     var show_interval = null;
41
42     // Cache error image (might not be available when error occurs)
43     var guacErrorImage = new Image();
44     guacErrorImage.src = "images/noguacamole-logo-24.png";
45
46     // Function for adding a class to an element
47     var addClass;
48
49     // Function for removing a class from an element
50     var removeClass;
51
52     // If Node.classList is supported, implement addClass/removeClass using that
53     if (Node.classList) {
54
55         addClass = function(element, classname) {
56             element.classList.add(classname);
57         };
58         
59         removeClass = function(element, classname) {
60             element.classList.remove(classname);
61         };
62         
63     }
64
65     // Otherwise, implement own
66     else {
67
68         addClass = function(element, classname) {
69
70             // Simply add new class
71             element.className += " " + classname;
72
73         };
74         
75         removeClass = function(element, classname) {
76
77             // Filter out classes with given name
78             element.className = element.className.replace(/([^ ]+)[ ]*/g,
79                 function(match, testClassname, spaces, offset, string) {
80
81                     // If same class, remove
82                     if (testClassname == classname)
83                         return "";
84
85                     // Otherwise, allow
86                     return match;
87                     
88                 }
89             );
90
91         };
92         
93     }
94
95
96     GuacamoleUI.hideStatus = function() {
97         removeClass(document.body, "guac-error");
98         GuacamoleUI.containers.state.style.visibility = "hidden";
99         GuacamoleUI.display.style.opacity = "1";
100     };
101     
102     GuacamoleUI.showStatus = function(text) {
103         removeClass(document.body, "guac-error");
104         GuacamoleUI.containers.state.style.visibility = "visible";
105         GuacamoleUI.state.textContent = text;
106         GuacamoleUI.display.style.opacity = "1";
107     };
108     
109     GuacamoleUI.showError = function(error) {
110         addClass(document.body, "guac-error");
111         GuacamoleUI.state.textContent = error;
112         GuacamoleUI.display.style.opacity = "0.1";
113     };
114
115     GuacamoleUI.shadeMenu = function() {
116
117         if (!menu_shaded) {
118
119             var step = Math.floor(GuacamoleUI.menu.offsetHeight / 10) + 1;
120             var offset = 0;
121             menu_shaded = true;
122
123             window.clearInterval(show_interval);
124             shade_interval = window.setInterval(function() {
125
126                 offset -= step;
127                 GuacamoleUI.menu.style.top = offset + "px";
128
129                 if (offset <= -GuacamoleUI.menu.offsetHeight) {
130                     window.clearInterval(shade_interval);
131                     GuacamoleUI.menu.style.visiblity = "hidden";
132                 }
133
134             }, 30);
135         }
136
137     };
138
139     GuacamoleUI.showMenu = function() {
140
141         if (menu_shaded) {
142
143             var step = Math.floor(GuacamoleUI.menu.offsetHeight / 5) + 1;
144             var offset = -GuacamoleUI.menu.offsetHeight;
145             menu_shaded = false;
146             GuacamoleUI.menu.style.visiblity = "";
147
148             window.clearInterval(shade_interval);
149             show_interval = window.setInterval(function() {
150
151                 offset += step;
152
153                 if (offset >= 0) {
154                     offset = 0;
155                     window.clearInterval(show_interval);
156                 }
157
158                 GuacamoleUI.menu.style.top = offset + "px";
159
160             }, 30);
161         }
162
163     };
164
165     // Show/Hide clipboard
166     GuacamoleUI.buttons.showClipboard.onclick = function() {
167
168         var displayed = GuacamoleUI.containers.clipboard.style.display;
169         if (displayed != "block") {
170             GuacamoleUI.containers.clipboard.style.display = "block";
171             GuacamoleUI.buttons.showClipboard.innerHTML = "Hide Clipboard";
172         }
173         else {
174             GuacamoleUI.containers.clipboard.style.display = "none";
175             GuacamoleUI.buttons.showClipboard.innerHTML = "Show Clipboard";
176             GuacamoleUI.clipboard.onchange();
177         }
178
179     };
180
181     // Assume no native OSK by default
182     GuacamoleUI.nativeOSK = false;
183
184     // Show/Hide keyboard
185     var keyboardResizeInterval = null;
186     GuacamoleUI.buttons.showKeyboard.onclick = function() {
187
188         // If we think the platform has a native OSK, use the event target to
189         // cause it to display.
190         if (GuacamoleUI.nativeOSK) {
191             GuacamoleUI.eventTarget.focus();
192             return;
193         }
194         
195         var displayed = GuacamoleUI.containers.keyboard.style.display;
196         if (displayed != "block") {
197             GuacamoleUI.containers.keyboard.style.display = "block";
198             GuacamoleUI.buttons.showKeyboard.textContent = "Hide Keyboard";
199
200             // Automatically update size
201             window.onresize = updateKeyboardSize;
202             keyboardResizeInterval = window.setInterval(updateKeyboardSize, 30);
203
204             updateKeyboardSize();
205         }
206         else {
207             GuacamoleUI.containers.keyboard.style.display = "none";
208             GuacamoleUI.buttons.showKeyboard.textContent = "Show Keyboard";
209
210             window.onresize = null;
211             window.clearInterval(keyboardResizeInterval);
212         }
213
214     };
215
216     // Logout
217     GuacamoleUI.buttons.logout.onclick = function() {
218         window.location.href = "logout";
219     };
220
221     // Timeouts for detecting if users wants menu to open or close
222     var detectMenuOpenTimeout = null;
223     var detectMenuCloseTimeout = null;
224
225     // Clear detection timeouts
226     GuacamoleUI.resetMenuDetect = function() {
227
228         if (detectMenuOpenTimeout != null) {
229             window.clearTimeout(detectMenuOpenTimeout);
230             detectMenuOpenTimeout = null;
231         }
232
233         if (detectMenuCloseTimeout != null) {
234             window.clearTimeout(detectMenuCloseTimeout);
235             detectMenuCloseTimeout = null;
236         }
237
238     };
239
240     // Initiate detection of menu open action. If not canceled through some
241     // user event, menu will open.
242     GuacamoleUI.startMenuOpenDetect = function() {
243
244         if (!detectMenuOpenTimeout) {
245
246             // Clear detection state
247             GuacamoleUI.resetMenuDetect();
248
249             // Wait and then show menu
250             detectMenuOpenTimeout = window.setTimeout(function() {
251
252                 // If menu opened via mouse, do not show native OSK
253                 GuacamoleUI.nativeOSK = false;
254
255                 GuacamoleUI.showMenu();
256                 detectMenuOpenTimeout = null;
257             }, 325);
258
259         }
260
261     };
262
263     // Initiate detection of menu close action. If not canceled through some
264     // user mouse event, menu will close.
265     GuacamoleUI.startMenuCloseDetect = function() {
266
267         if (!detectMenuCloseTimeout) {
268
269             // Clear detection state
270             GuacamoleUI.resetMenuDetect();
271
272             // Wait and then shade menu
273             detectMenuCloseTimeout = window.setTimeout(function() {
274                 GuacamoleUI.shadeMenu();
275                 detectMenuCloseTimeout = null;
276             }, 500);
277
278         }
279
280     };
281
282     // Show menu if mouseover any part of menu
283     GuacamoleUI.menu.addEventListener('mouseover', GuacamoleUI.showMenu, true);
284
285     // Stop detecting menu state change intents if mouse is over menu
286     GuacamoleUI.menu.addEventListener('mouseover', GuacamoleUI.resetMenuDetect, true);
287
288     // When mouse hovers over top of screen, start detection of intent to open menu
289     GuacamoleUI.menuControl.addEventListener('mousemove', GuacamoleUI.startMenuOpenDetect, true);
290
291     var menuShowLongPressTimeout = null;
292
293     GuacamoleUI.startLongPressDetect = function() {
294
295         if (!menuShowLongPressTimeout) {
296
297             menuShowLongPressTimeout = window.setTimeout(function() {
298                 
299                 menuShowLongPressTimeout = null;
300
301                 // Assume native OSK if menu shown via long-press
302                 GuacamoleUI.nativeOSK = true;
303                 GuacamoleUI.showMenu();
304
305             }, 800);
306
307         }
308     };
309
310     GuacamoleUI.stopLongPressDetect = function() {
311         window.clearTimeout(menuShowLongPressTimeout);
312         menuShowLongPressTimeout = null;
313     };
314
315     // Reset event target (add content, reposition cursor in middle.
316     GuacamoleUI.resetEventTarget = function() {
317         GuacamoleUI.eventTarget.value = "GUAC";
318         GuacamoleUI.eventTarget.selectionStart =
319         GuacamoleUI.eventTarget.selectionEnd   = 2;
320     };
321
322     // Detect long-press at bottom of screen
323     document.body.addEventListener('touchstart', GuacamoleUI.startLongPressDetect, true);
324
325     // Reconnect button
326     GuacamoleUI.buttons.reconnect.onclick = function() {
327         window.location.reload();
328     };
329
330     // On-screen keyboard
331     GuacamoleUI.keyboard = new Guacamole.OnScreenKeyboard("layouts/en-us-qwerty-mobile.xml");
332     GuacamoleUI.containers.keyboard.appendChild(GuacamoleUI.keyboard.getElement());
333
334     // Function for automatically updating keyboard size
335     var lastKeyboardWidth;
336     function updateKeyboardSize() {
337         var currentSize = GuacamoleUI.keyboard.getElement().offsetWidth;
338         if (lastKeyboardWidth != currentSize) {
339             GuacamoleUI.keyboard.resize(currentSize);
340             lastKeyboardWidth = currentSize;
341         }
342     };
343
344 })();
345
346 // Tie UI events / behavior to a specific Guacamole client
347 GuacamoleUI.attach = function(guac) {
348
349     var guac_display = guac.getDisplay();
350
351     // When mouse enters display, start detection of intent to close menu
352     guac_display.addEventListener('mouseover', GuacamoleUI.startMenuCloseDetect, true);
353
354     guac_display.onclick = function(e) {
355         e.preventDefault();
356         return false;
357     };
358
359     // Mouse
360     var mouse = new Guacamole.Mouse(guac_display);
361     mouse.onmousedown = mouse.onmouseup = mouse.onmousemove =
362         function(mouseState) {
363        
364             // Determine mouse position within view
365             var mouse_view_x = mouseState.x + guac_display.offsetLeft - window.pageXOffset;
366             var mouse_view_y = mouseState.y + guac_display.offsetTop  - window.pageYOffset;
367
368             // Determine viewport dimensioins
369             var view_width  = GuacamoleUI.viewport.offsetWidth;
370             var view_height = GuacamoleUI.viewport.offsetHeight;
371
372             // Determine scroll amounts based on mouse position relative to document
373
374             var scroll_amount_x;
375             if (mouse_view_x > view_width)
376                 scroll_amount_x = mouse_view_x - view_width;
377             else if (mouse_view_x < 0)
378                 scroll_amount_x = mouse_view_x;
379             else
380                 scroll_amount_x = 0;
381
382             var scroll_amount_y;
383             if (mouse_view_y > view_height)
384                 scroll_amount_y = mouse_view_y - view_height;
385             else if (mouse_view_y < 0)
386                 scroll_amount_y = mouse_view_y;
387             else
388                 scroll_amount_y = 0;
389
390             // Scroll (if necessary) to keep mouse on screen.
391             window.scrollBy(scroll_amount_x, scroll_amount_y);
392        
393             // Hide menu on movement
394             GuacamoleUI.startMenuCloseDetect();
395
396             // Stop detecting long presses if mouse is being used
397             GuacamoleUI.stopLongPressDetect();
398
399             // Send mouse event
400             guac.sendMouseState(mouseState);
401             
402         };
403
404     // Keyboard
405     var keyboard = new Guacamole.Keyboard(document);
406
407     function disableKeyboard() {
408         keyboard.onkeydown = null;
409         keyboard.onkeyup = null;
410     }
411
412     function enableKeyboard() {
413         keyboard.onkeydown = 
414             function (keysym) {
415           
416                 // If we're using native OSK, ensure event target is reset
417                 // on each key event.
418                 if (GuacamoleUI.nativeOSK)
419                     GuacamoleUI.resetEventTarget();
420                 
421                 guac.sendKeyEvent(1, keysym);
422             };
423
424         keyboard.onkeyup = 
425             function (keysym) {
426                 guac.sendKeyEvent(0, keysym);
427             };
428     }
429
430     // Enable keyboard by default
431     enableKeyboard();
432
433     // Handle client state change
434     guac.onstatechange = function(clientState) {
435         switch (clientState) {
436
437             // Idle
438             case 0:
439                 GuacamoleUI.showStatus("Idle.");
440                 break;
441
442             // Connecting
443             case 1:
444                 GuacamoleUI.shadeMenu();
445                 GuacamoleUI.showStatus("Connecting...");
446                 break;
447
448             // Connected + waiting
449             case 2:
450                 GuacamoleUI.showStatus("Connected, waiting for first update...");
451                 break;
452
453             // Connected
454             case 3:
455                 
456                 GuacamoleUI.hideStatus();
457                 GuacamoleUI.display.className =
458                     GuacamoleUI.display.className.replace(/guac-loading/, '');
459
460                 GuacamoleUI.menu.className = "connected";
461                 break;
462
463             // Disconnecting
464             case 4:
465                 GuacamoleUI.showStatus("Disconnecting...");
466                 break;
467
468             // Disconnected
469             case 5:
470                 GuacamoleUI.showStatus("Disconnected.");
471                 break;
472
473             // Unknown status code
474             default:
475                 GuacamoleUI.showStatus("[UNKNOWN STATUS]");
476
477         }
478     };
479
480     // Name instruction handler
481     guac.onname = function(name) {
482         document.title = name;
483     };
484
485     // Error handler
486     guac.onerror = function(error) {
487
488         // Disconnect, if connected
489         guac.disconnect();
490
491         // Display error message
492         GuacamoleUI.showError(error);
493         
494     };
495
496     // Disconnect on close
497     window.onunload = function() {
498         guac.disconnect();
499     };
500
501     // Handle clipboard events
502     GuacamoleUI.clipboard.onchange = function() {
503
504         var text = GuacamoleUI.clipboard.value;
505         guac.setClipboard(text);
506
507     };
508
509     // Ignore keypresses when clipboard is focused
510     GuacamoleUI.clipboard.onfocus = function() {
511         disableKeyboard();
512     };
513
514     // Capture keypresses when clipboard is not focused
515     GuacamoleUI.clipboard.onblur = function() {
516         enableKeyboard();
517     };
518
519     // Server copy handler
520     guac.onclipboard = function(data) {
521         GuacamoleUI.clipboard.value = data;
522     };
523
524     GuacamoleUI.keyboard.onkeydown = function(keysym) {
525         guac.sendKeyEvent(1, keysym);
526     };
527
528     GuacamoleUI.keyboard.onkeyup = function(keysym) {
529         guac.sendKeyEvent(0, keysym);
530     };
531
532     // Send Ctrl-Alt-Delete
533     GuacamoleUI.buttons.ctrlAltDelete.onclick = function() {
534
535         var KEYSYM_CTRL   = 0xFFE3;
536         var KEYSYM_ALT    = 0xFFE9;
537         var KEYSYM_DELETE = 0xFFFF;
538
539         guac.sendKeyEvent(1, KEYSYM_CTRL);
540         guac.sendKeyEvent(1, KEYSYM_ALT);
541         guac.sendKeyEvent(1, KEYSYM_DELETE);
542         guac.sendKeyEvent(0, KEYSYM_DELETE);
543         guac.sendKeyEvent(0, KEYSYM_ALT);
544         guac.sendKeyEvent(0, KEYSYM_CTRL);
545     };
546
547 };