Give automatically created prefix keymaps a name for debugging purposes
[conkeror.git] / modules / keyboard.js
blobc6791717babcf9323d7a4313d67bd22dbf17cf6d
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 && parent_kmap) {
271                 var p_bindings = parent_kmap.bindings;
272                 parent_kmap = null;
273                 var p_binding = p_bindings[key];
274                 if (p_binding && p_binding.keymap)
275                     parent_kmap = p_binding.keymap;
276             }
278             bindings[key] = make_binding();
279         }
280     }
283 // bind key to either the keymap or command in the keymap, kmap
284 // keywords:
286 //  $fallthrough: (bool) let this key be process by the web page
287 //      or gecko.
289 //  $repeat: (commnand name) shortcut command to call if a prefix
290 //      command key is pressed twice in a row.
292 define_keywords("$fallthrough", "$repeat");
293 function define_key(kmap, keys, cmd)
295     keywords(arguments);
296     var orig_keys = keys;
297     try {
298         var ref = get_caller_source_code_reference();
300         if (typeof(keys) == "string" && keys.length > 1)
301             keys = keys.split(" ");
303         if (!(typeof(keys) == "object") || !(keys instanceof Array))
304             keys = [keys];
306         // normalize the order of modifiers in string key combos
307         keys = keys.map(
308             function (k) {
309                 if (typeof(k) == "string")
310                     return format_key_combo(unformat_key_combo(k));
311                 else
312                     return k;
313             });
315         var new_command = null, new_keymap = null;
316         if (typeof(cmd) == "string" || typeof(cmd) == "function")
317             new_command = cmd;
318         else if (cmd instanceof keymap)
319             new_keymap = cmd;
320         else if (cmd != null)
321             throw new Error("Invalid `cmd' argument: " + cmd);
323         define_key_internal(ref, kmap, keys, new_command, new_keymap,
324                             forward_keywords(arguments));
326     } catch (e if (typeof(e) == "string")) {
327         dumpln("Warning: Error occurred while binding keys: " + orig_keys);
328         dumpln(e);
329     }
335  * Keypress Handler
336  */
338 define_variable("keyboard_key_sequence_help_timeout", 0,
339                 "Delay (in millseconds) before the current key sequence "+
340                 "prefix is displayed in the minibuffer.");
342 define_window_local_hook("keypress_hook", RUN_HOOK_UNTIL_SUCCESS);
346 function copy_event (event) {
347     var ev = {};
348     ev.keyCode = event.keyCode;
349     ev.charCode = event.charCode;
350     ev.ctrlKey = event.ctrlKey;
351     ev.metaKey = event.metaKey;
352     ev.altKey = event.altKey;
353     ev.shiftKey = event.shiftKey;
354     ev.sticky_modifiers = event.sticky_modifiers;
355     return ev;
358 function show_partial_key_sequence (window, state, ctx) {
359     if (!state.help_displayed)
360     {
361         state.help_timer_ID = window.setTimeout(
362             function () {
363                 window.minibuffer.show(ctx.key_sequence.join(" "));
364                 state.help_displayed = true;
365                 state.help_timer_ID = null;
366             }, keyboard_key_sequence_help_timeout);
367     }
368     else
369         window.minibuffer.show(ctx.key_sequence.join(" "));
372 function format_key_combo (event) {
373     var combo = '';
374     for each (var M in modifier_order) {
375         if (modifiers[M].in_event_p(event) ||
376             (event.sticky_modifiers &&
377              event.sticky_modifiers.indexOf(M) != -1))
378         {
379             combo += (M + '-');
380         }
381     }
382     if (event.charCode) {
383         if (event.charCode == 32) {
384             combo += 'space';
385         } else {
386             combo += String.fromCharCode(event.charCode);
387         }
388     } else if (event.keyCode) {
389         combo += keycode_to_vk_name[event.keyCode];
390     }
391     return combo;
394 function unformat_key_combo (combo) {
395     var event = {
396         keyCode: 0,
397         charCode: 0,
398         altKey: false,
399         ctrlKey: false,
400         metaKey: false,
401         shiftKey: false
402     };
403     var M;
404     var i = 0;
405     while (combo[i+1] == '-') {
406         M = combo[i];
407         modifiers[M].set_in_event(event);
408         i+=2;
409     }
410     var key = combo.substring(i);
411     if (key.length == 1) {
412         event.charCode = key.charCodeAt(0);
413     } else if (key == 'space') {
414         event.charCode = 32;
415     } else {
416         event.keyCode = vk_name_to_keycode[key];
417     }
418     return event;
421 function keypress_handler (true_event) {
422     try{
423         var window = this;
424         var state = window.keyboard;
426         var event = copy_event(true_event);
428         /* Filter out events from keys like the Windows/Super/Hyper key */
429         if (event.keyCode == 0 && event.charCode == 0)
430             return;
432         /* Clear minibuffer message */
433         window.minibuffer.clear();
435         var binding = null;
436         var done = true; // flag for end of key sequence
438         var ctx;
439         if (!state.current_context)
440             ctx = state.current_context = { window: window, key_sequence: [], sticky_modifiers: 0 };
441         else
442             ctx = state.current_context;
444         event.sticky_modifiers = ctx.sticky_modifiers;
445         ctx.sticky_modifiers = 0;
447         var combo = format_key_combo(event);
448         ctx.combo = combo;
449         ctx.event = event;
451         // keypress_hook is used, for example, by key aliases
452         if (keypress_hook.run(window, ctx, true_event))
453             return;
455         var top_keymap =
456             state.override_keymap ||
457             window.buffers.current.keymap;
459         var active_keymap =
460             state.active_keymap ||
461             top_keymap;
463         var overlay_keymap = ctx.overlay_keymap;
465         binding =
466             (overlay_keymap && lookup_key_binding(overlay_keymap, combo, event)) ||
467             lookup_key_binding(active_keymap, combo, event);
469         // Should we stop this event from being processed by the gui?
470         //
471         // 1) we have a binding, and the binding's fallthrough property is not
472         //    true.
473         //
474         // 2) we are in the middle of a key sequence, and we need to say that
475         //    the key sequence given has no command.
476         //
477         if (!binding || !binding.fallthrough)
478         {
479             true_event.preventDefault();
480             true_event.stopPropagation();
481         }
483         // Finally, process the binding.
484         ctx.key_sequence.push(combo);
485         if (binding) {
486             if (binding.keymap) {
487                 state.active_keymap = binding.keymap;
488                 show_partial_key_sequence(window, state, ctx);
489                 // We're going for another round
490                 done = false;
491             } else if (binding.command) {
492                 let command = binding.command;
493                 if (ctx.repeat == command)
494                     command = binding.repeat;
495                 call_interactively(ctx, command);
496                 if (typeof(command) == "string" &&
497                     interactive_commands.get(command).prefix)
498                 {
499                     state.active_keymap = null;
500                     show_partial_key_sequence(window, state, ctx);
501                     if (binding.repeat)
502                         ctx.repeat = command;
503                     done = false;
504                 }
505             }
506         } else {
507             window.minibuffer.message(ctx.key_sequence.join(" ") + " is undefined");
508         }
510         // Clean up if we're done
511         if (done)
512         {
513             if (state.help_timer_ID != null)
514             {
515                 window.clearTimeout(state.help_timer_ID);
516                 state.help_timer_ID = null;
517             }
518             state.help_displayed = false;
519             state.active_keymap = null;
520             state.current_context = null;
521         }
522     } catch(e) { dump_error(e);}
525 function keyboard(window)
527     this.window = window;
530 keyboard.prototype = {
531     last_key_down_event : null,
532     current_context : null,
533     active_keymap : null,
534     help_timer_ID : null,
535     help_displayed : false,
537     /* If this is non-null, it is used instead of the current buffer's
538      * keymap. */
539     override_keymap : null,
541     set_override_keymap : function (keymap) {
542         /* Clear out any in-progress key sequence. */
543         this.active_keymap = null;
544         this.current_context = null;
545         if (this.help_timer_ID != null)
546         {
547             this.window.clearTimeout(this.help_timer_ID);
548             this.help_timer_ID = null;
549         }
550         this.override_keymap = keymap;
551     }
555 function keyboard_initialize_window(window)
557     window.keyboard = new keyboard(window);
559     window.addEventListener ("keypress", keypress_handler, true /* capture */,
560                             false /* ignore untrusted events */);
563 add_hook("window_initialize_hook", keyboard_initialize_window);
565 function for_each_key_binding(keymap_or_buffer, callback) {
566     var keymap;
567     if (keymap_or_buffer instanceof conkeror.buffer) {
568         var buffer = keymap_or_buffer;
569         var window = buffer.window;
570         keymap = window.keyboard.override_keymap || buffer.keymap;
571     } else {
572         keymap = keymap_or_buffer;
573     }
574     var keymap_stack = [keymap];
575     var binding_stack = [];
576     function helper2(bind) {
577         binding_stack.push(bind);
578         callback(binding_stack);
579         if (bind.keymap && keymap_stack.indexOf(bind.keymap) == -1) {
580             keymap_stack.push(bind.keymap);
581             helper();
582             keymap_stack.pop();
583         }
584         binding_stack.pop();
585     }
586     function helper() {
587         while (true) {
588             var keymap = keymap_stack[keymap_stack.length - 1];
589             for (var i in keymap.bindings) {
590                 var b = keymap.bindings[i];
591                 helper2(b);
592             }
593             for (i in  keymap.predicate_bindings) {
594                 var bind = keymap.predicate_bindings[i];
595                 helper2(bind);
596                 var p = bind.key;
597                 if (p == match_any_key)
598                     return;
599             }
600             if (keymap.parent)
601                 keymap_stack[keymap_stack.length - 1] = keymap.parent;
602             else
603                 break;
604         }
605     }
606     helper();
609 function find_command_in_keymap(keymap_or_buffer, command) {
610     var list = [];
612     for_each_key_binding(keymap_or_buffer, function (bind_seq) {
613             var bind = bind_seq[bind_seq.length - 1];
614             if (bind.command == command)
615                 list.push(format_binding_sequence(bind_seq));
616         });
617     return list;
620 define_keymap("key_binding_reader_keymap");
621 define_key(key_binding_reader_keymap, match_any_key, "read-key-binding-key");
623 define_keywords("$buffer", "$keymap");
624 function key_binding_reader(continuation) {
625     keywords(arguments, $prompt = "Describe key:");
627     this.continuation = continuation;
629     if (arguments.$keymap)
630         this.target_keymap = arguments.$keymap;
631     else {
632         var buffer = arguments.$buffer;
633         var window = buffer.window;
634         this.target_keymap = window.keyboard.override_keymap || buffer.keymap;
635     }
637     this.key_sequence = [];
639     minibuffer_input_state.call(this, key_binding_reader_keymap, arguments.$prompt);
641 key_binding_reader.prototype = {
642     __proto__: minibuffer_input_state.prototype,
643     destroy: function () {
644         if (this.continuation)
645             this.continuation.throw(abort());
646     }
649 function invalid_key_binding(seq) {
650     var e = new Error(seq.join(" ") + " is undefined");
651     e.key_sequence = seq;
652     e.__proto__ = invalid_key_binding.prototype;
653     return e;
655 invalid_key_binding.prototype = {
656     __proto__: interactive_error.prototype
659 function read_key_binding_key(window, state, event) {
660     var combo = format_key_combo(event);
661     var binding = lookup_key_binding(state.target_keymap, combo, event);
663     state.key_sequence.push(combo);
665     if (binding == null) {
666         var c = state.continuation;
667         delete state.continuation;
668         window.minibuffer.pop_state();
669         c.throw(invalid_key_binding(state.key_sequence));
670         return;
671     }
673     if (binding.keymap) {
674         window.minibuffer._restore_normal_state();
675         window.minibuffer._input_text = state.key_sequence.join(" ") + " ";
676         state.target_keymap = binding.keymap;
677         return;
678     }
680     var c = state.continuation;
681     delete state.continuation;
683     window.minibuffer.pop_state();
685     if (c != null)
686         c([state.key_sequence, binding]);
688 interactive("read-key-binding-key", null, function (I) {
689     read_key_binding_key(I.window, I.minibuffer.check_state(key_binding_reader), I.event);
692 minibuffer.prototype.read_key_binding = function () {
693     keywords(arguments);
694     var s = new key_binding_reader((yield CONTINUATION), forward_keywords(arguments));
695     this.push_state(s);
696     var result = yield SUSPEND;
697     yield co_return(result);