Store caps by modifier in keys within central key array.
[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                     // Create cap element
177                     var cap_element = document.createElement("div");
178                     cap_element.className = "guacamole-keyboard-cap";
179                     key_element.appendChild(cap_element);
180
181                     // Create key
182                     var key = new Guacamole.OnScreenKeyboard.Key(cap_element);
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                             // Get modifier value
208                             var modifierValue = 0;
209                             if (required) {
210
211                                 // Get modifier value for specified comma-delimited
212                                 // list of required modifiers.
213                                 var requirements = required.value.split(",");
214                                 for (var i=0; i<requirements.length; i++) {
215                                     modifierValue |= getModifier(requirements[i]);
216                                 }
217
218                             }
219
220                             // Store cap
221                             key.modifierMask |= modifierValue;
222                             key.caps[modifierValue] = cap;
223
224                         }
225                     });
226
227                     scaledElements.push(new ScaledElement(key_element, key_units, 1, true));
228                     row.appendChild(key_element);
229
230                     // Initialize key
231                     key.select(0);
232
233                 }
234                 
235             });
236
237             return row;
238
239         }
240
241         function parse_column(e) {
242             
243             var col = document.createElement("div");
244             col.className = "guacamole-keyboard-column";
245
246             var align = col.attributes["align"];
247
248             if (align)
249                 col.style.textAlign = align.value;
250
251             // Columns can only contain rows
252             parseChildren(e, {
253                 "row": function(e) {
254                     col.appendChild(parse_row(e));
255                 }
256             });
257
258             return col;
259
260         }
261
262
263         // Parse document
264         var keyboard_element = xml.documentElement;
265         if (keyboard_element.tagName != "keyboard")
266             throw new Error("Root element must be keyboard");
267
268         // Get attributes
269         if (!keyboard_element.attributes["size"])
270             throw new Error("size attribute is required for keyboard");
271         
272         var keyboard_size = parseFloat(keyboard_element.attributes["size"].value);
273         
274         parseChildren(keyboard_element, {
275             
276             "row": function(e) {
277                 keyboard.appendChild(parse_row(e));
278             },
279             
280             "column": function(e) {
281                 keyboard.appendChild(parse_column(e));
282             }
283             
284         });
285
286     }
287
288     // Do not allow selection or mouse movement to propagate/register.
289     keyboard.onselectstart =
290     keyboard.onmousemove   =
291     keyboard.onmouseup     =
292     keyboard.onmousedown   =
293     function(e) {
294         e.stopPropagation();
295         return false;
296     };
297
298
299     this.onkeypressed  = null;
300     this.onkeyreleased = null;
301
302     this.getElement = function() {
303         return keyboard;
304     };
305
306     this.resize = function(width) {
307
308         // Get pixel size of a unit
309         var unit = Math.floor(width / keyboard_size);
310
311         // Resize all scaled elements
312         for (var i=0; i<scaledElements.length; i++) {
313             var scaledElement = scaledElements[i];
314             scaledElement.scale(unit)
315         }
316
317     };
318
319 };
320
321 Guacamole.OnScreenKeyboard.Key = function(element) {
322
323     var key = this;
324
325     /**
326      * Width of the key, relative to the size of the keyboard.
327      */
328     this.size = 1;
329
330     /**
331      * Whether this key is currently pressed.
332      */
333     this.pressed = false;
334
335     /**
336      * An associative map of all caps by modifier.
337      */
338     this.caps = {};
339
340     /**
341      * The currently active cap as chosen by select().
342      */
343     this.currentCap = null;
344
345     /**
346      * Bit mask with all modifiers that affect this key set.
347      */
348     this.modifierMask = 0;
349
350     /**
351      * Given the bitwise OR of all active modifiers, displays the key cap
352      * which applies.
353      */
354     this.select = function(modifier) {
355
356         key.currentCap = key.caps[modifier & key.modifierMask];
357         element.textContent = key.currentCap.text;
358
359     };
360
361 }
362
363 Guacamole.OnScreenKeyboard.Cap = function(text, keysym, modifier) {
364     
365     /**
366      * Modifier represented by this keycap
367      */
368     this.modifier = 0;
369     
370     /**
371      * The text to be displayed within this keycap
372      */
373     this.text = text;
374
375     /**
376      * The keysym this cap sends when its associated key is pressed/released
377      */
378     this.keysym = keysym;
379
380     // Set modifier if provided
381     if (modifier) this.modifier = modifier;
382     
383 }