1 
  2 /* ***** BEGIN LICENSE BLOCK *****
  3  * Version: MPL 1.1/GPL 2.0/LGPL 2.1
  4  *
  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/
  9  *
 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
 13  * License.
 14  *
 15  * The Original Code is guac-common-js.
 16  *
 17  * The Initial Developer of the Original Code is
 18  * Michael Jumper.
 19  * Portions created by the Initial Developer are Copyright (C) 2010
 20  * the Initial Developer. All Rights Reserved.
 21  *
 22  * Contributor(s):
 23  *
 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.
 35  *
 36  * ***** END LICENSE BLOCK ***** */
 37 
 38 /**
 39  * Namespace for all Guacamole JavaScript objects.
 40  * @namespace
 41  */
 42 var Guacamole = Guacamole || {};
 43 
 44 /**
 45  * Dynamic on-screen keyboard. Given the URL to an XML keyboard layout file,
 46  * this object will download and use the XML to construct a clickable on-screen
 47  * keyboard with its own key events.
 48  * 
 49  * @constructor
 50  * @param {String} url The URL of an XML keyboard layout file.
 51  */
 52 Guacamole.OnScreenKeyboard = function(url) {
 53 
 54     var on_screen_keyboard = this;
 55 
 56     /**
 57      * State of all modifiers. This is the bitwise OR of all active modifier
 58      * values.
 59      * 
 60      * @private
 61      */
 62     var modifiers = 0;
 63 
 64     var scaledElements = [];
 65     
 66     var modifier_masks = {};
 67     var next_mask = 1;
 68 
 69     /**
 70      * Adds a class to an element.
 71      * 
 72      * @private
 73      * @function
 74      * @param {Element} element The element to add a class to.
 75      * @param {String} classname The name of the class to add.
 76      */
 77     var addClass;
 78 
 79     /**
 80      * Removes a class from an element.
 81      * 
 82      * @private
 83      * @function
 84      * @param {Element} element The element to remove a class from.
 85      * @param {String} classname The name of the class to remove.
 86      */
 87     var removeClass;
 88 
 89     /**
 90      * The number of mousemove events to require before re-enabling mouse
 91      * event handling after receiving a touch event.
 92      */
 93     this.touchMouseThreshold = 3;
 94 
 95     /**
 96      * Counter of mouse events to ignore. This decremented by mousemove, and
 97      * while non-zero, mouse events will have no effect.
 98      * @private
 99      */
100     var ignore_mouse = 0;
101 
102     // Ignore all pending mouse events when touch events are the apparent source
103     function ignorePendingMouseEvents() { ignore_mouse = on_screen_keyboard.touchMouseThreshold; }
104 
105     // If Node.classList is supported, implement addClass/removeClass using that
106     if (Node.classList) {
107 
108         /** @ignore */
109         addClass = function(element, classname) {
110             element.classList.add(classname);
111         };
112         
113         /** @ignore */
114         removeClass = function(element, classname) {
115             element.classList.remove(classname);
116         };
117         
118     }
119 
120     // Otherwise, implement own
121     else {
122 
123         /** @ignore */
124         addClass = function(element, classname) {
125 
126             // Simply add new class
127             element.className += " " + classname;
128 
129         };
130         
131         /** @ignore */
132         removeClass = function(element, classname) {
133 
134             // Filter out classes with given name
135             element.className = element.className.replace(/([^ ]+)[ ]*/g,
136                 function(match, testClassname, spaces, offset, string) {
137 
138                     // If same class, remove
139                     if (testClassname == classname)
140                         return "";
141 
142                     // Otherwise, allow
143                     return match;
144                     
145                 }
146             );
147 
148         };
149         
150     }
151 
152     // Returns a unique power-of-two value for the modifier with the
153     // given name. The same value will be returned for the same modifier.
154     function getModifierMask(name) {
155         
156         var value = modifier_masks[name];
157         if (!value) {
158 
159             // Get current modifier, advance to next
160             value = next_mask;
161             next_mask <<= 1;
162 
163             // Store value of this modifier
164             modifier_masks[name] = value;
165 
166         }
167 
168         return value;
169             
170     }
171 
172     function ScaledElement(element, width, height, scaleFont) {
173 
174         this.width = width;
175         this.height = height;
176 
177         this.scale = function(pixels) {
178             element.style.width      = (width  * pixels) + "px";
179             element.style.height     = (height * pixels) + "px";
180 
181             if (scaleFont) {
182                 element.style.lineHeight = (height * pixels) + "px";
183                 element.style.fontSize   = pixels + "px";
184             }
185         }
186 
187     }
188 
189     // For each child of element, call handler defined in next
190     function parseChildren(element, next) {
191 
192         var children = element.childNodes;
193         for (var i=0; i<children.length; i++) {
194 
195             // Get child node
196             var child = children[i];
197 
198             // Do not parse text nodes
199             if (!child.tagName)
200                 continue;
201 
202             // Get handler for node
203             var handler = next[child.tagName];
204 
205             // Call handler if defined
206             if (handler)
207                 handler(child);
208 
209             // Throw exception if no handler
210             else
211                 throw new Error(
212                       "Unexpected " + child.tagName
213                     + " within " + element.tagName
214                 );
215 
216         }
217 
218     }
219 
220     // Create keyboard
221     var keyboard = document.createElement("div");
222     keyboard.className = "guac-keyboard";
223 
224     // Retrieve keyboard XML
225     var xmlhttprequest = new XMLHttpRequest();
226     xmlhttprequest.open("GET", url, false);
227     xmlhttprequest.send(null);
228 
229     var xml = xmlhttprequest.responseXML;
230 
231     if (xml) {
232 
233         function parse_row(e) {
234             
235             var row = document.createElement("div");
236             row.className = "guac-keyboard-row";
237 
238             parseChildren(e, {
239                 
240                 "column": function(e) {
241                     row.appendChild(parse_column(e));
242                 },
243                 
244                 "gap": function parse_gap(e) {
245 
246                     // Create element
247                     var gap = document.createElement("div");
248                     gap.className = "guac-keyboard-gap";
249 
250                     // Set gap size
251                     var gap_units = 1;
252                     if (e.getAttribute("size"))
253                         gap_units = parseFloat(e.getAttribute("size"));
254 
255                     scaledElements.push(new ScaledElement(gap, gap_units, gap_units));
256                     row.appendChild(gap);
257 
258                 },
259                 
260                 "key": function parse_key(e) {
261                     
262                     // Create element
263                     var key_element = document.createElement("div");
264                     key_element.className = "guac-keyboard-key";
265 
266                     // Append class if specified
267                     if (e.getAttribute("class"))
268                         key_element.className += " " + e.getAttribute("class");
269 
270                     // Position keys using container div
271                     var key_container_element = document.createElement("div");
272                     key_container_element.className = "guac-keyboard-key-container";
273                     key_container_element.appendChild(key_element);
274 
275                     // Create key
276                     var key = new Guacamole.OnScreenKeyboard.Key();
277 
278                     // Set key size
279                     var key_units = 1;
280                     if (e.getAttribute("size"))
281                         key_units = parseFloat(e.getAttribute("size"));
282 
283                     key.size = key_units;
284 
285                     parseChildren(e, {
286                         "cap": function parse_cap(e) {
287 
288                             // TODO: Handle "sticky" attribute
289                             
290                             // Get content of key cap
291                             var content = e.textContent || e.text;
292 
293                             // If read as blank, assume cap is a single space.
294                             if (content.length == 0)
295                                 content = " ";
296                             
297                             // Get keysym
298                             var real_keysym = null;
299                             if (e.getAttribute("keysym"))
300                                 real_keysym = parseInt(e.getAttribute("keysym"));
301 
302                             // If no keysym specified, try to get from key content
303                             else if (content.length == 1) {
304 
305                                 var charCode = content.charCodeAt(0);
306                                 if (charCode >= 0x0000 && charCode <= 0x00FF)
307                                     real_keysym = charCode;
308                                 else if (charCode >= 0x0100 && charCode <= 0x10FFFF)
309                                     real_keysym = 0x01000000 | charCode;
310 
311                             }
312                             
313                             // Create cap
314                             var cap = new Guacamole.OnScreenKeyboard.Cap(content, real_keysym);
315 
316                             if (e.getAttribute("modifier"))
317                                 cap.modifier = e.getAttribute("modifier");
318                             
319                             // Create cap element
320                             var cap_element = document.createElement("div");
321                             cap_element.className = "guac-keyboard-cap";
322                             cap_element.textContent = content;
323                             key_element.appendChild(cap_element);
324 
325                             // Append class if specified
326                             if (e.getAttribute("class"))
327                                 cap_element.className += " " + e.getAttribute("class");
328 
329                             // Get modifier value
330                             var modifierValue = 0;
331                             if (e.getAttribute("if")) {
332 
333                                 // Get modifier value for specified comma-delimited
334                                 // list of required modifiers.
335                                 var requirements = e.getAttribute("if").split(",");
336                                 for (var i=0; i<requirements.length; i++) {
337                                     modifierValue |= getModifierMask(requirements[i]);
338                                     addClass(cap_element, "guac-keyboard-requires-" + requirements[i]);
339                                     addClass(key_element, "guac-keyboard-uses-" + requirements[i]);
340                                 }
341 
342                             }
343 
344                             // Store cap
345                             key.modifierMask |= modifierValue;
346                             key.caps[modifierValue] = cap;
347 
348                         }
349                     });
350 
351                     scaledElements.push(new ScaledElement(key_container_element, key_units, 1, true));
352                     row.appendChild(key_container_element);
353 
354                     // Set up click handler for key
355                     function press() {
356 
357                         // Press key if not yet pressed
358                         if (!key.pressed) {
359 
360                             addClass(key_element, "guac-keyboard-pressed");
361 
362                             // Get current cap based on modifier state
363                             var cap = key.getCap(modifiers);
364 
365                             // Update modifier state
366                             if (cap.modifier) {
367 
368                                 // Construct classname for modifier
369                                 var modifierClass = "guac-keyboard-modifier-" + cap.modifier;
370                                 var modifierMask = getModifierMask(cap.modifier);
371 
372                                 // Toggle modifier state
373                                 modifiers ^= modifierMask;
374 
375                                 // Activate modifier if pressed
376                                 if (modifiers & modifierMask) {
377                                     
378                                     addClass(keyboard, modifierClass);
379                                     
380                                     // Send key event
381                                     if (on_screen_keyboard.onkeydown && cap.keysym)
382                                         on_screen_keyboard.onkeydown(cap.keysym);
383 
384                                 }
385 
386                                 // Deactivate if not pressed
387                                 else {
388 
389                                     removeClass(keyboard, modifierClass);
390                                     
391                                     // Send key event
392                                     if (on_screen_keyboard.onkeyup && cap.keysym)
393                                         on_screen_keyboard.onkeyup(cap.keysym);
394 
395                                 }
396 
397                             }
398 
399                             // If not modifier, send key event now
400                             else if (on_screen_keyboard.onkeydown && cap.keysym)
401                                 on_screen_keyboard.onkeydown(cap.keysym);
402 
403                             // Mark key as pressed
404                             key.pressed = true;
405 
406                         }
407 
408                     }
409 
410                     function release() {
411 
412                         // Release key if currently pressed
413                         if (key.pressed) {
414 
415                             // Get current cap based on modifier state
416                             var cap = key.getCap(modifiers);
417 
418                             removeClass(key_element, "guac-keyboard-pressed");
419 
420                             // Send key event if not a modifier key
421                             if (!cap.modifier && on_screen_keyboard.onkeyup && cap.keysym)
422                                 on_screen_keyboard.onkeyup(cap.keysym);
423 
424                             // Mark key as released
425                             key.pressed = false;
426 
427                         }
428 
429                     }
430 
431                     function touchPress(e) {
432                         e.preventDefault();
433                         ignore_mouse = on_screen_keyboard.touchMouseThreshold;
434                         press();
435                     }
436 
437                     function touchRelease(e) {
438                         e.preventDefault();
439                         ignore_mouse = on_screen_keyboard.touchMouseThreshold;
440                         release();
441                     }
442 
443                     function mousePress(e) {
444                         e.preventDefault();
445                         if (ignore_mouse == 0)
446                             press();
447                     }
448 
449                     function mouseRelease(e) {
450                         e.preventDefault();
451                         if (ignore_mouse == 0)
452                             release();
453                     }
454 
455                     key_element.addEventListener("touchstart", touchPress, true);
456                     key_element.addEventListener("touchend",   touchRelease, true);
457 
458                     key_element.addEventListener("mousedown", mousePress,   true);
459                     key_element.addEventListener("mouseup",   mouseRelease, true);
460                     key_element.addEventListener("mouseout",  mouseRelease, true);
461 
462                 }
463                 
464             });
465 
466             return row;
467 
468         }
469 
470         function parse_column(e) {
471             
472             var col = document.createElement("div");
473             col.className = "guac-keyboard-column";
474 
475             if (col.getAttribute("align"))
476                 col.style.textAlign = col.getAttribute("align");
477 
478             // Columns can only contain rows
479             parseChildren(e, {
480                 "row": function(e) {
481                     col.appendChild(parse_row(e));
482                 }
483             });
484 
485             return col;
486 
487         }
488 
489 
490         // Parse document
491         var keyboard_element = xml.documentElement;
492         if (keyboard_element.tagName != "keyboard")
493             throw new Error("Root element must be keyboard");
494 
495         // Get attributes
496         if (!keyboard_element.getAttribute("size"))
497             throw new Error("size attribute is required for keyboard");
498         
499         var keyboard_size = parseFloat(keyboard_element.getAttribute("size"));
500         
501         parseChildren(keyboard_element, {
502             
503             "row": function(e) {
504                 keyboard.appendChild(parse_row(e));
505             },
506             
507             "column": function(e) {
508                 keyboard.appendChild(parse_column(e));
509             }
510             
511         });
512 
513     }
514 
515     // Do not allow selection or mouse movement to propagate/register.
516     keyboard.onselectstart =
517     keyboard.onmousemove   =
518     keyboard.onmouseup     =
519     keyboard.onmousedown   =
520     function(e) {
521 
522         // If ignoring events, decrement counter
523         if (ignore_mouse)
524             ignore_mouse--;
525 
526         e.stopPropagation();
527         return false;
528 
529     };
530 
531     /**
532      * Fired whenever the user presses a key on this Guacamole.OnScreenKeyboard.
533      * 
534      * @event
535      * @param {Number} keysym The keysym of the key being pressed.
536      */
537     this.onkeydown = null;
538 
539     /**
540      * Fired whenever the user releases a key on this Guacamole.OnScreenKeyboard.
541      * 
542      * @event
543      * @param {Number} keysym The keysym of the key being released.
544      */
545     this.onkeyup   = null;
546 
547     /**
548      * Returns the element containing the entire on-screen keyboard.
549      * @returns {Element} The element containing the entire on-screen keyboard.
550      */
551     this.getElement = function() {
552         return keyboard;
553     };
554 
555     /**
556      * Resizes all elements within this Guacamole.OnScreenKeyboard such that
557      * the width is close to but does not exceed the specified width. The
558      * height of the keyboard is determined based on the width.
559      * 
560      * @param {Number} width The width to resize this Guacamole.OnScreenKeyboard
561      *                       to, in pixels.
562      */
563     this.resize = function(width) {
564 
565         // Get pixel size of a unit
566         var unit = Math.floor(width * 10 / keyboard_size) / 10;
567 
568         // Resize all scaled elements
569         for (var i=0; i<scaledElements.length; i++) {
570             var scaledElement = scaledElements[i];
571             scaledElement.scale(unit)
572         }
573 
574     };
575 
576 };
577 
578 
579 /**
580  * Basic representation of a single key of a keyboard. Each key has a set of
581  * caps associated with tuples of modifiers. The cap determins what happens
582  * when a key is pressed, while it is the state of modifier keys that determines
583  * what cap is in effect on any particular key.
584  * 
585  * @constructor
586  */
587 Guacamole.OnScreenKeyboard.Key = function() {
588 
589     var key = this;
590 
591     /**
592      * Whether this key is currently pressed.
593      */
594     this.pressed = false;
595 
596     /**
597      * Width of the key, relative to the size of the keyboard.
598      */
599     this.size = 1;
600 
601     /**
602      * An associative map of all caps by modifier.
603      */
604     this.caps = {};
605 
606     /**
607      * Bit mask with all modifiers that affect this key set.
608      */
609     this.modifierMask = 0;
610 
611     /**
612      * Given the bitwise OR of all active modifiers, returns the key cap
613      * which applies.
614      */
615     this.getCap = function(modifier) {
616         return key.caps[modifier & key.modifierMask];
617     };
618 
619 };
620 
621 /**
622  * Basic representation of a cap of a key. The cap is the visible part of a key
623  * and determines the active behavior of a key when pressed. The state of all
624  * modifiers on the keyboard determines the active cap for all keys, thus
625  * each cap is associated with a set of modifiers.
626  * 
627  * @constructor
628  * @param {String} text The text to be displayed within this cap.
629  * @param {Number} keysym The keysym this cap sends when its associated key is
630  *                        pressed or released.
631  * @param {String} modifier The modifier represented by this cap.
632  */
633 Guacamole.OnScreenKeyboard.Cap = function(text, keysym, modifier) {
634     
635     /**
636      * Modifier represented by this keycap
637      */
638     this.modifier = null;
639     
640     /**
641      * The text to be displayed within this keycap
642      */
643     this.text = text;
644 
645     /**
646      * The keysym this cap sends when its associated key is pressed/released
647      */
648     this.keysym = keysym;
649 
650     // Set modifier if provided
651     if (modifier) this.modifier = modifier;
652     
653 };
654