2 * (C) Copyright 2004-2007 Shawn Betts
3 * (C) Copyright 2007-2009 John J. Foerch
4 * (C) Copyright 2007-2008 Jeremy Maitin-Shepard
6 * Use, modification, and distribution are subject to the terms specified in the
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;
41 function modifier (in_event_p, set_in_event) {
42 this.in_event_p = in_event_p;
43 this.set_in_event = set_in_event;
47 A: new modifier(function (event) { return event.altKey; },
48 function (event) { event.altKey = true; }),
49 C: new modifier(function (event) { return event.ctrlKey; },
50 function (event) { event.ctrlKey = true; }),
51 M: new modifier(function (event) { return event.metaKey; },
52 function (event) { event.metaKey = true; }),
53 S: new modifier(function (event) {
54 return (event.keyCode &&
55 event.charCode == 0 &&
58 function (event) { event.shiftKey = true; })
60 var modifier_order = ['C', 'M', 'S'];
62 // check the platform and guess whether we should treat Alt as Meta
63 if (get_os() == 'Darwin') {
64 // In OS X, alt is a shift-like modifier, in that we
65 // only care about it for non-character events.
66 modifiers.A = new modifier(
68 return (event.keyCode &&
69 event.charCode == 0 &&
72 function (event) { event.altKey = true; });
73 modifier_order = ['C', 'M', 'A', 'S'];
75 modifiers.M = modifiers.A;
84 define_keywords("$parent", "$help", "$name", "$anonymous");
87 this.parent = arguments.$parent;
89 this.predicate_bindings = [];
90 this.help = arguments.$help;
91 this.name = arguments.$name;
92 this.anonymous = arguments.$anonymous;
95 function define_keymap (name) {
97 this[name] = new keymap($name = name, forward_keywords(arguments));
103 * Key Match Predicates.
105 * Predicate bindings are tried for a match after the ordinary key-combo
106 * bindings. They are predicate functions on the keypress event object.
107 * When such a predicate returns a true value, its associated command,
108 * keymap, or fallthrough declaration is performed.
111 function define_key_match_predicate (name, description, predicate) {
112 conkeror[name] = predicate;
113 conkeror[name].name = name;
114 conkeror[name].description = description;
117 define_key_match_predicate('match_any_key', 'any key',
118 function (event) { return true; });
120 define_key_match_predicate('match_any_unmodified_key', 'any unmodified key',
122 //XXX: the meaning of "unmodified" is platform dependent. for
123 // example, on OS X, Alt is used in combination with the
124 // character keys to type an alternate character. A possible
125 // solution is to set the altKey property of the event to null
126 // for all keypress events on OS X.
128 return event.charCode
132 && !event.sticky_modifiers;
133 } catch (e) {return false; }
140 function format_key_spec (key) {
141 if (key instanceof Function) {
143 return "<"+key.description+">";
145 return "<"+key.name+">";
146 return "<anonymous match function>";
151 function format_binding_sequence (seq) {
152 return seq.map(function (x) {
153 return format_key_spec(x.key);
158 function lookup_key_binding (kmap, combo, event) {
160 // Check if the key matches the keycode table
161 // var mods = get_modifiers(event);
162 var bindings = kmap.bindings;
164 if ((bind = bindings[combo]) != null)
167 // Check if the key matches a predicate
168 var pred_binds = kmap.predicate_bindings;
169 for (var i = 0; i < pred_binds.length; ++i) {
170 bind = pred_binds[i];
181 * $fallthrough, $repeat and $browser_object are as for define_key.
183 * ref is the source code reference of the call to define_key.
185 * kmap is the keymap in which the binding is to be defined.
187 * keys is the key sequence being bound. it may be necessary
188 * to auto-generate new keymaps to accomodate the key sequence.
190 * only one of new_command and new_keymap will be given.
191 * the one that is given is the thing being bound to.
193 define_keywords("$fallthrough", "$repeat", "$browser_object");
194 function define_key_internal (ref, kmap, keys, new_command, new_keymap) {
196 var args = arguments;
197 var parent_kmap = kmap.parent;
198 var final_binding; // flag to indicate the final key combo in the sequence.
199 var key; // current key combo as we iterate through the sequence.
200 var undefine_key = (new_command == null) &&
201 (new_keymap == null) &&
202 (! args.$fallthrough);
204 /* Replace `bind' with the binding specified by (cmd, fallthrough) */
205 function replace_binding (bind) {
207 bind.command = new_command;
208 bind.keymap = new_keymap;
209 bind.fallthrough = args.$fallthrough;
210 bind.source_code_reference = ref;
211 bind.repeat = args.$repeat;
212 bind.browser_object = args.$browser_object;
215 throw new Error("Key sequence has a non-keymap in prefix");
220 function make_binding () {
223 fallthrough: args.$fallthrough,
224 command: new_command,
226 source_code_reference: ref,
227 repeat: args.$repeat,
228 browser_object: args.$browser_object,
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;
238 source_code_reference: ref,
244 for (var i = 0; i < keys.length; ++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 if (final_binding && undefine_key) {
254 delete pred_binds[j];
256 replace_binding(pred_binds[j]);
262 if (!final_binding && parent_kmap) {
263 var parent_pred_binds = parent_kmap.predicate_bindings;
265 for (j = 0; j < parent_pred_binds.length; j++) {
266 if (parent_pred_binds[j].key == key &&
267 parent_pred_binds[j].keymap)
269 parent_kmap = parent_pred_binds[j].keymap;
274 // Not already present, must be added
275 pred_binds.push(make_binding());
277 var bindings = kmap.bindings;
278 var binding = bindings[key];
281 if (final_binding && undefine_key) {
282 delete bindings[key];
284 replace_binding(binding);
289 if (!final_binding) {
290 let temp_parent = parent_kmap;
292 while (temp_parent) {
293 let p_bindings = temp_parent.bindings;
294 let p_binding = p_bindings[key];
295 if (p_binding && p_binding.keymap) {
296 parent_kmap = p_binding.keymap;
299 temp_parent = temp_parent.parent;
304 bindings[key] = make_binding();
309 // bind key to either the keymap or command in the keymap, kmap
312 // $fallthrough: (bool) let this key be process by the web page
315 // $repeat: (commnand name) shortcut command to call if a prefix
316 // command key is pressed twice in a row.
318 // $browser_object: (browser object) Override the default
319 // browser-object for the command.
321 define_keywords("$fallthrough", "$repeat", "$browser_object");
322 function define_key (kmap, keys, cmd) {
324 var orig_keys = keys;
326 var ref = get_caller_source_code_reference();
328 if (typeof(keys) == "string" && keys.length > 1)
329 keys = keys.split(" ");
331 if (!(typeof(keys) == "object") || !(keys instanceof Array))
334 // normalize the order of modifiers in string key combos
337 if (typeof(k) == "string")
338 return format_key_combo(unformat_key_combo(k));
343 var new_command = null, new_keymap = null;
344 if (typeof(cmd) == "string" || typeof(cmd) == "function")
346 else if (cmd instanceof keymap)
348 else if (cmd != null)
349 throw new Error("Invalid `cmd' argument: " + cmd);
351 define_key_internal(ref, kmap, keys, new_command, new_keymap,
352 forward_keywords(arguments));
354 } catch (e if (typeof(e) == "string")) {
355 dumpln("Warning: Error occurred while binding keys: " + orig_keys);
361 function undefine_key (kmap, keys) {
362 define_key(kmap, keys);
370 define_variable("keyboard_key_sequence_help_timeout", 0,
371 "Delay (in millseconds) before the current key sequence "+
372 "prefix is displayed in the minibuffer.");
374 define_window_local_hook("keypress_hook", RUN_HOOK_UNTIL_SUCCESS);
378 function copy_event (event) {
380 ev.keyCode = event.keyCode;
381 ev.charCode = event.charCode;
382 ev.ctrlKey = event.ctrlKey;
383 ev.metaKey = event.metaKey;
384 ev.altKey = event.altKey;
385 ev.shiftKey = event.shiftKey;
386 ev.sticky_modifiers = event.sticky_modifiers;
390 function show_partial_key_sequence (window, state, ctx) {
391 if (!state.help_displayed)
393 state.help_timer_ID = window.setTimeout(
395 window.minibuffer.show(ctx.key_sequence.join(" "));
396 state.help_displayed = true;
397 state.help_timer_ID = null;
398 }, keyboard_key_sequence_help_timeout);
401 window.minibuffer.show(ctx.key_sequence.join(" "));
404 function format_key_combo (event) {
406 for each (var M in modifier_order) {
407 if (modifiers[M].in_event_p(event) ||
408 (event.sticky_modifiers &&
409 event.sticky_modifiers.indexOf(M) != -1))
414 if (event.charCode) {
415 if (event.charCode == 32) {
418 combo += String.fromCharCode(event.charCode);
420 } else if (event.keyCode) {
421 combo += keycode_to_vk_name[event.keyCode];
426 function unformat_key_combo (combo) {
437 var len = combo.length - 2;
438 while (i < len && combo[i+1] == '-') {
440 modifiers[M].set_in_event(event);
443 var key = combo.substring(i);
444 if (key.length == 1) {
445 event.charCode = key.charCodeAt(0);
446 } else if (key == 'space') {
449 event.keyCode = vk_name_to_keycode[key];
454 function keypress_handler (true_event) {
457 var state = window.keyboard;
459 var event = copy_event(true_event);
461 /* Filter out events from keys like the Windows/Super/Hyper key */
462 if (event.keyCode == 0 && event.charCode == 0 ||
463 event.keyCode == vk_name_to_keycode.caps_lock)
466 if (key_bindings_ignore_capslock && event.charCode) {
467 let c = String.fromCharCode(event.charCode);
469 event.charCode = c.toUpperCase().charCodeAt(0);
471 event.charCode = c.toLowerCase().charCodeAt(0);
474 /* Clear minibuffer message */
475 window.minibuffer.clear();
478 var done = true; // flag for end of key sequence
480 if (! state.current_context) {
481 state.current_context = new interactive_context(window.buffers.current);
482 state.current_context.key_sequence = [];
483 state.current_context.sticky_modifiers = 0;
485 var I = state.current_context;
487 event.sticky_modifiers = I.sticky_modifiers;
488 I.sticky_modifiers = 0;
490 var combo = format_key_combo(event);
491 I.key_sequence.push(combo);
495 // keypress_hook is used, for example, by key aliases
496 if (keypress_hook.run(window, I, true_event))
500 state.override_keymap ||
501 window.buffers.current.keymap;
504 state.active_keymap ||
507 var overlay_keymap = I.overlay_keymap;
510 (overlay_keymap && lookup_key_binding(overlay_keymap, combo, event)) ||
511 lookup_key_binding(active_keymap, combo, event);
513 // Should we stop this event from being processed by the gui?
515 // 1) we have a binding, and the binding's fallthrough property is not
518 // 2) we are in the middle of a key sequence, and we need to say that
519 // the key sequence given has no command.
521 if (!binding || !binding.fallthrough) {
522 true_event.preventDefault();
523 true_event.stopPropagation();
526 // Finally, process the binding.
528 if (binding.browser_object != null)
529 I.binding_browser_object = binding.browser_object;
530 if (binding.keymap) {
531 state.active_keymap = binding.keymap;
532 show_partial_key_sequence(window, state, I);
533 // We're going for another round
535 } else if (binding.command) {
536 let command = binding.command;
537 if (I.repeat == command)
538 command = binding.repeat;
539 call_interactively(I, command);
540 if (typeof(command) == "string" &&
541 interactive_commands.get(command).prefix)
543 state.active_keymap = null;
544 show_partial_key_sequence(window, state, I);
551 window.minibuffer.message(I.key_sequence.join(" ") + " is undefined");
554 // Clean up if we're done
556 if (state.help_timer_ID != null) {
557 window.clearTimeout(state.help_timer_ID);
558 state.help_timer_ID = null;
560 state.help_displayed = false;
561 state.active_keymap = null;
562 state.current_context = null;
564 } catch (e) { dump_error(e); }
567 function keyboard(window)
569 this.window = window;
572 keyboard.prototype = {
573 last_key_down_event : null,
574 current_context : null,
575 active_keymap : null,
576 help_timer_ID : null,
577 help_displayed : false,
579 /* If this is non-null, it is used instead of the current buffer's
581 override_keymap : null,
583 set_override_keymap : function (keymap) {
584 /* Clear out any in-progress key sequence. */
585 this.active_keymap = null;
586 this.current_context = null;
587 if (this.help_timer_ID != null)
589 this.window.clearTimeout(this.help_timer_ID);
590 this.help_timer_ID = null;
592 this.override_keymap = keymap;
597 function keyboard_initialize_window(window)
599 window.keyboard = new keyboard(window);
601 window.addEventListener ("keypress", keypress_handler, true /* capture */,
602 false /* ignore untrusted events */);
605 add_hook("window_initialize_hook", keyboard_initialize_window);
607 function for_each_key_binding(keymap_or_buffer, callback) {
609 if (keymap_or_buffer instanceof conkeror.buffer) {
610 var buffer = keymap_or_buffer;
611 var window = buffer.window;
612 keymap = window.keyboard.override_keymap || buffer.keymap;
614 keymap = keymap_or_buffer;
616 var keymap_stack = [keymap];
617 var binding_stack = [];
618 function helper2(bind) {
619 binding_stack.push(bind);
620 callback(binding_stack);
621 if (bind.keymap && keymap_stack.indexOf(bind.keymap) == -1) {
622 keymap_stack.push(bind.keymap);
630 var keymap = keymap_stack[keymap_stack.length - 1];
631 for (var i in keymap.bindings) {
632 var b = keymap.bindings[i];
635 for (i in keymap.predicate_bindings) {
636 var bind = keymap.predicate_bindings[i];
639 if (p == match_any_key)
643 keymap_stack[keymap_stack.length - 1] = keymap.parent;
651 function find_command_in_keymap(keymap_or_buffer, command) {
654 for_each_key_binding(keymap_or_buffer, function (bind_seq) {
655 var bind = bind_seq[bind_seq.length - 1];
656 if (bind.command == command)
657 list.push(format_binding_sequence(bind_seq));
662 define_keymap("key_binding_reader_keymap");
663 define_key(key_binding_reader_keymap, match_any_key, "read-key-binding-key");
665 define_keywords("$buffer", "$keymap");
666 function key_binding_reader(continuation) {
667 keywords(arguments, $prompt = "Describe key:");
669 this.continuation = continuation;
671 if (arguments.$keymap)
672 this.target_keymap = arguments.$keymap;
674 var buffer = arguments.$buffer;
675 var window = buffer.window;
676 this.target_keymap = window.keyboard.override_keymap || buffer.keymap;
679 this.key_sequence = [];
681 minibuffer_input_state.call(this, key_binding_reader_keymap, arguments.$prompt);
683 key_binding_reader.prototype = {
684 __proto__: minibuffer_input_state.prototype,
685 destroy: function () {
686 if (this.continuation)
687 this.continuation.throw(abort());
691 function invalid_key_binding(seq) {
692 var e = new Error(seq.join(" ") + " is undefined");
693 e.key_sequence = seq;
694 e.__proto__ = invalid_key_binding.prototype;
697 invalid_key_binding.prototype = {
698 __proto__: interactive_error.prototype
701 function read_key_binding_key(window, state, event) {
702 var combo = format_key_combo(event);
703 var binding = lookup_key_binding(state.target_keymap, combo, event);
705 state.key_sequence.push(combo);
707 if (binding == null) {
708 var c = state.continuation;
709 delete state.continuation;
710 window.minibuffer.pop_state();
711 c.throw(invalid_key_binding(state.key_sequence));
715 if (binding.keymap) {
716 window.minibuffer._restore_normal_state();
717 window.minibuffer._input_text = state.key_sequence.join(" ") + " ";
718 state.target_keymap = binding.keymap;
722 var c = state.continuation;
723 delete state.continuation;
725 window.minibuffer.pop_state();
728 c([state.key_sequence, binding]);
730 interactive("read-key-binding-key", null, function (I) {
731 read_key_binding_key(I.window, I.minibuffer.check_state(key_binding_reader), I.event);
734 minibuffer.prototype.read_key_binding = function () {
736 var s = new key_binding_reader((yield CONTINUATION), forward_keywords(arguments));
738 var result = yield SUSPEND;
739 yield co_return(result);