input_handle_command: allow event to be a string
[conkeror/arlinius.git] / modules / keymap.js
blobde54c9c3397a7495d8a15d4666c9f80d1cc208f4
1 /**
2  * (C) Copyright 2004-2007 Shawn Betts
3  * (C) Copyright 2007-2009 John J. Foerch
4  * (C) Copyright 2007-2008 Jeremy Maitin-Shepard
5  *
6  * Use, modification, and distribution are subject to the terms specified in the
7  * COPYING file.
8 **/
10 /* Generate vk name table  */
11 var keycode_to_vk_name = [];
12 var vk_name_to_keycode = {};
13 let (KeyEvent = Ci.nsIDOMKeyEvent,
14      prefix = "DOM_VK_") {
15     for (var i in KeyEvent) {
16         /* Check if this is a key binding */
17         if (i.substr(0, prefix.length) == prefix) {
18             let name = i.substr(prefix.length).toLowerCase();
19             let code = KeyEvent[i];
20             keycode_to_vk_name[code] = name;
21             vk_name_to_keycode[name] = code;
22         }
23     }
28  * Modifiers
29  */
31 function modifier (in_event_p, set_in_event) {
32     this.in_event_p = in_event_p;
33     this.set_in_event = set_in_event;
36 var modifiers = {
37     A: new modifier(function (event) { return event.altKey; },
38                     function (event) { event.altKey = true; }),
39     C: new modifier(function (event) { return event.ctrlKey; },
40                     function (event) { event.ctrlKey = true; }),
41     M: new modifier(function (event) { return event.metaKey; },
42                     function (event) { event.metaKey = true; }),
43     S: new modifier(function (event) {
44                         if (event.shiftKey) {
45                             var name;
46                             if (event.keyCode)
47                                 name = keycode_to_vk_name[event.keyCode];
48                             return ((name && name.length > 1) ||
49                                     (event.charCode == 32) ||
50                                     (event.button != null));
51                         }
52                         return false;
53                     },
54                     function (event) { event.shiftKey = true; })
56 var modifier_order = ['C', 'M', 'S'];
58 // check the platform and guess whether we should treat Alt as Meta
59 if (get_os() == 'Darwin') {
60     // In OS X, alt is a shift-like modifier, in that we
61     // only care about it for non-character events.
62     modifiers.A = new modifier(
63         function (event) {
64             if (event.altKey) {
65                 var name;
66                 if (event.keyCode)
67                     name = keycode_to_vk_name[event.keyCode];
68                 return ((name && name.length > 1) ||
69                         (event.charCode == 32) ||
70                         (event.button != null));
71             }
72             return false;
73         },
74         function (event) { event.altKey = true; });
75     modifier_order = ['C', 'M', 'A', 'S'];
76 } else {
77     modifiers.M = modifiers.A;
83  * Combos
84  */
86 function format_key_combo (event) {
87     var combo = '';
88     for each (var M in modifier_order) {
89         if (modifiers[M].in_event_p(event) ||
90             (event.sticky_modifiers &&
91              event.sticky_modifiers.indexOf(M) != -1))
92         {
93             combo += (M + '-');
94         }
95     }
96     if (event.charCode) {
97         if (event.charCode == 32)
98             combo += 'space';
99         else
100             combo += String.fromCharCode(event.charCode);
101     } else if (event.keyCode) {
102         combo += keycode_to_vk_name[event.keyCode];
103     } else if (event.button != null) {
104         combo += "mouse" + (event.button + 1);
105     }
106     return combo;
109 function unformat_key_combo (combo) {
110     var event = {
111         keyCode: 0,
112         charCode: 0,
113         altKey: false,
114         ctrlKey: false,
115         metaKey: false,
116         shiftKey: false
117     };
118     var M;
119     var i = 0;
120     var len = combo.length - 2;
121     var res;
122     while (i < len && combo[i+1] == '-') {
123         M = combo[i];
124         modifiers[M].set_in_event(event);
125         i+=2;
126     }
127     var key = combo.substring(i);
128     if (key.length == 1)
129         event.charCode = key.charCodeAt(0);
130     else if (key == 'space')
131         event.charCode = 32;
132     else if (vk_name_to_keycode[key])
133         event.keyCode = vk_name_to_keycode[key];
134     else if (key.substring(0, 5) == 'mouse')
135         event.button = parseInt(key.substring(5));
136     return event;
142  * Keymap datatype
143  */
145 define_keywords("$parent", "$help", "$name", "$anonymous");
146 function keymap () {
147     keywords(arguments);
148     this.parent = arguments.$parent;
149     this.bindings = {};
150     this.predicate_bindings = [];
151     this.fallthrough = [];
152     this.help = arguments.$help;
153     this.name = arguments.$name;
154     this.anonymous = arguments.$anonymous;
157 function define_keymap (name) {
158     keywords(arguments);
159     this[name] = new keymap($name = name, forward_keywords(arguments));
165  * Key Match Predicates.
166  */
168 function define_key_match_predicate (name, description, predicate) {
169     conkeror[name] = predicate;
170     conkeror[name].name = name;
171     conkeror[name].description = description;
172     return conkeror[name];
175 define_key_match_predicate('match_any_key', 'any key',
176     function (event) true);
178 // should be renamed to match_any_unmodified_character
179 define_key_match_predicate('match_any_unmodified_character', 'any unmodified character',
180     function (event) {
181         // this predicate can be used for both keypress and keydown
182         // fallthroughs.
183         try {
184             return ((event.type == 'keypress' && event.charCode)
185                     || event.keyCode > 31)
186                 && !modifiers.A.in_event_p(event)
187                 && !event.metaKey
188                 && !event.ctrlKey
189                 && !event.sticky_modifiers;
190         } catch (e) { return false; }
191     });
193 define_key_match_predicate('match_checkbox_keys', 'checkbox keys',
194     function (event) {
195         return event.keyCode == 32
196             && !event.shiftKey
197             && !event.metaKey
198             && !event.altKey
199             && !event.ctrlKey;
200         //XXX: keycode fallthroughs don't support sticky modifiers
201     });
203 define_key_match_predicate('match_text_keys', 'text editing keys',
204     function (event) {
205         return ((event.type == 'keypress' && event.charCode)
206                 || event.keyCode == 13 || event.keyCode > 31)
207             && !event.ctrlKey
208             && !event.metaKey
209             && !modifiers.A.in_event_p(event);
210         //XXX: keycode fallthroughs don't support sticky modifiers
211     });
215  */
217 function format_key_spec (key) {
218     if (key instanceof Function) {
219         if (key.description)
220             return "<"+key.description+">";
221         if (key.name)
222             return "<"+key.name+">";
223         return "<anonymous match function>";
224     }
225     return key;
228 function format_binding_sequence (seq) {
229     return seq.map(function (x) {
230             return format_key_spec(x.key);
231         }).join(" ");
235 function keymap_lookup (keymaps, combo, event) {
236     var i = keymaps.length - 1;
237     var kmap = keymaps[i];
238     var new_kmaps;
239     while (true) {
240         var bindings = kmap.bindings;
241         var bind = bindings[combo];
242         if (new_kmaps) {
243             if (bind) {
244                 if (bind.keymap)
245                     new_kmaps.unshift(bind.keymap);
246                 else
247                     return new_kmaps;
248             }
249         } else if (bind) {
250             if (bind.keymap)
251                 new_kmaps = [bind.keymap];
252             else
253                 return bind;
254         } else {
255             var pred_binds = kmap.predicate_bindings;
256             for (var j = 0, pblen = pred_binds.length; j < pblen; ++j) {
257                 bind = pred_binds[j];
258                 if (bind.key(event))
259                     return bind;
260             }
261         }
262         if (kmap.parent)
263             kmap = kmap.parent;
264         else if (i > 0)
265             kmap = keymaps[--i];
266         else if (new_kmaps)
267             return new_kmaps
268         else
269             return null;
270     }
274 function keymap_lookup_fallthrough (keymap, event) {
275     var predicates = keymap.fallthrough;
276     for (var i = 0, plen = predicates.length; i < plen; ++i) {
277         if (predicates[i](event))
278             return true;
279     }
280     return false;
284 function for_each_key_binding (keymaps, callback) {
285     var binding_sequence = [];
286     function helper (keymap) {
287         var binding;
288         for each (binding in keymap.bindings) {
289             binding_sequence.push(binding);
290             callback(binding_sequence);
291             if (binding.keymap)
292                 helper(binding.keymap);
293             binding_sequence.pop();
294         }
295         for each (binding in keymap.predicate_bindings) {
296             binding_sequence.push(binding);
297             callback(binding_sequence);
298             if (binding.keymap)
299                 helper(binding.keymap);
300             binding_sequence.pop();
301         }
302     }
303     //outer loop is to go down the parent-chain of keymaps
304     var i = keymaps.length - 1;
305     var keymap = keymaps[i];
306     while (true) {
307         helper(keymap);
308         if (keymap.parent)
309             keymap = keymap.parent;
310         else if (i > 0)
311             keymap = keymaps[--i];
312         else
313             break;
314     }
318 function keymap_lookup_command (keymaps, command) {
319     var list = [];
320     for_each_key_binding(keymaps, function (bind_seq) {
321             var bind = bind_seq[bind_seq.length - 1];
322             if (bind.command == command)
323                 list.push(format_binding_sequence(bind_seq));
324         });
325     return list;
334  * $fallthrough, $repeat and $browser_object are as for define_key.
336  * ref is the source code reference of the call to define_key.
338  * kmap is the keymap in which the binding is to be defined.
340  * seq is the key sequence being bound.  it may be necessary
341  * to auto-generate new keymaps to accomodate the key sequence.
343  * only one of new_command and new_keymap will be given.
344  * the one that is given is the thing being bound to.
345  */
346 define_keywords("$fallthrough", "$repeat", "$browser_object");
347 function define_key_internal (ref, kmap, seq, new_command, new_keymap) {
348     keywords(arguments);
349     var args = arguments;
350     var last_in_sequence; // flag to indicate the final key combo in the sequence.
351     var key; // current key combo as we iterate through the sequence.
352     var undefine_key = (new_command == null) &&
353         (new_keymap == null) &&
354         (! args.$fallthrough);
356     /* Replace `bind' with the binding specified by (cmd, fallthrough) */
357     function replace_binding (bind) {
358         if (last_in_sequence) {
359             bind.command = new_command;
360             bind.keymap = new_keymap;
361             bind.fallthrough = args.$fallthrough;
362             bind.source_code_reference = ref;
363             bind.repeat = args.$repeat;
364             bind.browser_object = args.$browser_object;
365         } else {
366             if (!bind.keymap)
367                 throw new Error("Key sequence has a non-keymap in prefix");
368             kmap = bind.keymap;
369         }
370     }
372     function make_binding () {
373         if (last_in_sequence) {
374             return { key: key,
375                      fallthrough: args.$fallthrough,
376                      command: new_command,
377                      keymap: new_keymap,
378                      source_code_reference: ref,
379                      repeat: args.$repeat,
380                      browser_object: args.$browser_object,
381                      bound_in: kmap };
382         } else {
383             let old_kmap = kmap;
384             kmap = new keymap($anonymous,
385                               $name = old_kmap.name + " " + format_key_spec(key));
386             kmap.bound_in = old_kmap;
387             return { key: key,
388                      keymap: kmap,
389                      source_code_reference: ref,
390                      bound_in: old_kmap };
391         }
392     }
394 sequence:
395     for (var i = 0, slen = seq.length; i < slen; ++i) {
396         key = seq[i];
397         last_in_sequence = (i == slen - 1);
399         if (typeof key == "function") { // it's a match predicate
400             // Check if the binding is already present in the keymap
401             var pred_binds = kmap.predicate_bindings;
402             for (var j = 0, pblen = pred_binds.length; j < pblen; j++) {
403                 if (pred_binds[j].key == key) {
404                     if (last_in_sequence && undefine_key)
405                         delete pred_binds[j];
406                     else
407                         replace_binding(pred_binds[j]);
408                     continue sequence;
409                 }
410             }
411             pred_binds.push(make_binding());
412         } else {
413             // Check if the binding is already present in the keymap
414             var bindings = kmap.bindings;
415             var binding = bindings[key];
416             if (binding) {
417                 if (last_in_sequence && undefine_key)
418                     delete bindings[key];
419                 else
420                     replace_binding(binding);
421                 continue sequence;
422             }
423             if (! undefine_key)
424                 bindings[key] = make_binding();
425         }
426     }
430  * bind SEQ to a keymap or command CMD in keymap KMAP.
432  *   If CMD is the special value `fallthrough', it will be bound as a
433  * fallthrough key.
435  * keywords:
437  *  $fallthrough: specifies that the keypress event will fall through
438  *      to gecko.  Note, by this method, only keypress will fall through.
439  *      Keyup and keydown will still be blocked.
441  *  $repeat: (command name) shortcut command to call if a prefix
442  *      command key is pressed twice in a row.
444  *  $browser_object: (browser object) Override the default
445  *      browser-object for the command.
446  */
447 define_keywords("$fallthrough", "$repeat", "$browser_object");
448 function define_key (kmap, seq, cmd) {
449     keywords(arguments);
450     var orig_seq = seq;
451     try {
452         var ref = get_caller_source_code_reference();
454         if (typeof seq == "string" && seq.length > 1)
455             seq = seq.split(" ");
457         if (!(typeof seq == "object") || !(seq instanceof Array))
458             seq = [seq];
460         // normalize the order of modifiers in key combos
461         seq = seq.map(
462             function (k) {
463                 if (typeof(k) == "string")
464                     return format_key_combo(unformat_key_combo(k));
465                 else
466                     return k;
467             });
469         var new_command = null;
470         var new_keymap = null;
471         if (typeof cmd == "string" || typeof cmd == "function")
472             new_command = cmd;
473         else if (cmd instanceof keymap)
474             new_keymap = cmd;
475         else if (cmd != null)
476             throw new Error("Invalid `cmd' argument: " + cmd);
478         define_key_internal(ref, kmap, seq, new_command, new_keymap,
479                             forward_keywords(arguments));
481     } catch (e if (typeof e == "string")) {
482         dumpln("Warning: Error occurred while binding sequence: " + orig_seq);
483         dumpln(e);
484     }
488 function undefine_key (kmap, seq) {
489     define_key(kmap, seq);
494  * define_fallthrough
496  *   Takes a keymap and a predicate on an event.  Fallthroughs defined by
497  * these means will cause all three of keydown, keypress, and keyup to
498  * fall through to gecko, whereas those defined by the $fallthrough
499  * keyword to define_key only affect keypress events.
501  *   The limitations of this method are that only the keyCode is available
502  * to the predicate, not the charCode, and keymap inheritance is not
503  * available for these "bindings".
504  */
505 function define_fallthrough (keymap, predicate) {
506     keymap.fallthrough.push(predicate);
511  * Minibuffer Read Key Binding
512  */
514 define_keymap("key_binding_reader_keymap");
515 define_key(key_binding_reader_keymap, match_any_key, "read-key-binding-key");
517 define_keywords("$buffer", "$keymap");
518 function key_binding_reader (window, continuation) {
519     keywords(arguments, $prompt = "Describe key:");
521     this.continuation = continuation;
523     if (arguments.$keymap)
524         this.target_keymap = arguments.$keymap;
525     else {
526         var buffer = arguments.$buffer;
527         this.target_keymap = get_current_keymaps(window);
528     }
530     this.key_sequence = [];
532     minibuffer_input_state.call(this, window, key_binding_reader_keymap, arguments.$prompt);
534 key_binding_reader.prototype = {
535     __proto__: minibuffer_input_state.prototype,
536     destroy: function (window) {
537         if (this.continuation)
538             this.continuation.throw(abort());
539         minibuffer_input_state.prototype.destroy.call(this, window);
540     }
543 function invalid_key_binding (seq) {
544     var e = new Error(seq.join(" ") + " is undefined");
545     e.key_sequence = seq;
546     e.__proto__ = invalid_key_binding.prototype;
547     return e;
549 invalid_key_binding.prototype = {
550     __proto__: interactive_error.prototype
553 function read_key_binding_key (window, state, event) {
554     var combo = format_key_combo(event);
555     var binding = keymap_lookup(state.target_keymap, combo, event);
557     state.key_sequence.push(combo);
559     if (binding == null) {
560         var c = state.continuation;
561         delete state.continuation;
562         window.minibuffer.pop_state();
563         c.throw(invalid_key_binding(state.key_sequence));
564         return;
565     }
567     if (binding.constructor == Array) { //keymaps stack
568         window.minibuffer._restore_normal_state();
569         window.minibuffer._input_text = state.key_sequence.join(" ") + " ";
570         state.target_keymap = binding;
571         return;
572     }
574     var c = state.continuation;
575     delete state.continuation;
577     window.minibuffer.pop_state();
579     if (c != null)
580         c([state.key_sequence, binding]);
582 interactive("read-key-binding-key",
583     "Handle a keystroke in the key binding reader minibuffer state.",
584      function (I) {
585          read_key_binding_key(I.window,
586                               I.minibuffer.check_state(key_binding_reader),
587                               I.event);
588      });
590 minibuffer.prototype.read_key_binding = function () {
591     keywords(arguments);
592     var s = new key_binding_reader(this.window, (yield CONTINUATION), forward_keywords(arguments));
593     this.push_state(s);
594     var result = yield SUSPEND;
595     yield co_return(result);