keyboard.js: properly assign parent keymap for auto-generated prefix keymaps
[conkeror.git] / modules / keyboard.js
blobef71cfd4bf392a6ef47a8f362c512acce5d61856
1 /**
2  * (C) Copyright 2004-2007 Shawn Betts
3  * (C) Copyright 2007-2008 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 require("window.js");
11 require("command-line.js");
13 /* Generate vk name table  */
14 var keycode_to_vk_name = [];
15 var vk_name_to_keycode = {};
17     let KeyEvent = Ci.nsIDOMKeyEvent;
18     let prefix = "DOM_VK_";
19     for (var i in KeyEvent) {
20         /* Check if this is a key binding */
21         if (i.substr(0, prefix.length) == prefix) {
22             let name = i.substr(prefix.length).toLowerCase();
23             let code = KeyEvent[i];
24             keycode_to_vk_name[code] = name;
25             vk_name_to_keycode[name] = code;
26         }
27     }
30 var abort_key = null;
34  * Modifiers
35  */
37 function modifier (in_event_p, set_in_event) {
38     this.in_event_p = in_event_p;
39     this.set_in_event = set_in_event;
42 var modifiers = {
43     A: new modifier(function (event) { return event.altKey; },
44                     function (event) { event.altKey = true; }),
45     C: new modifier(function (event) { return event.ctrlKey; },
46                     function (event) { event.ctrlKey = true; }),
47     M: new modifier(function (event) { return event.metaKey; },
48                     function (event) { event.metaKey = true; }),
49     S: new modifier(function (event) {
50                         return (event.keyCode &&
51                                 event.charCode == 0 &&
52                                 event.shiftKey);
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             return (event.keyCode &&
65                     event.charCode == 0 &&
66                     event.altKey);
67         },
68         function (event) { event.altKey = true; });
69     modifier_order = ['C', 'M', 'A', 'S'];
70 } else {
71     modifiers.M = modifiers.A;
77  * Keymap datatype
78  */
80 define_keywords("$parent", "$help", "$name", "$anonymous");
81 function keymap ()
83     keywords(arguments);
84     this.parent = arguments.$parent;
85     this.bindings = {};
86     this.predicate_bindings = [];
87     this.help = arguments.$help;
88     this.name = arguments.$name;
89     this.anonymous = arguments.$anonymous;
92 function define_keymap(name) {
93     keywords(arguments);
94     this[name] = new keymap($name = name, forward_keywords(arguments));
100  * Key Match Predicates.
102  *  Predicate bindings are tried for a match after the ordinary key-combo
103  * bindings.  They are predicate functions on the keypress event object.
104  * When such a predicate returns a true value, its associated command,
105  * keymap, or fallthrough declaration is performed.
106  */
108 function match_any_key (event)
110     return true;
113 function match_any_unmodified_key (event)
115     //XXX: the meaning of "unmodified" is platform dependent. for example,
116     // on OS X, Alt is used in combination with the character keys to type
117     // an alternate character.  A possible solution is to set the altKey
118     // property of the event to null for all keypress events on OS X.
119     try {
120         return event.charCode
121             && !event.altKey
122             && !event.metaKey
123             && !event.ctrlKey
124             && !event.sticky_modifiers;
125     } catch (e) {return false; }
130  */
132 function format_key_spec(key) {
133     if (key.match_function) {
134         if (key.match_function == match_any_key)
135             return "<any-key>";
136         if (key.match_function == match_any_unmodified_key)
137             return "<any-unmodified-key>";
138         return "<match-function>";
139     }
140     return key;
143 function format_binding_sequence(seq) {
144     return seq.map(function (x) {
145             return format_key_spec(x.key);
146         }).join(" ");
150 function lookup_key_binding(kmap, combo, event)
152     do {
153         // Check if the key matches the keycode table
154         // var mods = get_modifiers(event);
155         var bindings = kmap.bindings;
156         var bind;
157         if ((bind = bindings[combo]) != null)
158             return bind;
160         // Check if the key matches a predicate
161         var pred_binds = kmap.predicate_bindings;
162         for (var i = 0; i < pred_binds.length; ++i) {
163             bind = pred_binds[i];
164             if (bind.key(event))
165                 return bind;
166         }
167         kmap = kmap.parent;
168     } while (kmap);
169     return null;
174  * $fallthrough and $repeat are as for define_key.
176  * ref is the source code reference of the call to define_key.
178  * kmap is the keymap in which the binding is to be defined.
180  * keys is the key sequence being bound.  it may be necessary
181  * to auto-generate new keymaps to accomodate the key sequence.
183  * only one of new_command and new_keymap will be given.
184  * the one that is given is the thing being bound to.
185  */
186 define_keywords("$fallthrough", "$repeat");
187 function define_key_internal(ref, kmap, keys, new_command, new_keymap)
189     keywords(arguments);
190     var args = arguments;
191     var parent_kmap = kmap.parent;
192     var final_binding; // flag to indicate the final key combo in the sequence.
193     var key; // current key combo as we iterate through the sequence.
195     /* Replace `bind' with the binding specified by (cmd, fallthrough) */
196     function replace_binding (bind) {
197         if (final_binding) {
198             bind.command = new_command;
199             bind.keymap = new_keymap;
200             bind.fallthrough = args.$fallthrough;
201             bind.source_code_reference = ref;
202             bind.repeat = args.$repeat;
203         } else {
204             if (!bind.keymap)
205                 throw new Error("Key sequence has a non-keymap in prefix");
206             kmap = bind.keymap;
207         }
208     }
210     function make_binding () {
211         if (final_binding) {
212             return {key: key,
213                     fallthrough: args.$fallthrough,
214                     command: new_command,
215                     keymap: new_keymap,
216                     source_code_reference: ref,
217                     repeat: args.$repeat,
218                     bound_in: kmap};
219         } else {
220             let old_kmap = kmap;
221             // Check for a corresponding binding a parent
222             kmap = new keymap($parent = parent_kmap, $anonymous,
223                               $name = old_kmap.name + " " + format_key_spec(key));
224             kmap.bound_in = old_kmap;
225             return {key: key,
226                     keymap: kmap,
227                     source_code_reference: ref,
228                     bound_in: old_kmap};
229         }
230     }
232 outer:
233     for (var i = 0; i < keys.length; ++i) {
234         key = keys[i];
235         final_binding = (i == keys.length - 1);
237         // Check if the specified binding is already present in the kmap
238         if (typeof(key) == "function") { // it's a match predicate
239             var pred_binds = kmap.predicate_bindings;
240             for (var j = 0; j < pred_binds.length; j++) {
241                 if (pred_binds[j].key == key) {
242                     replace_binding(pred_binds[j]);
243                     continue outer;
244                 }
245             }
247             if (!final_binding && parent_kmap) {
248                 var parent_pred_binds = parent_kmap.predicate_bindings;
249                 parent_kmap = null;
250                 for (j = 0; j < parent_pred_binds.length; j++) {
251                     if (parent_pred_binds[j].key == key &&
252                         parent_pred_binds[j].keymap)
253                     {
254                         parent_kmap = parent_pred_binds[j].keymap;
255                         break;
256                     }
257                 }
258             }
259             // Not already present, must be added
260             pred_binds.push(make_binding());
261         } else {
262             var bindings = kmap.bindings;
263             var binding = bindings[key];
265             if (binding) {
266                 replace_binding(binding);
267                 continue outer;
268             }
270             if (!final_binding) {
271                 let temp_parent = parent_kmap;
272                 parent_kmap = null;
273                 while (temp_parent) {
274                     let p_bindings = temp_parent.bindings;
275                     let p_binding = p_bindings[key];
276                     if (p_binding && p_binding.keymap) {
277                         parent_kmap = p_binding.keymap;
278                         break;
279                     } else {
280                         temp_parent = temp_parent.parent;
281                     }
282                 }
283             }
285             bindings[key] = make_binding();
286         }
287     }
290 // bind key to either the keymap or command in the keymap, kmap
291 // keywords:
293 //  $fallthrough: (bool) let this key be process by the web page
294 //      or gecko.
296 //  $repeat: (commnand name) shortcut command to call if a prefix
297 //      command key is pressed twice in a row.
299 define_keywords("$fallthrough", "$repeat");
300 function define_key(kmap, keys, cmd)
302     keywords(arguments);
303     var orig_keys = keys;
304     try {
305         var ref = get_caller_source_code_reference();
307         if (typeof(keys) == "string" && keys.length > 1)
308             keys = keys.split(" ");
310         if (!(typeof(keys) == "object") || !(keys instanceof Array))
311             keys = [keys];
313         // normalize the order of modifiers in string key combos
314         keys = keys.map(
315             function (k) {
316                 if (typeof(k) == "string")
317                     return format_key_combo(unformat_key_combo(k));
318                 else
319                     return k;
320             });
322         var new_command = null, new_keymap = null;
323         if (typeof(cmd) == "string" || typeof(cmd) == "function")
324             new_command = cmd;
325         else if (cmd instanceof keymap)
326             new_keymap = cmd;
327         else if (cmd != null)
328             throw new Error("Invalid `cmd' argument: " + cmd);
330         define_key_internal(ref, kmap, keys, new_command, new_keymap,
331                             forward_keywords(arguments));
333     } catch (e if (typeof(e) == "string")) {
334         dumpln("Warning: Error occurred while binding keys: " + orig_keys);
335         dumpln(e);
336     }
342  * Keypress Handler
343  */
345 define_variable("keyboard_key_sequence_help_timeout", 0,
346                 "Delay (in millseconds) before the current key sequence "+
347                 "prefix is displayed in the minibuffer.");
349 define_window_local_hook("keypress_hook", RUN_HOOK_UNTIL_SUCCESS);
353 function copy_event (event) {
354     var ev = {};
355     ev.keyCode = event.keyCode;
356     ev.charCode = event.charCode;
357     ev.ctrlKey = event.ctrlKey;
358     ev.metaKey = event.metaKey;
359     ev.altKey = event.altKey;
360     ev.shiftKey = event.shiftKey;
361     ev.sticky_modifiers = event.sticky_modifiers;
362     return ev;
365 function show_partial_key_sequence (window, state, ctx) {
366     if (!state.help_displayed)
367     {
368         state.help_timer_ID = window.setTimeout(
369             function () {
370                 window.minibuffer.show(ctx.key_sequence.join(" "));
371                 state.help_displayed = true;
372                 state.help_timer_ID = null;
373             }, keyboard_key_sequence_help_timeout);
374     }
375     else
376         window.minibuffer.show(ctx.key_sequence.join(" "));
379 function format_key_combo (event) {
380     var combo = '';
381     for each (var M in modifier_order) {
382         if (modifiers[M].in_event_p(event) ||
383             (event.sticky_modifiers &&
384              event.sticky_modifiers.indexOf(M) != -1))
385         {
386             combo += (M + '-');
387         }
388     }
389     if (event.charCode) {
390         if (event.charCode == 32) {
391             combo += 'space';
392         } else {
393             combo += String.fromCharCode(event.charCode);
394         }
395     } else if (event.keyCode) {
396         combo += keycode_to_vk_name[event.keyCode];
397     }
398     return combo;
401 function unformat_key_combo (combo) {
402     var event = {
403         keyCode: 0,
404         charCode: 0,
405         altKey: false,
406         ctrlKey: false,
407         metaKey: false,
408         shiftKey: false
409     };
410     var M;
411     var i = 0;
412     while (combo[i+1] == '-') {
413         M = combo[i];
414         modifiers[M].set_in_event(event);
415         i+=2;
416     }
417     var key = combo.substring(i);
418     if (key.length == 1) {
419         event.charCode = key.charCodeAt(0);
420     } else if (key == 'space') {
421         event.charCode = 32;
422     } else {
423         event.keyCode = vk_name_to_keycode[key];
424     }
425     return event;
428 function keypress_handler (true_event) {
429     try{
430         var window = this;
431         var state = window.keyboard;
433         var event = copy_event(true_event);
435         /* Filter out events from keys like the Windows/Super/Hyper key */
436         if (event.keyCode == 0 && event.charCode == 0)
437             return;
439         /* Clear minibuffer message */
440         window.minibuffer.clear();
442         var binding = null;
443         var done = true; // flag for end of key sequence
445         var ctx;
446         if (!state.current_context)
447             ctx = state.current_context = { window: window, key_sequence: [], sticky_modifiers: 0 };
448         else
449             ctx = state.current_context;
451         event.sticky_modifiers = ctx.sticky_modifiers;
452         ctx.sticky_modifiers = 0;
454         var combo = format_key_combo(event);
455         ctx.combo = combo;
456         ctx.event = event;
458         // keypress_hook is used, for example, by key aliases
459         if (keypress_hook.run(window, ctx, true_event))
460             return;
462         var top_keymap =
463             state.override_keymap ||
464             window.buffers.current.keymap;
466         var active_keymap =
467             state.active_keymap ||
468             top_keymap;
470         var overlay_keymap = ctx.overlay_keymap;
472         binding =
473             (overlay_keymap && lookup_key_binding(overlay_keymap, combo, event)) ||
474             lookup_key_binding(active_keymap, combo, event);
476         // Should we stop this event from being processed by the gui?
477         //
478         // 1) we have a binding, and the binding's fallthrough property is not
479         //    true.
480         //
481         // 2) we are in the middle of a key sequence, and we need to say that
482         //    the key sequence given has no command.
483         //
484         if (!binding || !binding.fallthrough)
485         {
486             true_event.preventDefault();
487             true_event.stopPropagation();
488         }
490         // Finally, process the binding.
491         ctx.key_sequence.push(combo);
492         if (binding) {
493             if (binding.keymap) {
494                 state.active_keymap = binding.keymap;
495                 show_partial_key_sequence(window, state, ctx);
496                 // We're going for another round
497                 done = false;
498             } else if (binding.command) {
499                 let command = binding.command;
500                 if (ctx.repeat == command)
501                     command = binding.repeat;
502                 call_interactively(ctx, command);
503                 if (typeof(command) == "string" &&
504                     interactive_commands.get(command).prefix)
505                 {
506                     state.active_keymap = null;
507                     show_partial_key_sequence(window, state, ctx);
508                     if (binding.repeat)
509                         ctx.repeat = command;
510                     done = false;
511                 }
512             }
513         } else {
514             window.minibuffer.message(ctx.key_sequence.join(" ") + " is undefined");
515         }
517         // Clean up if we're done
518         if (done)
519         {
520             if (state.help_timer_ID != null)
521             {
522                 window.clearTimeout(state.help_timer_ID);
523                 state.help_timer_ID = null;
524             }
525             state.help_displayed = false;
526             state.active_keymap = null;
527             state.current_context = null;
528         }
529     } catch(e) { dump_error(e);}
532 function keyboard(window)
534     this.window = window;
537 keyboard.prototype = {
538     last_key_down_event : null,
539     current_context : null,
540     active_keymap : null,
541     help_timer_ID : null,
542     help_displayed : false,
544     /* If this is non-null, it is used instead of the current buffer's
545      * keymap. */
546     override_keymap : null,
548     set_override_keymap : function (keymap) {
549         /* Clear out any in-progress key sequence. */
550         this.active_keymap = null;
551         this.current_context = null;
552         if (this.help_timer_ID != null)
553         {
554             this.window.clearTimeout(this.help_timer_ID);
555             this.help_timer_ID = null;
556         }
557         this.override_keymap = keymap;
558     }
562 function keyboard_initialize_window(window)
564     window.keyboard = new keyboard(window);
566     window.addEventListener ("keypress", keypress_handler, true /* capture */,
567                             false /* ignore untrusted events */);
570 add_hook("window_initialize_hook", keyboard_initialize_window);
572 function for_each_key_binding(keymap_or_buffer, callback) {
573     var keymap;
574     if (keymap_or_buffer instanceof conkeror.buffer) {
575         var buffer = keymap_or_buffer;
576         var window = buffer.window;
577         keymap = window.keyboard.override_keymap || buffer.keymap;
578     } else {
579         keymap = keymap_or_buffer;
580     }
581     var keymap_stack = [keymap];
582     var binding_stack = [];
583     function helper2(bind) {
584         binding_stack.push(bind);
585         callback(binding_stack);
586         if (bind.keymap && keymap_stack.indexOf(bind.keymap) == -1) {
587             keymap_stack.push(bind.keymap);
588             helper();
589             keymap_stack.pop();
590         }
591         binding_stack.pop();
592     }
593     function helper() {
594         while (true) {
595             var keymap = keymap_stack[keymap_stack.length - 1];
596             for (var i in keymap.bindings) {
597                 var b = keymap.bindings[i];
598                 helper2(b);
599             }
600             for (i in  keymap.predicate_bindings) {
601                 var bind = keymap.predicate_bindings[i];
602                 helper2(bind);
603                 var p = bind.key;
604                 if (p == match_any_key)
605                     return;
606             }
607             if (keymap.parent)
608                 keymap_stack[keymap_stack.length - 1] = keymap.parent;
609             else
610                 break;
611         }
612     }
613     helper();
616 function find_command_in_keymap(keymap_or_buffer, command) {
617     var list = [];
619     for_each_key_binding(keymap_or_buffer, function (bind_seq) {
620             var bind = bind_seq[bind_seq.length - 1];
621             if (bind.command == command)
622                 list.push(format_binding_sequence(bind_seq));
623         });
624     return list;
627 define_keymap("key_binding_reader_keymap");
628 define_key(key_binding_reader_keymap, match_any_key, "read-key-binding-key");
630 define_keywords("$buffer", "$keymap");
631 function key_binding_reader(continuation) {
632     keywords(arguments, $prompt = "Describe key:");
634     this.continuation = continuation;
636     if (arguments.$keymap)
637         this.target_keymap = arguments.$keymap;
638     else {
639         var buffer = arguments.$buffer;
640         var window = buffer.window;
641         this.target_keymap = window.keyboard.override_keymap || buffer.keymap;
642     }
644     this.key_sequence = [];
646     minibuffer_input_state.call(this, key_binding_reader_keymap, arguments.$prompt);
648 key_binding_reader.prototype = {
649     __proto__: minibuffer_input_state.prototype,
650     destroy: function () {
651         if (this.continuation)
652             this.continuation.throw(abort());
653     }
656 function invalid_key_binding(seq) {
657     var e = new Error(seq.join(" ") + " is undefined");
658     e.key_sequence = seq;
659     e.__proto__ = invalid_key_binding.prototype;
660     return e;
662 invalid_key_binding.prototype = {
663     __proto__: interactive_error.prototype
666 function read_key_binding_key(window, state, event) {
667     var combo = format_key_combo(event);
668     var binding = lookup_key_binding(state.target_keymap, combo, event);
670     state.key_sequence.push(combo);
672     if (binding == null) {
673         var c = state.continuation;
674         delete state.continuation;
675         window.minibuffer.pop_state();
676         c.throw(invalid_key_binding(state.key_sequence));
677         return;
678     }
680     if (binding.keymap) {
681         window.minibuffer._restore_normal_state();
682         window.minibuffer._input_text = state.key_sequence.join(" ") + " ";
683         state.target_keymap = binding.keymap;
684         return;
685     }
687     var c = state.continuation;
688     delete state.continuation;
690     window.minibuffer.pop_state();
692     if (c != null)
693         c([state.key_sequence, binding]);
695 interactive("read-key-binding-key", null, function (I) {
696     read_key_binding_key(I.window, I.minibuffer.check_state(key_binding_reader), I.event);
699 minibuffer.prototype.read_key_binding = function () {
700     keywords(arguments);
701     var s = new key_binding_reader((yield CONTINUATION), forward_keywords(arguments));
702     this.push_state(s);
703     var result = yield SUSPEND;
704     yield co_return(result);