Experimental auto-scroll of UI when mouse goes offscreen.
[guacamole.git] / src / main / webapp / scripts / interface.js
1
2 // UI Definition
3 var GuacamoleUI = {
4
5     "display"     : document.getElementById("display"),
6     "menu"        : document.getElementById("menu"),
7     "menuControl" : document.getElementById("menuControl"),
8     "logo"        : document.getElementById("status-logo"),
9     "state"       : document.getElementById("state"),
10
11     "buttons": {
12
13         "showClipboard": document.getElementById("showClipboard"),
14         "showKeyboard" : document.getElementById("showKeyboard"),
15         "ctrlAltDelete": document.getElementById("ctrlAltDelete"),
16         "reconnect"    : document.getElementById("reconnect"),
17         "logout"       : document.getElementById("logout")
18
19     },
20
21     "containers": {
22         "error"    : document.getElementById("errorDialog"),
23         "clipboard": document.getElementById("clipboardDiv"),
24         "keyboard" : document.getElementById("keyboardContainer")
25     },
26     
27     "error"     : document.getElementById("errorText"),
28     "clipboard" : document.getElementById("clipboard")
29
30 };
31
32 // Constant UI initialization and behavior
33 (function() {
34
35     var menu_shaded = false;
36
37     var shade_interval = null;
38     var show_interval = null;
39
40     // Cache error image (might not be available when error occurs)
41     var guacErrorImage = new Image();
42     guacErrorImage.src = "images/noguacamole-logo-24.png";
43
44     GuacamoleUI.showError = function(error) {
45
46         GuacamoleUI.menu.className = "error";
47         GuacamoleUI.display.className += " guac-error";
48
49         GuacamoleUI.logo.src = guacErrorImage.src;
50         GuacamoleUI.error.textContent = error;
51         GuacamoleUI.containers.error.style.visibility = "visible";
52
53     };
54
55     GuacamoleUI.shadeMenu = function() {
56
57         if (!menu_shaded) {
58
59             var step = Math.floor(GuacamoleUI.menu.offsetHeight / 10) + 1;
60             var offset = 0;
61             menu_shaded = true;
62
63             window.clearInterval(show_interval);
64             shade_interval = window.setInterval(function() {
65
66                 offset -= step;
67                 GuacamoleUI.menu.style.top = offset + "px";
68
69                 if (offset <= -GuacamoleUI.menu.offsetHeight) {
70                     window.clearInterval(shade_interval);
71                     GuacamoleUI.menu.style.visiblity = "hidden";
72                 }
73
74             }, 30);
75         }
76
77     };
78
79     GuacamoleUI.showMenu = function() {
80
81         if (menu_shaded) {
82
83             var step = Math.floor(GuacamoleUI.menu.offsetHeight / 5) + 1;
84             var offset = -GuacamoleUI.menu.offsetHeight;
85             menu_shaded = false;
86             GuacamoleUI.menu.style.visiblity = "";
87
88             window.clearInterval(shade_interval);
89             show_interval = window.setInterval(function() {
90
91                 offset += step;
92
93                 if (offset >= 0) {
94                     offset = 0;
95                     window.clearInterval(show_interval);
96                 }
97
98                 GuacamoleUI.menu.style.top = offset + "px";
99
100             }, 30);
101         }
102
103     };
104
105     // Show/Hide clipboard
106     GuacamoleUI.buttons.showClipboard.onclick = function() {
107
108         var displayed = GuacamoleUI.containers.clipboard.style.display;
109         if (displayed != "block") {
110             GuacamoleUI.containers.clipboard.style.display = "block";
111             GuacamoleUI.buttons.showClipboard.innerHTML = "Hide Clipboard";
112         }
113         else {
114             GuacamoleUI.containers.clipboard.style.display = "none";
115             GuacamoleUI.buttons.showClipboard.innerHTML = "Show Clipboard";
116             GuacamoleUI.clipboard.onchange();
117         }
118
119     };
120
121     // Show/Hide keyboard
122     var keyboardResizeInterval = null;
123     GuacamoleUI.buttons.showKeyboard.onclick = function() {
124
125         var displayed = GuacamoleUI.containers.keyboard.style.display;
126         if (displayed != "block") {
127             GuacamoleUI.containers.keyboard.style.display = "block";
128             GuacamoleUI.buttons.showKeyboard.textContent = "Hide Keyboard";
129
130             // Automatically update size
131             window.onresize = updateKeyboardSize;
132             keyboardResizeInterval = window.setInterval(updateKeyboardSize, 30);
133
134             updateKeyboardSize();
135         }
136         else {
137             GuacamoleUI.containers.keyboard.style.display = "none";
138             GuacamoleUI.buttons.showKeyboard.textContent = "Show Keyboard";
139
140             window.onresize = null;
141             window.clearInterval(keyboardResizeInterval);
142         }
143
144     };
145
146     // Logout
147     GuacamoleUI.buttons.logout.onclick = function() {
148         window.location.href = "logout";
149     };
150
151     // Timeouts for detecting if users wants menu to open or close
152     var detectMenuOpenTimeout = null;
153     var detectMenuCloseTimeout = null;
154
155     // Clear detection timeouts
156     function resetMenuDetect() {
157
158         if (detectMenuOpenTimeout != null) {
159             window.clearTimeout(detectMenuOpenTimeout);
160             detectMenuOpenTimeout = null;
161         }
162
163         if (detectMenuCloseTimeout != null) {
164             window.clearTimeout(detectMenuCloseTimeout);
165             detectMenuCloseTimeout = null;
166         }
167
168     }
169
170     // Initiate detection of menu open action. If not canceled through some
171     // user event, menu will open.
172     function startMenuOpenDetect() {
173
174         // Clear detection state
175         resetMenuDetect();
176
177         // Wait and then show menu
178         detectMenuOpenTimeout = window.setTimeout(function() {
179             GuacamoleUI.showMenu();
180             detectMenuOpenTimeout = null;
181         }, 325);
182
183     }
184
185     // Initiate detection of menu close action. If not canceled through some
186     // user event, menu will close.
187     function startMenuCloseDetect() {
188
189         // Clear detection state
190         resetMenuDetect();
191
192         // Wait and then shade menu
193         detectMenuCloseTimeout = window.setTimeout(function() {
194             GuacamoleUI.shadeMenu();
195             detectMenuCloseTimeout = null;
196         }, 500);
197
198     }
199
200     // Show menu if mouseover any part of menu
201     GuacamoleUI.menu.addEventListener('mouseover', GuacamoleUI.showMenu, true);
202
203     // Stop detecting menu state change intents if mouse is over menu
204     GuacamoleUI.menu.addEventListener('mouseover', resetMenuDetect, true);
205
206     // When mouse hovers over top of screen, start detection of intent to open menu
207     GuacamoleUI.menuControl.addEventListener('mousemove', startMenuOpenDetect, true);
208
209     // When mouse enters display, start detection of intent to close menu
210     GuacamoleUI.display.addEventListener('mouseover', startMenuCloseDetect, true);
211
212     // Show menu if mouse leaves document
213     document.addEventListener('mouseout', function(e) {
214         
215         // Get parent of the element the mouse pointer is leaving
216         if (!e) e = window.event;
217         var target = e.relatedTarget || e.toElement;
218         
219         // Ensure target is not document nor child of document
220         var targetParent = target;
221         while (targetParent != null) {
222             if (targetParent == document) return;
223             targetParent = targetParent.parentNode;
224         }
225
226         // Start detection of intent to open menu
227         startMenuOpenDetect();
228  
229     }, true);
230
231     // Reconnect button
232     GuacamoleUI.buttons.reconnect.onclick = function() {
233         window.location.reload();
234     };
235
236     GuacamoleUI.display.onclick = function(e) {
237         e.preventDefault();
238         return false;
239     };
240
241     // On-screen keyboard
242     GuacamoleUI.keyboard = new Guacamole.OnScreenKeyboard("layouts/en-us-qwerty-mobile.xml");
243     GuacamoleUI.containers.keyboard.appendChild(GuacamoleUI.keyboard.getElement());
244
245     // Function for automatically updating keyboard size
246     var lastKeyboardWidth;
247     function updateKeyboardSize() {
248         var currentSize = GuacamoleUI.keyboard.getElement().offsetWidth;
249         if (lastKeyboardWidth != currentSize) {
250             GuacamoleUI.keyboard.resize(currentSize);
251             lastKeyboardWidth = currentSize;
252         }
253     };
254
255 })();
256
257 // Tie UI events / behavior to a specific Guacamole client
258 GuacamoleUI.attach = function(guac) {
259
260     // Mouse
261     var mouse = new Guacamole.Mouse(GuacamoleUI.display);
262     mouse.onmousedown = mouse.onmouseup = mouse.onmousemove =
263         function(mouseState) {
264        
265             // Determine mouse position within view
266             var mouse_view_x = mouseState.x + GuacamoleUI.display.offsetLeft - window.pageXOffset;
267             var mouse_view_y = mouseState.y + GuacamoleUI.display.offsetTop - window.pageYOffset;
268
269             var view_width = document.body.clientWidth;
270             var view_height = document.body.clientHeight;
271
272             // Determine scroll amounts based on mouse position relative to document
273
274             var scroll_amount_x;
275             if (mouse_view_x > view_width)
276                 scroll_amount_x = mouse_view_x - view_width;
277             else if (mouse_view_x < 0)
278                 scroll_amount_x = mouse_view_x;
279             else
280                 scroll_amount_x = 0;
281
282             var scroll_amount_y;
283             if (mouse_view_y > view_height)
284                 scroll_amount_y = mouse_view_y - view_height;
285             else if (mouse_view_y < 0)
286                 scroll_amount_y = mouse_view_y;
287             else
288                 scroll_amount_y = 0;
289        
290             // Scroll (if necessary) to keep mouse on screen.
291             window.scrollBy(scroll_amount_x, scroll_amount_y);
292        
293             // Send mouse event
294             guac.sendMouseState(mouseState);
295             
296         };
297
298     // Keyboard
299     var keyboard = new Guacamole.Keyboard(document);
300
301     function disableKeyboard() {
302         keyboard.onkeydown = null;
303         keyboard.onkeyup = null;
304     }
305
306     function enableKeyboard() {
307         keyboard.onkeydown = 
308             function (keysym) {
309                 guac.sendKeyEvent(1, keysym);
310             };
311
312         keyboard.onkeyup = 
313             function (keysym) {
314                 guac.sendKeyEvent(0, keysym);
315             };
316     }
317
318     // Enable keyboard by default
319     enableKeyboard();
320
321     // Handle client state change
322     guac.onstatechange = function(clientState) {
323         switch (clientState) {
324
325             // Idle
326             case 0:
327                 GuacamoleUI.state.textContent = "Idle."
328                 break;
329
330             // Connecting
331             case 1:
332                 GuacamoleUI.state.textContent = "Connecting...";
333                 break;
334
335             // Connected + waiting
336             case 2:
337                 GuacamoleUI.state.textContent = "Connected, waiting for first update...";
338                 break;
339
340             // Connected
341             case 3:
342                 
343                 GuacamoleUI.display.className =
344                     GuacamoleUI.display.className.replace(/guac-loading/, '');
345
346                 GuacamoleUI.menu.className = "connected";
347                 GuacamoleUI.state.textContent = "Connected.";
348                 GuacamoleUI.shadeMenu();
349                 break;
350
351             // Disconnecting
352             case 4:
353                 GuacamoleUI.state.textContent = "Disconnecting...";
354                 break;
355
356             // Disconnected
357             case 5:
358                 GuacamoleUI.state.textContent = "Disconnected.";
359                 break;
360
361             // Unknown status code
362             default:
363                 GuacamoleUI.state.textContent = "Unknown";
364
365         }
366     };
367
368     // Name instruction handler
369     guac.onname = function(name) {
370         document.title = name;
371     };
372
373     // Error handler
374     guac.onerror = function(error) {
375
376         // Disconnect, if connected
377         guac.disconnect();
378
379         // Display error message
380         GuacamoleUI.showError(error);
381
382         // Show error by desaturating display
383         var layers = guac.getLayers();
384         for (var i=0; i<layers.length; i++) {
385             layers[i].filter(desaturateFilter);
386         }
387
388         // Filter for desaturation
389         function desaturateFilter(data, width, height) {
390
391             for (var i=0; i<data.length; i+=4) {
392
393                 // Get RGB values
394                 var r = data[i];
395                 var g = data[i+1];
396                 var b = data[i+2];
397
398                 // Desaturate
399                 var v = Math.max(r, g, b) / 2;
400                 data[i]   = v;
401                 data[i+1] = v;
402                 data[i+2] = v;
403
404             }
405
406         }
407         
408     };
409
410     // Disconnect on close
411     window.onunload = function() {
412         guac.disconnect();
413     };
414
415     // Handle clipboard events
416     GuacamoleUI.clipboard.onchange = function() {
417
418         var text = GuacamoleUI.clipboard.value;
419         guac.setClipboard(text);
420
421     };
422
423     // Ignore keypresses when clipboard is focused
424     GuacamoleUI.clipboard.onfocus = function() {
425         disableKeyboard();
426     };
427
428     // Capture keypresses when clipboard is not focused
429     GuacamoleUI.clipboard.onblur = function() {
430         enableKeyboard();
431     };
432
433     // Server copy handler
434     guac.onclipboard = function(data) {
435         GuacamoleUI.clipboard.value = data;
436     };
437
438     GuacamoleUI.keyboard.onkeypress = function(keysym) {
439         guac.sendKeyEvent(1, keysym);
440     };
441
442     GuacamoleUI.keyboard.onkeyrelease = function(keysym) {
443         guac.sendKeyEvent(0, keysym);
444     };
445
446     // Send Ctrl-Alt-Delete
447     GuacamoleUI.buttons.ctrlAltDelete.onclick = function() {
448
449         var KEYSYM_CTRL   = 0xFFE3;
450         var KEYSYM_ALT    = 0xFFE9;
451         var KEYSYM_DELETE = 0xFFFF;
452
453         guac.sendKeyEvent(1, KEYSYM_CTRL);
454         guac.sendKeyEvent(1, KEYSYM_ALT);
455         guac.sendKeyEvent(1, KEYSYM_DELETE);
456         guac.sendKeyEvent(0, KEYSYM_DELETE);
457         guac.sendKeyEvent(0, KEYSYM_ALT);
458         guac.sendKeyEvent(0, KEYSYM_CTRL);
459     };
460
461 };