key_binding_reader: protect current keymap stack
[conkeror.git] / modules / keymap.js
blob516e77ce24816dffc395bb2bba898c7b557c9d9e
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 in_module(null);
12 /* Generate vk name table  */
13 var keycode_to_vk_name = [];
14 var vk_name_to_keycode = {};
15 let (KeyEvent = Ci.nsIDOMKeyEvent,
16      prefix = "DOM_VK_") {
17     for (var i in KeyEvent) {
18         /* Check if this is a key binding */
19         if (i.substr(0, prefix.length) == prefix) {
20             let name = i.substr(prefix.length).toLowerCase();
21             let code = KeyEvent[i];
22             keycode_to_vk_name[code] = name;
23             vk_name_to_keycode[name] = code;
24         }
25     }
30  * Modifiers
31  */
33 function modifier (in_event_p, set_in_event) {
34     this.in_event_p = in_event_p;
35     this.set_in_event = set_in_event;
38 var modifiers = {
39     A: new modifier(function (event) { return event.altKey; },
40                     function (event) { event.altKey = true; }),
41     C: new modifier(function (event) { return event.ctrlKey; },
42                     function (event) { event.ctrlKey = true; }),
43     M: new modifier(function (event) { return event.metaKey; },
44                     function (event) { event.metaKey = true; }),
45     S: new modifier(function (event) {
46                         if (event.shiftKey) {
47                             var name;
48                             if (event.keyCode)
49                                 name = keycode_to_vk_name[event.keyCode];
50                             return ((name && name.length > 1) ||
51                                     (event.charCode == 32) ||
52                                     (event.button != null));
53                         }
54                         return false;
55                     },
56                     function (event) { event.shiftKey = true; })
58 var modifier_order = ['C', 'M', 'S'];
60 // check the platform and guess whether we should treat Alt as Meta
61 if (get_os() == 'Darwin') {
62     // In OS X, alt is a shift-like modifier, in that we
63     // only care about it for non-character events.
64     modifiers.A = new modifier(
65         function (event) {
66             if (event.altKey) {
67                 var name;
68                 if (event.keyCode)
69                     name = keycode_to_vk_name[event.keyCode];
70                 return ((name && name.length > 1) ||
71                         (event.charCode == 32) ||
72                         (event.button != null));
73             }
74             return false;
75         },
76         function (event) { event.altKey = true; });
77     modifier_order = ['C', 'M', 'A', 'S'];
78 } else {
79     modifiers.M = modifiers.A;
85  * Combos
86  */
88 function format_key_combo (event) {
89     var combo = '';
90     for each (var M in modifier_order) {
91         if (modifiers[M].in_event_p(event) ||
92             (event.sticky_modifiers &&
93              event.sticky_modifiers.indexOf(M) != -1))
94         {
95             combo += (M + '-');
96         }
97     }
98     if (event.charCode) {
99         if (event.charCode == 32)
100             combo += 'space';
101         else
102             combo += String.fromCharCode(event.charCode);
103     } else if (event.keyCode) {
104         combo += keycode_to_vk_name[event.keyCode];
105     } else if (event.button != null) {
106         combo += "mouse" + (event.button + 1);
107     }
108     return combo;
111 function unformat_key_combo (combo) {
112     var event = {
113         keyCode: 0,
114         charCode: 0,
115         altKey: false,
116         ctrlKey: false,
117         metaKey: false,
118         shiftKey: false
119     };
120     var M;
121     var i = 0;
122     var len = combo.length - 2;
123     var res;
124     while (i < len && combo[i+1] == '-') {
125         M = combo[i];
126         modifiers[M].set_in_event(event);
127         i+=2;
128     }
129     var key = combo.substring(i);
130     if (key.length == 1)
131         event.charCode = key.charCodeAt(0);
132     else if (key == 'space')
133         event.charCode = 32;
134     else if (vk_name_to_keycode[key])
135         event.keyCode = vk_name_to_keycode[key];
136     else if (key.substring(0, 5) == 'mouse')
137         event.button = parseInt(key.substring(5));
138     return event;
144  * Keymap datatype
145  */
147 define_keywords("$parent", "$help", "$name", "$anonymous");
148 function keymap () {
149     keywords(arguments);
150     this.parent = arguments.$parent;
151     this.bindings = {};
152     this.predicate_bindings = [];
153     this.fallthrough = [];
154     this.help = arguments.$help;
155     this.name = arguments.$name;
156     this.anonymous = arguments.$anonymous;
159 function define_keymap (name) {
160     keywords(arguments);
161     this[name] = new keymap($name = name, forward_keywords(arguments));
167  * Key Match Predicates.
168  */
170 function define_key_match_predicate (name, description, predicate) {
171     conkeror[name] = predicate;
172     conkeror[name].name = name;
173     conkeror[name].description = description;
174     return conkeror[name];
177 define_key_match_predicate('match_any_key', 'any key',
178     function (event) true);
180 // should be renamed to match_any_unmodified_character
181 define_key_match_predicate('match_any_unmodified_character', 'any unmodified character',
182     function (event) {
183         // this predicate can be used for both keypress and keydown
184         // fallthroughs.
185         try {
186             return ((event.type == 'keypress' && event.charCode)
187                     || event.keyCode > 31)
188                 && !modifiers.A.in_event_p(event)
189                 && !event.metaKey
190                 && !event.ctrlKey
191                 && !event.sticky_modifiers;
192         } catch (e) { return false; }
193     });
195 define_key_match_predicate('match_checkbox_keys', 'checkbox keys',
196     function (event) {
197         return event.keyCode == 32
198             && !event.shiftKey
199             && !event.metaKey
200             && !event.altKey
201             && !event.ctrlKey;
202         //XXX: keycode fallthroughs don't support sticky modifiers
203     });
205 define_key_match_predicate('match_text_keys', 'text editing keys',
206     function (event) {
207         return ((event.type == 'keypress' && event.charCode)
208                 || event.keyCode == 13 || event.keyCode > 31)
209             && !event.ctrlKey
210             && !event.metaKey
211             && !modifiers.A.in_event_p(event);
212         //XXX: keycode fallthroughs don't support sticky modifiers
213     });
217  */
219 function format_key_spec (key) {
220     if (key instanceof Function) {
221         if (key.description)
222             return "<"+key.description+">";
223         if (key.name)
224             return "<"+key.name+">";
225         return "<anonymous match function>";
226     }
227     return key;
230 function format_binding_sequence (seq) {
231     return seq.map(function (x) {
232             return format_key_spec(x.key);
233         }).join(" ");
237 function keymap_lookup (keymaps, combo, event) {
238     var i = keymaps.length - 1;
239     var kmap = keymaps[i];
240     var new_kmaps;
241     while (true) {
242         var bindings = kmap.bindings;
243         var bind = bindings[combo];
244         if (new_kmaps) {
245             if (bind) {
246                 if (bind.keymap)
247                     new_kmaps.unshift(bind.keymap);
248                 else
249                     return new_kmaps;
250             }
251         } else if (bind) {
252             if (bind.keymap)
253                 new_kmaps = [bind.keymap];
254             else
255                 return bind;
256         } else {
257             var pred_binds = kmap.predicate_bindings;
258             for (var j = 0, pblen = pred_binds.length; j < pblen; ++j) {
259                 bind = pred_binds[j];
260                 if (bind.key(event))
261                     return bind;
262             }
263         }
264         if (kmap.parent)
265             kmap = kmap.parent;
266         else if (i > 0)
267             kmap = keymaps[--i];
268         else if (new_kmaps)
269             return new_kmaps
270         else
271             return null;
272     }
276 function keymap_lookup_fallthrough (keymap, event) {
277     var predicates = keymap.fallthrough;
278     for (var i = 0, plen = predicates.length; i < plen; ++i) {
279         if (predicates[i](event))
280             return true;
281     }
282     return false;
286 function for_each_key_binding (keymaps, callback) {
287     var binding_sequence = [];
288     function helper (keymap) {
289         var binding;
290         for each (binding in keymap.bindings) {
291             binding_sequence.push(binding);
292             callback(binding_sequence);
293             if (binding.keymap)
294                 helper(binding.keymap);
295             binding_sequence.pop();
296         }
297         for each (binding in keymap.predicate_bindings) {
298             binding_sequence.push(binding);
299             callback(binding_sequence);
300             if (binding.keymap)
301                 helper(binding.keymap);
302             binding_sequence.pop();
303         }
304     }
305     //outer loop is to go down the parent-chain of keymaps
306     var i = keymaps.length - 1;
307     var keymap = keymaps[i];
308     while (true) {
309         helper(keymap);
310         if (keymap.parent)
311             keymap = keymap.parent;
312         else if (i > 0)
313             keymap = keymaps[--i];
314         else
315             break;
316     }
320 function keymap_lookup_command (keymaps, command) {
321     var list = [];
322     for_each_key_binding(keymaps, function (bind_seq) {
323             var bind = bind_seq[bind_seq.length - 1];
324             if (bind.command == command)
325                 list.push(format_binding_sequence(bind_seq));
326         });
327     return list;
336  * $fallthrough, $repeat and $browser_object are as for define_key.
338  * ref is the source code reference of the call to define_key.
340  * kmap is the keymap in which the binding is to be defined.
342  * seq is the key sequence being bound.  it may be necessary
343  * to auto-generate new keymaps to accomodate the key sequence.
345  * only one of new_command and new_keymap will be given.
346  * the one that is given is the thing being bound to.
347  */
348 define_keywords("$fallthrough", "$repeat", "$browser_object");
349 function define_key_internal (ref, kmap, seq, new_command, new_keymap) {
350     keywords(arguments);
351     var args = arguments;
352     var last_in_sequence; // flag to indicate the final key combo in the sequence.
353     var key; // current key combo as we iterate through the sequence.
354     var undefine_key = (new_command == null) &&
355         (new_keymap == null) &&
356         (! args.$fallthrough);
358     /* Replace `bind' with the binding specified by (cmd, fallthrough) */
359     function replace_binding (bind) {
360         if (last_in_sequence) {
361             bind.command = new_command;
362             bind.keymap = new_keymap;
363             bind.fallthrough = args.$fallthrough;
364             bind.source_code_reference = ref;
365             bind.repeat = args.$repeat;
366             bind.browser_object = args.$browser_object;
367         } else {
368             if (!bind.keymap)
369                 throw new Error("Key sequence has a non-keymap in prefix");
370             kmap = bind.keymap;
371         }
372     }
374     function make_binding () {
375         if (last_in_sequence) {
376             return { key: key,
377                      fallthrough: args.$fallthrough,
378                      command: new_command,
379                      keymap: new_keymap,
380                      source_code_reference: ref,
381                      repeat: args.$repeat,
382                      browser_object: args.$browser_object,
383                      bound_in: kmap };
384         } else {
385             let old_kmap = kmap;
386             kmap = new keymap($anonymous,
387                               $name = old_kmap.name + " " + format_key_spec(key));
388             kmap.bound_in = old_kmap;
389             return { key: key,
390                      keymap: kmap,
391                      source_code_reference: ref,
392                      bound_in: old_kmap };
393         }
394     }
396 sequence:
397     for (var i = 0, slen = seq.length; i < slen; ++i) {
398         key = seq[i];
399         last_in_sequence = (i == slen - 1);
401         if (typeof key == "function") { // it's a match predicate
402             // Check if the binding is already present in the keymap
403             var pred_binds = kmap.predicate_bindings;
404             for (var j = 0, pblen = pred_binds.length; j < pblen; j++) {
405                 if (pred_binds[j].key == key) {
406                     if (last_in_sequence && undefine_key)
407                         delete pred_binds[j];
408                     else
409                         replace_binding(pred_binds[j]);
410                     continue sequence;
411                 }
412             }
413             pred_binds.push(make_binding());
414         } else {
415             // Check if the binding is already present in the keymap
416             var bindings = kmap.bindings;
417             var binding = bindings[key];
418             if (binding) {
419                 if (last_in_sequence && undefine_key)
420                     delete bindings[key];
421                 else
422                     replace_binding(binding);
423                 continue sequence;
424             }
425             if (! undefine_key)
426                 bindings[key] = make_binding();
427         }
428     }
432  * bind SEQ to a keymap or command CMD in keymap KMAP.
434  *   If CMD is the special value `fallthrough', it will be bound as a
435  * fallthrough key.
437  * keywords:
439  *  $fallthrough: specifies that the keypress event will fall through
440  *      to gecko.  Note, by this method, only keypress will fall through.
441  *      Keyup and keydown will still be blocked.
443  *  $repeat: (command name) shortcut command to call if a prefix
444  *      command key is pressed twice in a row.
446  *  $browser_object: (browser object) Override the default
447  *      browser-object for the command.
448  */
449 define_keywords("$fallthrough", "$repeat", "$browser_object");
450 function define_key (kmap, seq, cmd) {
451     keywords(arguments);
452     var orig_seq = seq;
453     try {
454         var ref = get_caller_source_code_reference();
456         if (typeof seq == "string" && seq.length > 1)
457             seq = seq.split(" ");
459         if (!(typeof seq == "object") || !(seq instanceof Array))
460             seq = [seq];
462         // normalize the order of modifiers in key combos
463         seq = seq.map(
464             function (k) {
465                 if (typeof(k) == "string")
466                     return format_key_combo(unformat_key_combo(k));
467                 else
468                     return k;
469             });
471         var new_command = null;
472         var new_keymap = null;
473         if (typeof cmd == "string" || typeof cmd == "function")
474             new_command = cmd;
475         else if (cmd instanceof keymap)
476             new_keymap = cmd;
477         else if (cmd != null)
478             throw new Error("Invalid `cmd' argument: " + cmd);
480         define_key_internal(ref, kmap, seq, new_command, new_keymap,
481                             forward_keywords(arguments));
483     } catch (e if (typeof e == "string")) {
484         dumpln("Warning: Error occurred while binding sequence: " + orig_seq);
485         dumpln(e);
486     }
490 function undefine_key (kmap, seq) {
491     define_key(kmap, seq);
496  * define_fallthrough
498  *   Takes a keymap and a predicate on an event.  Fallthroughs defined by
499  * these means will cause all three of keydown, keypress, and keyup to
500  * fall through to gecko, whereas those defined by the $fallthrough
501  * keyword to define_key only affect keypress events.
503  *   The limitations of this method are that only the keyCode is available
504  * to the predicate, not the charCode, and keymap inheritance is not
505  * available for these "bindings".
506  */
507 function define_fallthrough (keymap, predicate) {
508     keymap.fallthrough.push(predicate);
513  * Minibuffer Read Key Binding
514  */
516 define_keymap("key_binding_reader_keymap");
517 define_key(key_binding_reader_keymap, match_any_key, "read-key-binding-key");
519 define_keywords("$keymap");
520 function key_binding_reader (window, continuation) {
521     keywords(arguments, $prompt = "Describe key:");
523     this.continuation = continuation;
525     if (arguments.$keymap)
526         this.target_keymap = arguments.$keymap;
527     else
528         this.target_keymap = get_current_keymaps(window).slice();
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);
598 provide("keymap");