gmane page-mode: binding for browser-object-links
[conkeror.git] / modules / keyboard.js
blobab3df789efb6a70b2a81d55a436dfe70c6a7135e
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 define_variable("key_bindings_ignore_capslock", false,
14     "When true, the case of characters in key bindings will be based "+
15     "only on whether shift was pressed--upper-case if yes, lower-case if "+
16     "no.  Effectively, this overrides the capslock key.  This option has "+
17     "no effect on ordinary typing in input fields.");
19 /* Generate vk name table  */
20 var keycode_to_vk_name = [];
21 var vk_name_to_keycode = {};
23     let KeyEvent = Ci.nsIDOMKeyEvent;
24     let prefix = "DOM_VK_";
25     for (var i in KeyEvent) {
26         /* Check if this is a key binding */
27         if (i.substr(0, prefix.length) == prefix) {
28             let name = i.substr(prefix.length).toLowerCase();
29             let code = KeyEvent[i];
30             keycode_to_vk_name[code] = name;
31             vk_name_to_keycode[name] = code;
32         }
33     }
36 var abort_key = null;
40  * Modifiers
41  */
43 function modifier (in_event_p, set_in_event) {
44     this.in_event_p = in_event_p;
45     this.set_in_event = set_in_event;
48 var modifiers = {
49     A: new modifier(function (event) { return event.altKey; },
50                     function (event) { event.altKey = true; }),
51     C: new modifier(function (event) { return event.ctrlKey; },
52                     function (event) { event.ctrlKey = true; }),
53     M: new modifier(function (event) { return event.metaKey; },
54                     function (event) { event.metaKey = true; }),
55     S: new modifier(function (event) {
56                         return (event.keyCode &&
57                                 event.charCode == 0 &&
58                                 event.shiftKey);
59                     },
60                     function (event) { event.shiftKey = true; })
62 var modifier_order = ['C', 'M', 'S'];
64 // check the platform and guess whether we should treat Alt as Meta
65 if (get_os() == 'Darwin') {
66     // In OS X, alt is a shift-like modifier, in that we
67     // only care about it for non-character events.
68     modifiers.A = new modifier(
69         function (event) {
70             return (event.keyCode &&
71                     event.charCode == 0 &&
72                     event.altKey);
73         },
74         function (event) { event.altKey = true; });
75     modifier_order = ['C', 'M', 'A', 'S'];
76 } else {
77     modifiers.M = modifiers.A;
83  * Keymap datatype
84  */
86 define_keywords("$parent", "$help", "$name", "$anonymous");
87 function keymap ()
89     keywords(arguments);
90     this.parent = arguments.$parent;
91     this.bindings = {};
92     this.predicate_bindings = [];
93     this.help = arguments.$help;
94     this.name = arguments.$name;
95     this.anonymous = arguments.$anonymous;
98 function define_keymap(name) {
99     keywords(arguments);
100     this[name] = new keymap($name = name, forward_keywords(arguments));
106  * Key Match Predicates.
108  *  Predicate bindings are tried for a match after the ordinary key-combo
109  * bindings.  They are predicate functions on the keypress event object.
110  * When such a predicate returns a true value, its associated command,
111  * keymap, or fallthrough declaration is performed.
112  */
114 function define_key_match_predicate (name, description, predicate) {
115     conkeror[name] = predicate;
116     conkeror[name].name = name;
117     conkeror[name].description = description;
120 define_key_match_predicate('match_any_key', 'any key',
121     function (event) { return true; });
123 define_key_match_predicate('match_any_unmodified_key', 'any unmodified key',
124     function (event) {
125         //XXX: the meaning of "unmodified" is platform dependent. for
126         // example, on OS X, Alt is used in combination with the
127         // character keys to type an alternate character.  A possible
128         // solution is to set the altKey property of the event to null
129         // for all keypress events on OS X.
130         try {
131             return event.charCode
132                 && !event.altKey
133                 && !event.metaKey
134                 && !event.ctrlKey
135                 && !event.sticky_modifiers;
136         } catch (e) {return false; }
137     });
141  */
143 function format_key_spec(key) {
144     if (key instanceof Function) {
145         if (key.description)
146             return "<"+key.description+">";
147         if (key.name)
148             return "<"+key.name+">";
149         return "<anonymous match function>";
150     }
151     return key;
154 function format_binding_sequence(seq) {
155     return seq.map(function (x) {
156             return format_key_spec(x.key);
157         }).join(" ");
161 function lookup_key_binding(kmap, combo, event)
163     do {
164         // Check if the key matches the keycode table
165         // var mods = get_modifiers(event);
166         var bindings = kmap.bindings;
167         var bind;
168         if ((bind = bindings[combo]) != null)
169             return bind;
171         // Check if the key matches a predicate
172         var pred_binds = kmap.predicate_bindings;
173         for (var i = 0; i < pred_binds.length; ++i) {
174             bind = pred_binds[i];
175             if (bind.key(event))
176                 return bind;
177         }
178         kmap = kmap.parent;
179     } while (kmap);
180     return null;
185  * $fallthrough and $repeat are as for define_key.
187  * ref is the source code reference of the call to define_key.
189  * kmap is the keymap in which the binding is to be defined.
191  * keys is the key sequence being bound.  it may be necessary
192  * to auto-generate new keymaps to accomodate the key sequence.
194  * only one of new_command and new_keymap will be given.
195  * the one that is given is the thing being bound to.
196  */
197 define_keywords("$fallthrough", "$repeat");
198 function define_key_internal(ref, kmap, keys, new_command, new_keymap)
200     keywords(arguments);
201     var args = arguments;
202     var parent_kmap = kmap.parent;
203     var final_binding; // flag to indicate the final key combo in the sequence.
204     var key; // current key combo as we iterate through the sequence.
206     /* Replace `bind' with the binding specified by (cmd, fallthrough) */
207     function replace_binding (bind) {
208         if (final_binding) {
209             bind.command = new_command;
210             bind.keymap = new_keymap;
211             bind.fallthrough = args.$fallthrough;
212             bind.source_code_reference = ref;
213             bind.repeat = args.$repeat;
214         } else {
215             if (!bind.keymap)
216                 throw new Error("Key sequence has a non-keymap in prefix");
217             kmap = bind.keymap;
218         }
219     }
221     function make_binding () {
222         if (final_binding) {
223             return {key: key,
224                     fallthrough: args.$fallthrough,
225                     command: new_command,
226                     keymap: new_keymap,
227                     source_code_reference: ref,
228                     repeat: args.$repeat,
229                     bound_in: kmap};
230         } else {
231             let old_kmap = kmap;
232             // Check for a corresponding binding a parent
233             kmap = new keymap($parent = parent_kmap, $anonymous,
234                               $name = old_kmap.name + " " + format_key_spec(key));
235             kmap.bound_in = old_kmap;
236             return {key: key,
237                     keymap: kmap,
238                     source_code_reference: ref,
239                     bound_in: old_kmap};
240         }
241     }
243 outer:
244     for (var i = 0; i < keys.length; ++i) {
245         key = keys[i];
246         final_binding = (i == keys.length - 1);
248         // Check if the specified binding is already present in the kmap
249         if (typeof(key) == "function") { // it's a match predicate
250             var pred_binds = kmap.predicate_bindings;
251             for (var j = 0; j < pred_binds.length; j++) {
252                 if (pred_binds[j].key == key) {
253                     replace_binding(pred_binds[j]);
254                     continue outer;
255                 }
256             }
258             if (!final_binding && parent_kmap) {
259                 var parent_pred_binds = parent_kmap.predicate_bindings;
260                 parent_kmap = null;
261                 for (j = 0; j < parent_pred_binds.length; j++) {
262                     if (parent_pred_binds[j].key == key &&
263                         parent_pred_binds[j].keymap)
264                     {
265                         parent_kmap = parent_pred_binds[j].keymap;
266                         break;
267                     }
268                 }
269             }
270             // Not already present, must be added
271             pred_binds.push(make_binding());
272         } else {
273             var bindings = kmap.bindings;
274             var binding = bindings[key];
276             if (binding) {
277                 replace_binding(binding);
278                 continue outer;
279             }
281             if (!final_binding) {
282                 let temp_parent = parent_kmap;
283                 parent_kmap = null;
284                 while (temp_parent) {
285                     let p_bindings = temp_parent.bindings;
286                     let p_binding = p_bindings[key];
287                     if (p_binding && p_binding.keymap) {
288                         parent_kmap = p_binding.keymap;
289                         break;
290                     } else {
291                         temp_parent = temp_parent.parent;
292                     }
293                 }
294             }
296             bindings[key] = make_binding();
297         }
298     }
301 // bind key to either the keymap or command in the keymap, kmap
302 // keywords:
304 //  $fallthrough: (bool) let this key be process by the web page
305 //      or gecko.
307 //  $repeat: (commnand name) shortcut command to call if a prefix
308 //      command key is pressed twice in a row.
310 define_keywords("$fallthrough", "$repeat");
311 function define_key(kmap, keys, cmd)
313     keywords(arguments);
314     var orig_keys = keys;
315     try {
316         var ref = get_caller_source_code_reference();
318         if (typeof(keys) == "string" && keys.length > 1)
319             keys = keys.split(" ");
321         if (!(typeof(keys) == "object") || !(keys instanceof Array))
322             keys = [keys];
324         // normalize the order of modifiers in string key combos
325         keys = keys.map(
326             function (k) {
327                 if (typeof(k) == "string")
328                     return format_key_combo(unformat_key_combo(k));
329                 else
330                     return k;
331             });
333         var new_command = null, new_keymap = null;
334         if (typeof(cmd) == "string" || typeof(cmd) == "function")
335             new_command = cmd;
336         else if (cmd instanceof keymap)
337             new_keymap = cmd;
338         else if (cmd != null)
339             throw new Error("Invalid `cmd' argument: " + cmd);
341         define_key_internal(ref, kmap, keys, new_command, new_keymap,
342                             forward_keywords(arguments));
344     } catch (e if (typeof(e) == "string")) {
345         dumpln("Warning: Error occurred while binding keys: " + orig_keys);
346         dumpln(e);
347     }
353  * Keypress Handler
354  */
356 define_variable("keyboard_key_sequence_help_timeout", 0,
357                 "Delay (in millseconds) before the current key sequence "+
358                 "prefix is displayed in the minibuffer.");
360 define_window_local_hook("keypress_hook", RUN_HOOK_UNTIL_SUCCESS);
364 function copy_event (event) {
365     var ev = {};
366     ev.keyCode = event.keyCode;
367     ev.charCode = event.charCode;
368     ev.ctrlKey = event.ctrlKey;
369     ev.metaKey = event.metaKey;
370     ev.altKey = event.altKey;
371     ev.shiftKey = event.shiftKey;
372     ev.sticky_modifiers = event.sticky_modifiers;
373     return ev;
376 function show_partial_key_sequence (window, state, ctx) {
377     if (!state.help_displayed)
378     {
379         state.help_timer_ID = window.setTimeout(
380             function () {
381                 window.minibuffer.show(ctx.key_sequence.join(" "));
382                 state.help_displayed = true;
383                 state.help_timer_ID = null;
384             }, keyboard_key_sequence_help_timeout);
385     }
386     else
387         window.minibuffer.show(ctx.key_sequence.join(" "));
390 function format_key_combo (event) {
391     var combo = '';
392     for each (var M in modifier_order) {
393         if (modifiers[M].in_event_p(event) ||
394             (event.sticky_modifiers &&
395              event.sticky_modifiers.indexOf(M) != -1))
396         {
397             combo += (M + '-');
398         }
399     }
400     if (event.charCode) {
401         if (event.charCode == 32) {
402             combo += 'space';
403         } else {
404             combo += String.fromCharCode(event.charCode);
405         }
406     } else if (event.keyCode) {
407         combo += keycode_to_vk_name[event.keyCode];
408     }
409     return combo;
412 function unformat_key_combo (combo) {
413     var event = {
414         keyCode: 0,
415         charCode: 0,
416         altKey: false,
417         ctrlKey: false,
418         metaKey: false,
419         shiftKey: false
420     };
421     var M;
422     var i = 0;
423     var len = combo.length - 2;
424     while (i < len && combo[i+1] == '-') {
425         M = combo[i];
426         modifiers[M].set_in_event(event);
427         i+=2;
428     }
429     var key = combo.substring(i);
430     if (key.length == 1) {
431         event.charCode = key.charCodeAt(0);
432     } else if (key == 'space') {
433         event.charCode = 32;
434     } else {
435         event.keyCode = vk_name_to_keycode[key];
436     }
437     return event;
440 function keypress_handler (true_event) {
441     try{
442         var window = this;
443         var state = window.keyboard;
445         var event = copy_event(true_event);
447         /* Filter out events from keys like the Windows/Super/Hyper key */
448         if (event.keyCode == 0 && event.charCode == 0 ||
449             event.keyCode == vk_name_to_keycode.caps_lock)
450             return;
452         if (key_bindings_ignore_capslock && event.charCode) {
453             let c = String.fromCharCode(event.charCode);
454             if (event.shiftKey)
455                 event.charCode = c.toUpperCase().charCodeAt(0);
456             else
457                 event.charCode = c.toLowerCase().charCodeAt(0);
458         }
460         /* Clear minibuffer message */
461         window.minibuffer.clear();
463         var binding = null;
464         var done = true; // flag for end of key sequence
466         var ctx;
467         if (!state.current_context)
468             ctx = state.current_context = { window: window, key_sequence: [], sticky_modifiers: 0 };
469         else
470             ctx = state.current_context;
472         event.sticky_modifiers = ctx.sticky_modifiers;
473         ctx.sticky_modifiers = 0;
475         var combo = format_key_combo(event);
476         ctx.combo = combo;
477         ctx.event = event;
479         // keypress_hook is used, for example, by key aliases
480         if (keypress_hook.run(window, ctx, true_event))
481             return;
483         var top_keymap =
484             state.override_keymap ||
485             window.buffers.current.keymap;
487         var active_keymap =
488             state.active_keymap ||
489             top_keymap;
491         var overlay_keymap = ctx.overlay_keymap;
493         binding =
494             (overlay_keymap && lookup_key_binding(overlay_keymap, combo, event)) ||
495             lookup_key_binding(active_keymap, combo, event);
497         // Should we stop this event from being processed by the gui?
498         //
499         // 1) we have a binding, and the binding's fallthrough property is not
500         //    true.
501         //
502         // 2) we are in the middle of a key sequence, and we need to say that
503         //    the key sequence given has no command.
504         //
505         if (!binding || !binding.fallthrough)
506         {
507             true_event.preventDefault();
508             true_event.stopPropagation();
509         }
511         // Finally, process the binding.
512         ctx.key_sequence.push(combo);
513         if (binding) {
514             if (binding.keymap) {
515                 state.active_keymap = binding.keymap;
516                 show_partial_key_sequence(window, state, ctx);
517                 // We're going for another round
518                 done = false;
519             } else if (binding.command) {
520                 let command = binding.command;
521                 if (ctx.repeat == command)
522                     command = binding.repeat;
523                 call_interactively(ctx, command);
524                 if (typeof(command) == "string" &&
525                     interactive_commands.get(command).prefix)
526                 {
527                     state.active_keymap = null;
528                     show_partial_key_sequence(window, state, ctx);
529                     if (binding.repeat)
530                         ctx.repeat = command;
531                     done = false;
532                 }
533             }
534         } else {
535             window.minibuffer.message(ctx.key_sequence.join(" ") + " is undefined");
536         }
538         // Clean up if we're done
539         if (done)
540         {
541             if (state.help_timer_ID != null)
542             {
543                 window.clearTimeout(state.help_timer_ID);
544                 state.help_timer_ID = null;
545             }
546             state.help_displayed = false;
547             state.active_keymap = null;
548             state.current_context = null;
549         }
550     } catch(e) { dump_error(e);}
553 function keydown_handler (event) {
554     event.stopPropagation();
557 function keyup_handler (event) {
558     event.stopPropagation();
561 function keyboard (window) {
562     this.window = window;
565 keyboard.prototype = {
566     last_key_down_event : null,
567     current_context : null,
568     active_keymap : null,
569     help_timer_ID : null,
570     help_displayed : false,
572     /* If this is non-null, it is used instead of the current buffer's
573      * keymap. */
574     override_keymap : null,
576     set_override_keymap : function (keymap) {
577         /* Clear out any in-progress key sequence. */
578         this.active_keymap = null;
579         this.current_context = null;
580         if (this.help_timer_ID != null)
581         {
582             this.window.clearTimeout(this.help_timer_ID);
583             this.help_timer_ID = null;
584         }
585         this.override_keymap = keymap;
586     }
590 function keyboard_initialize_window (window) {
591     window.keyboard = new keyboard(window);
592     window.addEventListener("keypress", keypress_handler, true /* capture */,
593                             false /* ignore untrusted events */);
594     window.addEventListener("keydown", keydown_handler, true, false);
595     window.addEventListener("keyup", keyup_handler, true, false);
598 add_hook("window_initialize_hook", keyboard_initialize_window);
600 function for_each_key_binding(keymap_or_buffer, callback) {
601     var keymap;
602     if (keymap_or_buffer instanceof conkeror.buffer) {
603         var buffer = keymap_or_buffer;
604         var window = buffer.window;
605         keymap = window.keyboard.override_keymap || buffer.keymap;
606     } else {
607         keymap = keymap_or_buffer;
608     }
609     var keymap_stack = [keymap];
610     var binding_stack = [];
611     function helper2(bind) {
612         binding_stack.push(bind);
613         callback(binding_stack);
614         if (bind.keymap && keymap_stack.indexOf(bind.keymap) == -1) {
615             keymap_stack.push(bind.keymap);
616             helper();
617             keymap_stack.pop();
618         }
619         binding_stack.pop();
620     }
621     function helper() {
622         while (true) {
623             var keymap = keymap_stack[keymap_stack.length - 1];
624             for (var i in keymap.bindings) {
625                 var b = keymap.bindings[i];
626                 helper2(b);
627             }
628             for (i in  keymap.predicate_bindings) {
629                 var bind = keymap.predicate_bindings[i];
630                 helper2(bind);
631                 var p = bind.key;
632                 if (p == match_any_key)
633                     return;
634             }
635             if (keymap.parent)
636                 keymap_stack[keymap_stack.length - 1] = keymap.parent;
637             else
638                 break;
639         }
640     }
641     helper();
644 function find_command_in_keymap(keymap_or_buffer, command) {
645     var list = [];
647     for_each_key_binding(keymap_or_buffer, function (bind_seq) {
648             var bind = bind_seq[bind_seq.length - 1];
649             if (bind.command == command)
650                 list.push(format_binding_sequence(bind_seq));
651         });
652     return list;
655 define_keymap("key_binding_reader_keymap");
656 define_key(key_binding_reader_keymap, match_any_key, "read-key-binding-key");
658 define_keywords("$buffer", "$keymap");
659 function key_binding_reader(continuation) {
660     keywords(arguments, $prompt = "Describe key:");
662     this.continuation = continuation;
664     if (arguments.$keymap)
665         this.target_keymap = arguments.$keymap;
666     else {
667         var buffer = arguments.$buffer;
668         var window = buffer.window;
669         this.target_keymap = window.keyboard.override_keymap || buffer.keymap;
670     }
672     this.key_sequence = [];
674     minibuffer_input_state.call(this, key_binding_reader_keymap, arguments.$prompt);
676 key_binding_reader.prototype = {
677     __proto__: minibuffer_input_state.prototype,
678     destroy: function () {
679         if (this.continuation)
680             this.continuation.throw(abort());
681     }
684 function invalid_key_binding(seq) {
685     var e = new Error(seq.join(" ") + " is undefined");
686     e.key_sequence = seq;
687     e.__proto__ = invalid_key_binding.prototype;
688     return e;
690 invalid_key_binding.prototype = {
691     __proto__: interactive_error.prototype
694 function read_key_binding_key(window, state, event) {
695     var combo = format_key_combo(event);
696     var binding = lookup_key_binding(state.target_keymap, combo, event);
698     state.key_sequence.push(combo);
700     if (binding == null) {
701         var c = state.continuation;
702         delete state.continuation;
703         window.minibuffer.pop_state();
704         c.throw(invalid_key_binding(state.key_sequence));
705         return;
706     }
708     if (binding.keymap) {
709         window.minibuffer._restore_normal_state();
710         window.minibuffer._input_text = state.key_sequence.join(" ") + " ";
711         state.target_keymap = binding.keymap;
712         return;
713     }
715     var c = state.continuation;
716     delete state.continuation;
718     window.minibuffer.pop_state();
720     if (c != null)
721         c([state.key_sequence, binding]);
723 interactive("read-key-binding-key", null, function (I) {
724     read_key_binding_key(I.window, I.minibuffer.check_state(key_binding_reader), I.event);
727 minibuffer.prototype.read_key_binding = function () {
728     keywords(arguments);
729     var s = new key_binding_reader((yield CONTINUATION), forward_keywords(arguments));
730     this.push_state(s);
731     var result = yield SUSPEND;
732     yield co_return(result);