Better classname assignment for keycaps, restructure of keyboard (keyboard/key-contai...
[guacamole-common-js.git] / src / main / resources / oskeyboard.js
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 guacamole-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 // Guacamole namespace
39 var Guacamole = Guacamole || {};
40
41 /**
42  * Dynamic on-screen keyboard. Given the URL to an XML keyboard layout file,
43  * this object will download and use the XML to construct a clickable on-screen
44  * keyboard with its own key events.
45  * 
46  * @constructor
47  * @param {String} url The URL of an XML keyboard layout file.
48  */
49 Guacamole.OnScreenKeyboard = function(url) {
50
51     var scaledElements = [];
52     
53     var modifiers = {};
54     var currentModifier = 1;
55
56     // Returns a unique power-of-two value for the modifier with the
57     // given name. The same value will be returned for the same modifier.
58     function getModifier(name) {
59         
60         var value = modifiers[name];
61         if (!value) {
62
63             // Get current modifier, advance to next
64             value = currentModifier;
65             currentModifier <<= 1;
66
67             // Store value of this modifier
68             modifiers[name] = value;
69
70         }
71
72         return value;
73             
74     }
75
76     function ScaledElement(element, width, height, scaleFont) {
77
78         this.width = width;
79         this.height = height;
80
81         this.scale = function(pixels) {
82             element.style.width      = Math.floor(width  * pixels) + "px";
83             element.style.height     = Math.floor(height * pixels) + "px";
84
85             if (scaleFont) {
86                 element.style.lineHeight = Math.floor(height * pixels) + "px";
87                 element.style.fontSize   = pixels + "px";
88             }
89         }
90
91     }
92
93     // For each child of element, call handler defined in next
94     function parseChildren(element, next) {
95
96         var children = element.childNodes;
97         for (var i=0; i<children.length; i++) {
98
99             // Get child node
100             var child = children[i];
101
102             // Do not parse text nodes
103             if (!child.tagName)
104                 continue;
105
106             // Get handler for node
107             var handler = next[child.tagName];
108
109             // Call handler if defined
110             if (handler)
111                 handler(child);
112
113             // Throw exception if no handler
114             else
115                 throw new Error(
116                       "Unexpected " + child.tagName
117                     + " within " + element.tagName
118                 );
119
120         }
121
122     }
123
124     // Create keyboard
125     var keyboard = document.createElement("div");
126     keyboard.className = "guacamole-keyboard";
127
128     // Retrieve keyboard XML
129     var xmlhttprequest = new XMLHttpRequest();
130     xmlhttprequest.open("GET", url, false);
131     xmlhttprequest.send(null);
132
133     var xml = xmlhttprequest.responseXML;
134
135     if (xml) {
136
137         function parse_row(e) {
138             
139             var row = document.createElement("div");
140             row.className = "guacamole-keyboard-row";
141
142             parseChildren(e, {
143                 
144                 "column": function(e) {
145                     row.appendChild(parse_column(e));
146                 },
147                 
148                 "gap": function parse_gap(e) {
149
150                     // Get attributes
151                     var gap_size = e.attributes["size"];
152
153                     // Create element
154                     var gap = document.createElement("div");
155                     gap.className = "guacamole-keyboard-gap";
156
157                     // Set gap size
158                     var gap_units = 1;
159                     if (gap_size)
160                         gap_units = parseFloat(gap_size.value);
161
162                     scaledElements.push(new ScaledElement(gap, gap_units, gap_units));
163                     row.appendChild(gap);
164
165                 },
166                 
167                 "key": function parse_key(e) {
168                     
169                     // Get attributes
170                     var key_size = e.attributes["size"];
171
172                     // Create element
173                     var key_element = document.createElement("div");
174                     key_element.className = "guacamole-keyboard-key";
175
176                     // Position keys using container div
177                     var key_container_element = document.createElement("div");
178                     key_container_element.className = "guacamole-keyboard-key-container";
179                     key_container_element.appendChild(key_element);
180
181                     // Create key
182                     var key = new Guacamole.OnScreenKeyboard.Key();
183
184                     // Set key size
185                     var key_units = 1;
186                     if (key_size)
187                         key_units = parseFloat(key_size.value);
188
189                     key.size = key_units;
190
191                     parseChildren(e, {
192                         "cap": function parse_cap(e) {
193
194                             // Get attributes
195                             var required = e.attributes["if"];
196                             var modifier = e.attributes["modifier"];
197                             var keysym   = e.attributes["keysym"];
198                             var sticky   = e.attributes["sticky"];
199                             
200                             // Get content of key cap
201                             var content = e.textContent;
202                             
203                             // Create cap
204                             var cap = new Guacamole.OnScreenKeyboard.Cap(content,
205                                 keysym ? keysym.value : null);
206                             
207                             // Create cap element
208                             var cap_element = document.createElement("div");
209                             cap_element.className = "guacamole-keyboard-cap";
210                             cap_element.textContent = content;
211                             key_element.appendChild(cap_element);
212
213                             // Get modifier value
214                             var modifierValue = 0;
215                             if (required) {
216
217                                 // Get modifier value for specified comma-delimited
218                                 // list of required modifiers.
219                                 var requirements = required.value.split(",");
220                                 for (var i=0; i<requirements.length; i++) {
221                                     modifierValue |= getModifier(requirements[i]);
222                                     cap_element.className += " guacamole-keyboard-requires-" + requirements[i];
223                                 }
224
225                             }
226
227                             // Store cap
228                             key.modifierMask |= modifierValue;
229                             key.caps[modifierValue] = cap;
230
231                         }
232                     });
233
234                     scaledElements.push(new ScaledElement(key_container_element, key_units, 1, true));
235                     row.appendChild(key_container_element);
236
237                 }
238                 
239             });
240
241             return row;
242
243         }
244
245         function parse_column(e) {
246             
247             var col = document.createElement("div");
248             col.className = "guacamole-keyboard-column";
249
250             var align = col.attributes["align"];
251
252             if (align)
253                 col.style.textAlign = align.value;
254
255             // Columns can only contain rows
256             parseChildren(e, {
257                 "row": function(e) {
258                     col.appendChild(parse_row(e));
259                 }
260             });
261
262             return col;
263
264         }
265
266
267         // Parse document
268         var keyboard_element = xml.documentElement;
269         if (keyboard_element.tagName != "keyboard")
270             throw new Error("Root element must be keyboard");
271
272         // Get attributes
273         if (!keyboard_element.attributes["size"])
274             throw new Error("size attribute is required for keyboard");
275         
276         var keyboard_size = parseFloat(keyboard_element.attributes["size"].value);
277         
278         parseChildren(keyboard_element, {
279             
280             "row": function(e) {
281                 keyboard.appendChild(parse_row(e));
282             },
283             
284             "column": function(e) {
285                 keyboard.appendChild(parse_column(e));
286             }
287             
288         });
289
290     }
291
292     // Do not allow selection or mouse movement to propagate/register.
293     keyboard.onselectstart =
294     keyboard.onmousemove   =
295     keyboard.onmouseup     =
296     keyboard.onmousedown   =
297     function(e) {
298         e.stopPropagation();
299         return false;
300     };
301
302
303     this.onkeypressed  = null;
304     this.onkeyreleased = null;
305
306     this.getElement = function() {
307         return keyboard;
308     };
309
310     this.resize = function(width) {
311
312         // Get pixel size of a unit
313         var unit = Math.floor(width / keyboard_size);
314
315         // Resize all scaled elements
316         for (var i=0; i<scaledElements.length; i++) {
317             var scaledElement = scaledElements[i];
318             scaledElement.scale(unit)
319         }
320
321     };
322
323 };
324
325 Guacamole.OnScreenKeyboard.Key = function() {
326
327     var key = this;
328
329     /**
330      * Width of the key, relative to the size of the keyboard.
331      */
332     this.size = 1;
333
334     /**
335      * Whether this key is currently pressed.
336      */
337     this.pressed = false;
338
339     /**
340      * An associative map of all caps by modifier.
341      */
342     this.caps = {};
343
344     /**
345      * The currently active cap as chosen by select().
346      */
347     this.currentCap = null;
348
349     /**
350      * Bit mask with all modifiers that affect this key set.
351      */
352     this.modifierMask = 0;
353
354     /**
355      * Given the bitwise OR of all active modifiers, displays the key cap
356      * which applies.
357      */
358     this.select = function(modifier) {
359
360         key.currentCap = key.caps[modifier & key.modifierMask];
361
362     };
363
364 }
365
366 Guacamole.OnScreenKeyboard.Cap = function(text, keysym, modifier) {
367     
368     /**
369      * Modifier represented by this keycap
370      */
371     this.modifier = 0;
372     
373     /**
374      * The text to be displayed within this keycap
375      */
376     this.text = text;
377
378     /**
379      * The keysym this cap sends when its associated key is pressed/released
380      */
381     this.keysym = keysym;
382
383     // Set modifier if provided
384     if (modifier) this.modifier = modifier;
385     
386 }