2 * (C) Copyright 2004-2007 Shawn Betts
3 * (C) Copyright 2007-2008 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;
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;
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 &&
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(
70 return (event.keyCode &&
71 event.charCode == 0 &&
74 function (event) { event.altKey = true; });
75 modifier_order = ['C', 'M', 'A', 'S'];
77 modifiers.M = modifiers.A;
86 define_keywords("$parent", "$help", "$name", "$anonymous");
90 this.parent = arguments.$parent;
92 this.predicate_bindings = [];
93 this.help = arguments.$help;
94 this.name = arguments.$name;
95 this.anonymous = arguments.$anonymous;
98 function define_keymap(name) {
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.
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',
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.
131 return event.charCode
135 && !event.sticky_modifiers;
136 } catch (e) {return false; }
143 function format_key_spec(key) {
144 if (key instanceof Function) {
146 return "<"+key.description+">";
148 return "<"+key.name+">";
149 return "<anonymous match function>";
154 function format_binding_sequence(seq) {
155 return seq.map(function (x) {
156 return format_key_spec(x.key);
161 function lookup_key_binding(kmap, combo, event)
164 // Check if the key matches the keycode table
165 // var mods = get_modifiers(event);
166 var bindings = kmap.bindings;
168 if ((bind = bindings[combo]) != null)
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];
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.
197 define_keywords("$fallthrough", "$repeat");
198 function define_key_internal(ref, kmap, keys, new_command, new_keymap)
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) {
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;
216 throw new Error("Key sequence has a non-keymap in prefix");
221 function make_binding () {
224 fallthrough: args.$fallthrough,
225 command: new_command,
227 source_code_reference: ref,
228 repeat: args.$repeat,
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 replace_binding(pred_binds[j]);
258 if (!final_binding && parent_kmap) {
259 var parent_pred_binds = parent_kmap.predicate_bindings;
261 for (j = 0; j < parent_pred_binds.length; j++) {
262 if (parent_pred_binds[j].key == key &&
263 parent_pred_binds[j].keymap)
265 parent_kmap = parent_pred_binds[j].keymap;
270 // Not already present, must be added
271 pred_binds.push(make_binding());
273 var bindings = kmap.bindings;
274 var binding = bindings[key];
277 replace_binding(binding);
281 if (!final_binding) {
282 let temp_parent = parent_kmap;
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;
291 temp_parent = temp_parent.parent;
296 bindings[key] = make_binding();
301 // bind key to either the keymap or command in the keymap, kmap
304 // $fallthrough: (bool) let this key be process by the web page
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)
314 var orig_keys = keys;
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))
324 // normalize the order of modifiers in string key combos
327 if (typeof(k) == "string")
328 return format_key_combo(unformat_key_combo(k));
333 var new_command = null, new_keymap = null;
334 if (typeof(cmd) == "string" || typeof(cmd) == "function")
336 else if (cmd instanceof keymap)
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);
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) {
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;
376 function show_partial_key_sequence (window, state, ctx) {
377 if (!state.help_displayed)
379 state.help_timer_ID = window.setTimeout(
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);
387 window.minibuffer.show(ctx.key_sequence.join(" "));
390 function format_key_combo (event) {
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))
400 if (event.charCode) {
401 if (event.charCode == 32) {
404 combo += String.fromCharCode(event.charCode);
406 } else if (event.keyCode) {
407 combo += keycode_to_vk_name[event.keyCode];
412 function unformat_key_combo (combo) {
423 var len = combo.length - 2;
424 while (i < len && combo[i+1] == '-') {
426 modifiers[M].set_in_event(event);
429 var key = combo.substring(i);
430 if (key.length == 1) {
431 event.charCode = key.charCodeAt(0);
432 } else if (key == 'space') {
435 event.keyCode = vk_name_to_keycode[key];
440 function keypress_handler (true_event) {
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)
452 if (key_bindings_ignore_capslock && event.charCode) {
453 let c = String.fromCharCode(event.charCode);
455 event.charCode = c.toUpperCase().charCodeAt(0);
457 event.charCode = c.toLowerCase().charCodeAt(0);
460 /* Clear minibuffer message */
461 window.minibuffer.clear();
464 var done = true; // flag for end of key sequence
467 if (!state.current_context)
468 ctx = state.current_context = { window: window, key_sequence: [], sticky_modifiers: 0 };
470 ctx = state.current_context;
472 event.sticky_modifiers = ctx.sticky_modifiers;
473 ctx.sticky_modifiers = 0;
475 var combo = format_key_combo(event);
479 // keypress_hook is used, for example, by key aliases
480 if (keypress_hook.run(window, ctx, true_event))
484 state.override_keymap ||
485 window.buffers.current.keymap;
488 state.active_keymap ||
491 var overlay_keymap = ctx.overlay_keymap;
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?
499 // 1) we have a binding, and the binding's fallthrough property is not
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.
505 if (!binding || !binding.fallthrough)
507 true_event.preventDefault();
508 true_event.stopPropagation();
511 // Finally, process the binding.
512 ctx.key_sequence.push(combo);
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
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)
527 state.active_keymap = null;
528 show_partial_key_sequence(window, state, ctx);
530 ctx.repeat = command;
535 window.minibuffer.message(ctx.key_sequence.join(" ") + " is undefined");
538 // Clean up if we're done
541 if (state.help_timer_ID != null)
543 window.clearTimeout(state.help_timer_ID);
544 state.help_timer_ID = null;
546 state.help_displayed = false;
547 state.active_keymap = null;
548 state.current_context = null;
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
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)
582 this.window.clearTimeout(this.help_timer_ID);
583 this.help_timer_ID = null;
585 this.override_keymap = keymap;
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) {
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;
607 keymap = keymap_or_buffer;
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);
623 var keymap = keymap_stack[keymap_stack.length - 1];
624 for (var i in keymap.bindings) {
625 var b = keymap.bindings[i];
628 for (i in keymap.predicate_bindings) {
629 var bind = keymap.predicate_bindings[i];
632 if (p == match_any_key)
636 keymap_stack[keymap_stack.length - 1] = keymap.parent;
644 function find_command_in_keymap(keymap_or_buffer, command) {
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));
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;
667 var buffer = arguments.$buffer;
668 var window = buffer.window;
669 this.target_keymap = window.keyboard.override_keymap || buffer.keymap;
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());
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;
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));
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;
715 var c = state.continuation;
716 delete state.continuation;
718 window.minibuffer.pop_state();
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 () {
729 var s = new key_binding_reader((yield CONTINUATION), forward_keywords(arguments));
731 var result = yield SUSPEND;
732 yield co_return(result);