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) ||
56 event.charCode == 32) &&
59 function (event) { event.shiftKey = true; })
61 var modifier_order = ['C', 'M', 'S'];
63 // check the platform and guess whether we should treat Alt as Meta
64 if (get_os() == 'Darwin') {
65 // In OS X, alt is a shift-like modifier, in that we
66 // only care about it for non-character events.
67 modifiers.A = new modifier(
69 return (event.keyCode &&
70 event.charCode == 0 &&
73 function (event) { event.altKey = true; });
74 modifier_order = ['C', 'M', 'A', 'S'];
76 modifiers.M = modifiers.A;
85 define_keywords("$parent", "$help", "$name", "$anonymous");
88 this.parent = arguments.$parent;
90 this.predicate_bindings = [];
91 this.help = arguments.$help;
92 this.name = arguments.$name;
93 this.anonymous = arguments.$anonymous;
96 function define_keymap (name) {
98 this[name] = new keymap($name = name, forward_keywords(arguments));
104 * Key Match Predicates.
106 * Predicate bindings are tried for a match after the ordinary key-combo
107 * bindings. They are predicate functions on the keypress event object.
108 * When such a predicate returns a true value, its associated command,
109 * keymap, or fallthrough declaration is performed.
112 function define_key_match_predicate (name, description, predicate) {
113 conkeror[name] = predicate;
114 conkeror[name].name = name;
115 conkeror[name].description = description;
118 define_key_match_predicate('match_any_key', 'any key',
119 function (event) { return true; });
121 define_key_match_predicate('match_any_unmodified_key', 'any unmodified key',
123 //XXX: the meaning of "unmodified" is platform dependent. for
124 // example, on OS X, Alt is used in combination with the
125 // character keys to type an alternate character. A possible
126 // solution is to set the altKey property of the event to null
127 // for all keypress events on OS X.
129 return event.charCode
133 && !event.sticky_modifiers;
134 } catch (e) {return false; }
141 function format_key_spec (key) {
142 if (key instanceof Function) {
144 return "<"+key.description+">";
146 return "<"+key.name+">";
147 return "<anonymous match function>";
152 function format_binding_sequence (seq) {
153 return seq.map(function (x) {
154 return format_key_spec(x.key);
159 function lookup_key_binding (kmap, combo, event) {
161 // Check if the key matches the keycode table
162 // var mods = get_modifiers(event);
163 var bindings = kmap.bindings;
165 if ((bind = bindings[combo]) != null)
168 // Check if the key matches a predicate
169 var pred_binds = kmap.predicate_bindings;
170 for (var i = 0; i < pred_binds.length; ++i) {
171 bind = pred_binds[i];
182 * $fallthrough, $repeat and $browser_object are as for define_key.
184 * ref is the source code reference of the call to define_key.
186 * kmap is the keymap in which the binding is to be defined.
188 * keys is the key sequence being bound. it may be necessary
189 * to auto-generate new keymaps to accomodate the key sequence.
191 * only one of new_command and new_keymap will be given.
192 * the one that is given is the thing being bound to.
194 define_keywords("$fallthrough", "$repeat", "$browser_object");
195 function define_key_internal (ref, kmap, keys, new_command, new_keymap) {
197 var args = arguments;
198 var parent_kmap = kmap.parent;
199 var final_binding; // flag to indicate the final key combo in the sequence.
200 var key; // current key combo as we iterate through the sequence.
201 var undefine_key = (new_command == null) &&
202 (new_keymap == null) &&
203 (! args.$fallthrough);
205 /* Replace `bind' with the binding specified by (cmd, fallthrough) */
206 function replace_binding (bind) {
208 bind.command = new_command;
209 bind.keymap = new_keymap;
210 bind.fallthrough = args.$fallthrough;
211 bind.source_code_reference = ref;
212 bind.repeat = args.$repeat;
213 bind.browser_object = args.$browser_object;
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,
229 browser_object: args.$browser_object,
233 // Check for a corresponding binding a parent
234 kmap = new keymap($parent = parent_kmap, $anonymous,
235 $name = old_kmap.name + " " + format_key_spec(key));
236 kmap.bound_in = old_kmap;
239 source_code_reference: ref,
245 for (var i = 0; i < keys.length; ++i) {
247 final_binding = (i == keys.length - 1);
249 // Check if the specified binding is already present in the kmap
250 if (typeof(key) == "function") { // it's a match predicate
251 var pred_binds = kmap.predicate_bindings;
252 for (var j = 0; j < pred_binds.length; j++) {
253 if (pred_binds[j].key == key) {
254 if (final_binding && undefine_key) {
255 delete pred_binds[j];
257 replace_binding(pred_binds[j]);
263 if (!final_binding && parent_kmap) {
264 var parent_pred_binds = parent_kmap.predicate_bindings;
266 for (j = 0; j < parent_pred_binds.length; j++) {
267 if (parent_pred_binds[j].key == key &&
268 parent_pred_binds[j].keymap)
270 parent_kmap = parent_pred_binds[j].keymap;
275 // Not already present, must be added
276 pred_binds.push(make_binding());
278 var bindings = kmap.bindings;
279 var binding = bindings[key];
282 if (final_binding && undefine_key) {
283 delete bindings[key];
285 replace_binding(binding);
290 if (!final_binding) {
291 let temp_parent = parent_kmap;
293 while (temp_parent) {
294 let p_bindings = temp_parent.bindings;
295 let p_binding = p_bindings[key];
296 if (p_binding && p_binding.keymap) {
297 parent_kmap = p_binding.keymap;
300 temp_parent = temp_parent.parent;
305 bindings[key] = make_binding();
310 // bind key to either the keymap or command in the keymap, kmap
313 // $fallthrough: (bool) let this key be process by the web page
316 // $repeat: (commnand name) shortcut command to call if a prefix
317 // command key is pressed twice in a row.
319 // $browser_object: (browser object) Override the default
320 // browser-object for the command.
322 define_keywords("$fallthrough", "$repeat", "$browser_object");
323 function define_key (kmap, keys, cmd) {
325 var orig_keys = keys;
327 var ref = get_caller_source_code_reference();
329 if (typeof(keys) == "string" && keys.length > 1)
330 keys = keys.split(" ");
332 if (!(typeof(keys) == "object") || !(keys instanceof Array))
335 // normalize the order of modifiers in string key combos
338 if (typeof(k) == "string")
339 return format_key_combo(unformat_key_combo(k));
344 var new_command = null, new_keymap = null;
345 if (typeof(cmd) == "string" || typeof(cmd) == "function")
347 else if (cmd instanceof keymap)
349 else if (cmd != null)
350 throw new Error("Invalid `cmd' argument: " + cmd);
352 define_key_internal(ref, kmap, keys, new_command, new_keymap,
353 forward_keywords(arguments));
355 } catch (e if (typeof(e) == "string")) {
356 dumpln("Warning: Error occurred while binding keys: " + orig_keys);
362 function undefine_key (kmap, keys) {
363 define_key(kmap, keys);
371 define_variable("keyboard_key_sequence_help_timeout", 0,
372 "Delay (in millseconds) before the current key sequence "+
373 "prefix is displayed in the minibuffer.");
375 define_window_local_hook("keypress_hook", RUN_HOOK_UNTIL_SUCCESS);
379 function copy_event (event) {
381 ev.keyCode = event.keyCode;
382 ev.charCode = event.charCode;
383 ev.ctrlKey = event.ctrlKey;
384 ev.metaKey = event.metaKey;
385 ev.altKey = event.altKey;
386 ev.shiftKey = event.shiftKey;
387 ev.sticky_modifiers = event.sticky_modifiers;
391 function show_partial_key_sequence (window, state, ctx) {
392 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);
400 window.minibuffer.show(ctx.key_sequence.join(" "));
403 function format_key_combo (event) {
405 for each (var M in modifier_order) {
406 if (modifiers[M].in_event_p(event) ||
407 (event.sticky_modifiers &&
408 event.sticky_modifiers.indexOf(M) != -1))
413 if (event.charCode) {
414 if (event.charCode == 32) {
417 combo += String.fromCharCode(event.charCode);
419 } else if (event.keyCode) {
420 combo += keycode_to_vk_name[event.keyCode];
425 function unformat_key_combo (combo) {
436 var len = combo.length - 2;
437 while (i < len && combo[i+1] == '-') {
439 modifiers[M].set_in_event(event);
442 var key = combo.substring(i);
443 if (key.length == 1) {
444 event.charCode = key.charCodeAt(0);
445 } else if (key == 'space') {
448 event.keyCode = vk_name_to_keycode[key];
453 function keypress_handler (true_event) {
456 var state = window.keyboard;
458 var event = copy_event(true_event);
460 /* Filter out events from keys like the Windows/Super/Hyper key */
461 if (event.keyCode == 0 && event.charCode == 0 ||
462 event.keyCode == vk_name_to_keycode.caps_lock)
465 if (key_bindings_ignore_capslock && event.charCode) {
466 let c = String.fromCharCode(event.charCode);
468 event.charCode = c.toUpperCase().charCodeAt(0);
470 event.charCode = c.toLowerCase().charCodeAt(0);
473 /* Clear minibuffer message */
474 window.minibuffer.clear();
477 var done = true; // flag for end of key sequence
479 if (! state.current_context) {
480 state.current_context = new interactive_context(window.buffers.current);
481 state.current_context.key_sequence = [];
482 state.current_context.sticky_modifiers = 0;
484 var I = state.current_context;
486 event.sticky_modifiers = I.sticky_modifiers;
487 I.sticky_modifiers = 0;
489 var combo = format_key_combo(event);
490 I.key_sequence.push(combo);
494 // keypress_hook is used, for example, by key aliases
495 if (keypress_hook.run(window, I, true_event))
499 state.override_keymap ||
500 window.buffers.current.keymap;
503 state.active_keymap ||
506 var overlay_keymap = I.overlay_keymap;
509 (overlay_keymap && lookup_key_binding(overlay_keymap, combo, event)) ||
510 lookup_key_binding(active_keymap, combo, event);
512 // Should we stop this event from being processed by the gui?
514 // 1) we have a binding, and the binding's fallthrough property is not
517 // 2) we are in the middle of a key sequence, and we need to say that
518 // the key sequence given has no command.
520 if (!binding || !binding.fallthrough) {
521 true_event.preventDefault();
522 true_event.stopPropagation();
525 // Finally, process the binding.
527 if (binding.browser_object != null)
528 I.binding_browser_object = binding.browser_object;
529 if (binding.keymap) {
530 state.active_keymap = binding.keymap;
531 show_partial_key_sequence(window, state, I);
532 // We're going for another round
534 } else if (binding.command) {
535 let command = binding.command;
536 if (I.repeat == command)
537 command = binding.repeat;
538 call_interactively(I, command);
539 if (typeof(command) == "string" &&
540 interactive_commands.get(command).prefix)
542 state.active_keymap = null;
543 show_partial_key_sequence(window, state, I);
550 window.minibuffer.message(I.key_sequence.join(" ") + " is undefined");
553 // Clean up if we're done
555 if (state.help_timer_ID != null) {
556 window.clearTimeout(state.help_timer_ID);
557 state.help_timer_ID = null;
559 state.help_displayed = false;
560 state.active_keymap = null;
561 state.current_context = null;
563 } catch (e) { dump_error(e); }
566 function keyboard (window) {
567 this.window = window;
570 keyboard.prototype = {
571 last_key_down_event : null,
572 current_context : null,
573 active_keymap : null,
574 help_timer_ID : null,
575 help_displayed : false,
577 /* If this is non-null, it is used instead of the current buffer's
579 override_keymap : null,
581 set_override_keymap : function (keymap) {
582 /* Clear out any in-progress key sequence. */
583 this.active_keymap = null;
584 this.current_context = null;
585 if (this.help_timer_ID != null) {
586 this.window.clearTimeout(this.help_timer_ID);
587 this.help_timer_ID = null;
589 this.override_keymap = keymap;
594 function keyboard_initialize_window (window) {
595 window.keyboard = new keyboard(window);
596 window.addEventListener("keypress", keypress_handler, true /* capture */,
597 false /* ignore untrusted events */);
600 add_hook("window_initialize_hook", keyboard_initialize_window);
602 function for_each_key_binding (keymap_or_buffer, callback) {
604 if (keymap_or_buffer instanceof conkeror.buffer) {
605 var buffer = keymap_or_buffer;
606 var window = buffer.window;
607 keymap = window.keyboard.override_keymap || buffer.keymap;
609 keymap = keymap_or_buffer;
611 var keymap_stack = [keymap];
612 var binding_stack = [];
613 function helper2 (bind) {
614 binding_stack.push(bind);
615 callback(binding_stack);
616 if (bind.keymap && keymap_stack.indexOf(bind.keymap) == -1) {
617 keymap_stack.push(bind.keymap);
625 var keymap = keymap_stack[keymap_stack.length - 1];
626 for (var i in keymap.bindings) {
627 var b = keymap.bindings[i];
630 for (i in keymap.predicate_bindings) {
631 var bind = keymap.predicate_bindings[i];
634 if (p == match_any_key)
638 keymap_stack[keymap_stack.length - 1] = keymap.parent;
646 function find_command_in_keymap (keymap_or_buffer, command) {
649 for_each_key_binding(keymap_or_buffer, function (bind_seq) {
650 var bind = bind_seq[bind_seq.length - 1];
651 if (bind.command == command)
652 list.push(format_binding_sequence(bind_seq));
657 define_keymap("key_binding_reader_keymap");
658 define_key(key_binding_reader_keymap, match_any_key, "read-key-binding-key");
660 define_keywords("$buffer", "$keymap");
661 function key_binding_reader (continuation) {
662 keywords(arguments, $prompt = "Describe key:");
664 this.continuation = continuation;
666 if (arguments.$keymap)
667 this.target_keymap = arguments.$keymap;
669 var buffer = arguments.$buffer;
670 var window = buffer.window;
671 this.target_keymap = window.keyboard.override_keymap || buffer.keymap;
674 this.key_sequence = [];
676 minibuffer_input_state.call(this, key_binding_reader_keymap, arguments.$prompt);
678 key_binding_reader.prototype = {
679 __proto__: minibuffer_input_state.prototype,
680 destroy: function () {
681 if (this.continuation)
682 this.continuation.throw(abort());
686 function invalid_key_binding (seq) {
687 var e = new Error(seq.join(" ") + " is undefined");
688 e.key_sequence = seq;
689 e.__proto__ = invalid_key_binding.prototype;
692 invalid_key_binding.prototype = {
693 __proto__: interactive_error.prototype
696 function read_key_binding_key (window, state, event) {
697 var combo = format_key_combo(event);
698 var binding = lookup_key_binding(state.target_keymap, combo, event);
700 state.key_sequence.push(combo);
702 if (binding == null) {
703 var c = state.continuation;
704 delete state.continuation;
705 window.minibuffer.pop_state();
706 c.throw(invalid_key_binding(state.key_sequence));
710 if (binding.keymap) {
711 window.minibuffer._restore_normal_state();
712 window.minibuffer._input_text = state.key_sequence.join(" ") + " ";
713 state.target_keymap = binding.keymap;
717 var c = state.continuation;
718 delete state.continuation;
720 window.minibuffer.pop_state();
723 c([state.key_sequence, binding]);
725 interactive("read-key-binding-key", null, function (I) {
726 read_key_binding_key(I.window, I.minibuffer.check_state(key_binding_reader), I.event);
729 minibuffer.prototype.read_key_binding = function () {
731 var s = new key_binding_reader((yield CONTINUATION), forward_keywords(arguments));
733 var result = yield SUSPEND;
734 yield co_return(result);