fd8134191438bd35746ad15e11dac9ad5ee46b6e
[guacamole-common-js.git] / src / main / resources / keyboard.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  * Provides cross-browser and cross-keyboard keyboard for a specific element.
43  * Browser and keyboard layout variation is abstracted away, providing events
44  * which represent keys as their corresponding X11 keysym.
45  * 
46  * @constructor
47  * @param {Element} element The Element to use to provide keyboard events.
48  */
49 Guacamole.Keyboard = function(element) {
50
51     /**
52      * Reference to this Guacamole.Keyboard.
53      * @private
54      */
55     var guac_keyboard = this;
56
57     /**
58      * Fired whenever the user presses a key with the element associated
59      * with this Guacamole.Keyboard in focus.
60      * 
61      * @event
62      * @param {Number} keysym The keysym of the key being pressed.
63      * @returns {Boolean} true if the originating event of this keypress should
64      *                    be allowed through to the browser, false or undefined
65      *                    otherwise.
66      */
67     this.onkeydown = null;
68
69     /**
70      * Fired whenever the user releases a key with the element associated
71      * with this Guacamole.Keyboard in focus.
72      * 
73      * @event
74      * @param {Number} keysym The keysym of the key being released.
75      * @returns {Boolean} true if the originating event of this key release 
76      *                    should be allowed through to the browser, false or
77      *                    undefined otherwise.
78      */
79     this.onkeyup = null;
80
81     /**
82      * Map of known JavaScript keycodes which do not map to typable characters
83      * to their unshifted X11 keysym equivalents.
84      * @private
85      */
86     var unshiftedKeySym = {
87         8:   0xFF08, // backspace
88         9:   0xFF09, // tab
89         13:  0xFF0D, // enter
90         16:  0xFFE1, // shift
91         17:  0xFFE3, // ctrl
92         18:  0xFFE9, // alt
93         19:  0xFF13, // pause/break
94         20:  0xFFE5, // caps lock
95         27:  0xFF1B, // escape
96         33:  0xFF55, // page up
97         34:  0xFF56, // page down
98         35:  0xFF57, // end
99         36:  0xFF50, // home
100         37:  0xFF51, // left arrow
101         38:  0xFF52, // up arrow
102         39:  0xFF53, // right arrow
103         40:  0xFF54, // down arrow
104         45:  0xFF63, // insert
105         46:  0xFFFF, // delete
106         91:  0xFFEB, // left window key (super_l)
107         92:  0xFF67, // right window key (menu key?)
108         93:  null,   // select key
109         112: 0xFFBE, // f1
110         113: 0xFFBF, // f2
111         114: 0xFFC0, // f3
112         115: 0xFFC1, // f4
113         116: 0xFFC2, // f5
114         117: 0xFFC3, // f6
115         118: 0xFFC4, // f7
116         119: 0xFFC5, // f8
117         120: 0xFFC6, // f9
118         121: 0xFFC7, // f10
119         122: 0xFFC8, // f11
120         123: 0xFFC9, // f12
121         144: 0xFF7F, // num lock
122         145: 0xFF14  // scroll lock
123     };
124
125     /**
126      * Map of known JavaScript keycodes which do not map to typable characters
127      * to their shifted X11 keysym equivalents. Keycodes must only be listed
128      * here if their shifted X11 keysym equivalents differ from their unshifted
129      * equivalents.
130      * @private
131      */
132     var shiftedKeySym = {
133         18:  0xFFE7  // alt
134     };
135
136     /**
137      * All modifiers and their states.
138      */
139     this.modifiers = {
140         
141         /**
142          * Whether shift is currently pressed.
143          */
144         "shift": false,
145         
146         /**
147          * Whether ctrl is currently pressed.
148          */
149         "ctrl" : false,
150         
151         /**
152          * Whether alt is currently pressed.
153          */
154         "alt"  : false
155
156     };
157
158     /**
159      * The state of every key, indexed by keysym. If a particular key is
160      * pressed, the value of pressed for that keysym will be true. If a key
161      * is not currently pressed, the value for that keysym may be false or
162      * undefined.
163      */
164     this.pressed = [];
165
166     var keydownChar = new Array();
167
168     // ID of routine repeating keystrokes. -1 = not repeating.
169     var repeatKeyTimeoutId = -1;
170     var repeatKeyIntervalId = -1;
171
172     // Starts repeating keystrokes
173     function startRepeat(keySym) {
174         repeatKeyIntervalId = setInterval(function() {
175             sendKeyReleased(keySym);
176             sendKeyPressed(keySym);
177         }, 50);
178     }
179
180     // Stops repeating keystrokes
181     function stopRepeat() {
182         if (repeatKeyTimeoutId != -1) clearInterval(repeatKeyTimeoutId);
183         if (repeatKeyIntervalId != -1) clearInterval(repeatKeyIntervalId);
184     }
185
186
187     function getKeySymFromKeyIdentifier(shifted, keyIdentifier) {
188
189         var unicodePrefixLocation = keyIdentifier.indexOf("U+");
190         if (unicodePrefixLocation >= 0) {
191
192             var hex = keyIdentifier.substring(unicodePrefixLocation+2);
193             var codepoint = parseInt(hex, 16);
194             var typedCharacter;
195
196             // Convert case if shifted
197             if (shifted == 0)
198                 typedCharacter = String.fromCharCode(codepoint).toLowerCase();
199             else
200                 typedCharacter = String.fromCharCode(codepoint).toUpperCase();
201
202             // Get codepoint
203             codepoint = typedCharacter.charCodeAt(0);
204
205             return getKeySymFromCharCode(codepoint);
206
207         }
208
209         return null;
210
211     }
212
213     function getKeySymFromCharCode(keyCode) {
214
215         if (keyCode >= 0x0000 && keyCode <= 0x00FF)
216             return keyCode;
217
218         if (keyCode >= 0x0100 && keyCode <= 0x10FFFF)
219             return 0x01000000 | keyCode;
220
221         return null;
222
223     }
224
225     function getKeySymFromKeyCode(keyCode) {
226
227         var keysym = null;
228         if (!guac_keyboard.modifiers.shift) keysym = unshiftedKeySym[keyCode];
229         else {
230             keysym = shiftedKeySym[keyCode];
231             if (keysym == null) keysym = unshiftedKeySym[keyCode];
232         }
233
234         return keysym;
235
236     }
237
238
239     // Sends a single keystroke over the network
240     function sendKeyPressed(keysym) {
241
242         // Mark key as pressed
243         guac_keyboard.pressed[keysym] = true;
244
245         // Send key event
246         if (keysym != null && guac_keyboard.onkeydown)
247             return guac_keyboard.onkeydown(keysym) != false;
248         
249         return true;
250
251     }
252
253     // Sends a single keystroke over the network
254     function sendKeyReleased(keysym) {
255
256         // Mark key as released
257         guac_keyboard.pressed[keysym] = false;
258
259         // Send key event
260         if (keysym != null && guac_keyboard.onkeyup)
261             return guac_keyboard.onkeyup(keysym) != false;
262
263         return true;
264
265     }
266
267
268     var KEYDOWN = 1;
269     var KEYPRESS = 2;
270
271     var keySymSource = null;
272
273     // When key pressed
274     var keydownCode = null;
275     element.onkeydown = function(e) {
276
277         // Only intercept if handler set
278         if (!guac_keyboard.onkeydown) return true;
279
280         var keynum;
281         if (window.event) keynum = window.event.keyCode;
282         else if (e.which) keynum = e.which;
283
284         // Ctrl/Alt/Shift
285         if (keynum == 16)      guac_keyboard.modifiers.shift = true;
286         else if (keynum == 17) guac_keyboard.modifiers.ctrl  = true;
287         else if (keynum == 18) guac_keyboard.modifiers.alt   = true;
288
289         // If keysym is defined for given key code, key events can come from
290         // KEYDOWN.
291         var keysym = getKeySymFromKeyCode(keynum);
292         if (keysym)
293             keySymSource = KEYDOWN;
294
295         // Otherwise, if modifier keys are held down, try to get from keyIdentifier
296         else if ((guac_keyboard.modifiers.ctrl || guac_keyboard.modifiers.alt) && e.keyIdentifier) {
297
298             // Get keysym from keyIdentifier
299             keysym = getKeySymFromKeyIdentifier(guac_keyboard.modifiers.shift, e.keyIdentifier);
300
301             // Get keysyms and events from KEYDOWN
302             keySymSource = KEYDOWN;
303
304         }
305
306         // Otherwise, resort to KEYPRESS
307         else
308             keySymSource = KEYPRESS;
309
310         keydownCode = keynum;
311
312         // Ignore key if we don't need to use KEYPRESS.
313         // Send key event here
314         if (keySymSource == KEYDOWN) {
315
316             if (keydownChar[keynum] != keysym) {
317
318                 // Send event
319                 keydownChar[keynum] = keysym;
320                 var returnValue = sendKeyPressed(keysym);
321
322                 // Clear old key repeat, if any.
323                 stopRepeat();
324
325                 // Start repeating (if not a modifier key) after a short delay
326                 if (keynum != 16 && keynum != 17 && keynum != 18)
327                     repeatKeyTimeoutId = setTimeout(function() { startRepeat(keysym); }, 500);
328
329                 // Use return code provided by handler
330                 return returnValue;
331
332             }
333
334             // Default to canceling event if no keypress is being sent, but
335             // source of events is keydown.
336             return false;
337
338         }
339
340         return true;
341
342     };
343
344     // When key pressed
345     element.onkeypress = function(e) {
346
347         // Only intercept if handler set
348         if (!guac_keyboard.onkeydown) return true;
349
350         if (keySymSource != KEYPRESS) return false;
351
352         var keynum;
353         if (window.event) keynum = window.event.keyCode;
354         else if (e.which) keynum = e.which;
355
356         var keysym = getKeySymFromCharCode(keynum);
357         if (keysym && keydownChar[keynum] != keysym) {
358
359             // If this button already pressed, release first
360             var lastKeyDownChar = keydownChar[keydownCode];
361             if (lastKeyDownChar)
362                 sendKeyReleased(lastKeyDownChar);
363
364             keydownChar[keydownCode] = keysym;
365
366             // Clear old key repeat, if any.
367             stopRepeat();
368
369             // Send key event
370             var returnValue = sendKeyPressed(keysym);
371
372             // Start repeating (if not a modifier key) after a short delay
373             repeatKeyTimeoutId = setTimeout(function() { startRepeat(keysym); }, 500);
374
375             return returnValue;
376         }
377
378         // Default to canceling event if no keypress is being sent, but
379         // source of events is keypress.
380         return false;
381
382     };
383
384     // When key released
385     element.onkeyup = function(e) {
386
387         // Only intercept if handler set
388         if (!guac_keyboard.onkeyup) return true;
389
390         var keynum;
391         if (window.event) keynum = window.event.keyCode;
392         else if (e.which) keynum = e.which;
393         
394         // Ctrl/Alt/Shift
395         if (keynum == 16)      guac_keyboard.modifiers.shift = false;
396         else if (keynum == 17) guac_keyboard.modifiers.ctrl  = false;
397         else if (keynum == 18) guac_keyboard.modifiers.alt   = false;
398         else
399             stopRepeat();
400
401         // Get corresponding character
402         var lastKeyDownChar = keydownChar[keynum];
403
404         // Clear character record
405         keydownChar[keynum] = null;
406
407         // Send release event
408         return sendKeyReleased(lastKeyDownChar);
409
410     };
411
412     // When focus is lost, clear modifiers.
413     element.onblur = function() {
414         guac_keyboard.modifiers.alt = false;
415         guac_keyboard.modifiers.ctrl = false;
416         guac_keyboard.modifiers.shift = false;
417     };
418
419 };