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 /* 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;
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;
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 &&
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(
64 return (event.keyCode &&
65 event.charCode == 0 &&
68 function (event) { event.altKey = true; });
69 modifier_order = ['C', 'M', 'A', 'S'];
71 modifiers.M = modifiers.A;
80 define_keywords("$parent", "$help", "$name", "$anonymous");
84 this.parent = arguments.$parent;
86 this.predicate_bindings = [];
87 this.help = arguments.$help;
88 this.name = arguments.$name;
89 this.anonymous = arguments.$anonymous;
92 function define_keymap(name) {
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.
108 function match_any_key (event)
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.
120 return event.charCode
124 && !event.sticky_modifiers;
125 } catch (e) {return false; }
132 function format_key_spec(key) {
133 if (key.match_function) {
134 if (key.match_function == match_any_key)
136 if (key.match_function == match_any_unmodified_key)
137 return "<any-unmodified-key>";
138 return "<match-function>";
143 function format_binding_sequence(seq) {
144 return seq.map(function (x) {
145 return format_key_spec(x.key);
150 function lookup_key_binding(kmap, combo, event)
153 // Check if the key matches the keycode table
154 // var mods = get_modifiers(event);
155 var bindings = kmap.bindings;
157 if ((bind = bindings[combo]) != null)
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];
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.
186 define_keywords("$fallthrough", "$repeat");
187 function define_key_internal(ref, kmap, keys, new_command, new_keymap)
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) {
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;
205 throw new Error("Key sequence has a non-keymap in prefix");
210 function make_binding () {
213 fallthrough: args.$fallthrough,
214 command: new_command,
216 source_code_reference: ref,
217 repeat: args.$repeat,
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;
227 source_code_reference: ref,
233 for (var i = 0; i < keys.length; ++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]);
247 if (!final_binding && parent_kmap) {
248 var parent_pred_binds = parent_kmap.predicate_bindings;
250 for (j = 0; j < parent_pred_binds.length; j++) {
251 if (parent_pred_binds[j].key == key &&
252 parent_pred_binds[j].keymap)
254 parent_kmap = parent_pred_binds[j].keymap;
259 // Not already present, must be added
260 pred_binds.push(make_binding());
262 var bindings = kmap.bindings;
263 var binding = bindings[key];
266 replace_binding(binding);
270 if (!final_binding) {
271 let temp_parent = parent_kmap;
273 while (temp_parent) {
274 let p_bindings = temp_parent.bindings;
275 let p_binding = p_bindings[key];
276 if (p_binding && p_binding.keymap) {
277 parent_kmap = p_binding.keymap;
280 temp_parent = temp_parent.parent;
285 bindings[key] = make_binding();
290 // bind key to either the keymap or command in the keymap, kmap
293 // $fallthrough: (bool) let this key be process by the web page
296 // $repeat: (commnand name) shortcut command to call if a prefix
297 // command key is pressed twice in a row.
299 define_keywords("$fallthrough", "$repeat");
300 function define_key(kmap, keys, cmd)
303 var orig_keys = keys;
305 var ref = get_caller_source_code_reference();
307 if (typeof(keys) == "string" && keys.length > 1)
308 keys = keys.split(" ");
310 if (!(typeof(keys) == "object") || !(keys instanceof Array))
313 // normalize the order of modifiers in string key combos
316 if (typeof(k) == "string")
317 return format_key_combo(unformat_key_combo(k));
322 var new_command = null, new_keymap = null;
323 if (typeof(cmd) == "string" || typeof(cmd) == "function")
325 else if (cmd instanceof keymap)
327 else if (cmd != null)
328 throw new Error("Invalid `cmd' argument: " + cmd);
330 define_key_internal(ref, kmap, keys, new_command, new_keymap,
331 forward_keywords(arguments));
333 } catch (e if (typeof(e) == "string")) {
334 dumpln("Warning: Error occurred while binding keys: " + orig_keys);
345 define_variable("keyboard_key_sequence_help_timeout", 0,
346 "Delay (in millseconds) before the current key sequence "+
347 "prefix is displayed in the minibuffer.");
349 define_window_local_hook("keypress_hook", RUN_HOOK_UNTIL_SUCCESS);
353 function copy_event (event) {
355 ev.keyCode = event.keyCode;
356 ev.charCode = event.charCode;
357 ev.ctrlKey = event.ctrlKey;
358 ev.metaKey = event.metaKey;
359 ev.altKey = event.altKey;
360 ev.shiftKey = event.shiftKey;
361 ev.sticky_modifiers = event.sticky_modifiers;
365 function show_partial_key_sequence (window, state, ctx) {
366 if (!state.help_displayed)
368 state.help_timer_ID = window.setTimeout(
370 window.minibuffer.show(ctx.key_sequence.join(" "));
371 state.help_displayed = true;
372 state.help_timer_ID = null;
373 }, keyboard_key_sequence_help_timeout);
376 window.minibuffer.show(ctx.key_sequence.join(" "));
379 function format_key_combo (event) {
381 for each (var M in modifier_order) {
382 if (modifiers[M].in_event_p(event) ||
383 (event.sticky_modifiers &&
384 event.sticky_modifiers.indexOf(M) != -1))
389 if (event.charCode) {
390 if (event.charCode == 32) {
393 combo += String.fromCharCode(event.charCode);
395 } else if (event.keyCode) {
396 combo += keycode_to_vk_name[event.keyCode];
401 function unformat_key_combo (combo) {
412 while (combo[i+1] == '-') {
414 modifiers[M].set_in_event(event);
417 var key = combo.substring(i);
418 if (key.length == 1) {
419 event.charCode = key.charCodeAt(0);
420 } else if (key == 'space') {
423 event.keyCode = vk_name_to_keycode[key];
428 function keypress_handler (true_event) {
431 var state = window.keyboard;
433 var event = copy_event(true_event);
435 /* Filter out events from keys like the Windows/Super/Hyper key */
436 if (event.keyCode == 0 && event.charCode == 0)
439 /* Clear minibuffer message */
440 window.minibuffer.clear();
443 var done = true; // flag for end of key sequence
446 if (!state.current_context)
447 ctx = state.current_context = { window: window, key_sequence: [], sticky_modifiers: 0 };
449 ctx = state.current_context;
451 event.sticky_modifiers = ctx.sticky_modifiers;
452 ctx.sticky_modifiers = 0;
454 var combo = format_key_combo(event);
458 // keypress_hook is used, for example, by key aliases
459 if (keypress_hook.run(window, ctx, true_event))
463 state.override_keymap ||
464 window.buffers.current.keymap;
467 state.active_keymap ||
470 var overlay_keymap = ctx.overlay_keymap;
473 (overlay_keymap && lookup_key_binding(overlay_keymap, combo, event)) ||
474 lookup_key_binding(active_keymap, combo, event);
476 // Should we stop this event from being processed by the gui?
478 // 1) we have a binding, and the binding's fallthrough property is not
481 // 2) we are in the middle of a key sequence, and we need to say that
482 // the key sequence given has no command.
484 if (!binding || !binding.fallthrough)
486 true_event.preventDefault();
487 true_event.stopPropagation();
490 // Finally, process the binding.
491 ctx.key_sequence.push(combo);
493 if (binding.keymap) {
494 state.active_keymap = binding.keymap;
495 show_partial_key_sequence(window, state, ctx);
496 // We're going for another round
498 } else if (binding.command) {
499 let command = binding.command;
500 if (ctx.repeat == command)
501 command = binding.repeat;
502 call_interactively(ctx, command);
503 if (typeof(command) == "string" &&
504 interactive_commands.get(command).prefix)
506 state.active_keymap = null;
507 show_partial_key_sequence(window, state, ctx);
509 ctx.repeat = command;
514 window.minibuffer.message(ctx.key_sequence.join(" ") + " is undefined");
517 // Clean up if we're done
520 if (state.help_timer_ID != null)
522 window.clearTimeout(state.help_timer_ID);
523 state.help_timer_ID = null;
525 state.help_displayed = false;
526 state.active_keymap = null;
527 state.current_context = null;
529 } catch(e) { dump_error(e);}
532 function keyboard(window)
534 this.window = window;
537 keyboard.prototype = {
538 last_key_down_event : null,
539 current_context : null,
540 active_keymap : null,
541 help_timer_ID : null,
542 help_displayed : false,
544 /* If this is non-null, it is used instead of the current buffer's
546 override_keymap : null,
548 set_override_keymap : function (keymap) {
549 /* Clear out any in-progress key sequence. */
550 this.active_keymap = null;
551 this.current_context = null;
552 if (this.help_timer_ID != null)
554 this.window.clearTimeout(this.help_timer_ID);
555 this.help_timer_ID = null;
557 this.override_keymap = keymap;
562 function keyboard_initialize_window(window)
564 window.keyboard = new keyboard(window);
566 window.addEventListener ("keypress", keypress_handler, true /* capture */,
567 false /* ignore untrusted events */);
570 add_hook("window_initialize_hook", keyboard_initialize_window);
572 function for_each_key_binding(keymap_or_buffer, callback) {
574 if (keymap_or_buffer instanceof conkeror.buffer) {
575 var buffer = keymap_or_buffer;
576 var window = buffer.window;
577 keymap = window.keyboard.override_keymap || buffer.keymap;
579 keymap = keymap_or_buffer;
581 var keymap_stack = [keymap];
582 var binding_stack = [];
583 function helper2(bind) {
584 binding_stack.push(bind);
585 callback(binding_stack);
586 if (bind.keymap && keymap_stack.indexOf(bind.keymap) == -1) {
587 keymap_stack.push(bind.keymap);
595 var keymap = keymap_stack[keymap_stack.length - 1];
596 for (var i in keymap.bindings) {
597 var b = keymap.bindings[i];
600 for (i in keymap.predicate_bindings) {
601 var bind = keymap.predicate_bindings[i];
604 if (p == match_any_key)
608 keymap_stack[keymap_stack.length - 1] = keymap.parent;
616 function find_command_in_keymap(keymap_or_buffer, command) {
619 for_each_key_binding(keymap_or_buffer, function (bind_seq) {
620 var bind = bind_seq[bind_seq.length - 1];
621 if (bind.command == command)
622 list.push(format_binding_sequence(bind_seq));
627 define_keymap("key_binding_reader_keymap");
628 define_key(key_binding_reader_keymap, match_any_key, "read-key-binding-key");
630 define_keywords("$buffer", "$keymap");
631 function key_binding_reader(continuation) {
632 keywords(arguments, $prompt = "Describe key:");
634 this.continuation = continuation;
636 if (arguments.$keymap)
637 this.target_keymap = arguments.$keymap;
639 var buffer = arguments.$buffer;
640 var window = buffer.window;
641 this.target_keymap = window.keyboard.override_keymap || buffer.keymap;
644 this.key_sequence = [];
646 minibuffer_input_state.call(this, key_binding_reader_keymap, arguments.$prompt);
648 key_binding_reader.prototype = {
649 __proto__: minibuffer_input_state.prototype,
650 destroy: function () {
651 if (this.continuation)
652 this.continuation.throw(abort());
656 function invalid_key_binding(seq) {
657 var e = new Error(seq.join(" ") + " is undefined");
658 e.key_sequence = seq;
659 e.__proto__ = invalid_key_binding.prototype;
662 invalid_key_binding.prototype = {
663 __proto__: interactive_error.prototype
666 function read_key_binding_key(window, state, event) {
667 var combo = format_key_combo(event);
668 var binding = lookup_key_binding(state.target_keymap, combo, event);
670 state.key_sequence.push(combo);
672 if (binding == null) {
673 var c = state.continuation;
674 delete state.continuation;
675 window.minibuffer.pop_state();
676 c.throw(invalid_key_binding(state.key_sequence));
680 if (binding.keymap) {
681 window.minibuffer._restore_normal_state();
682 window.minibuffer._input_text = state.key_sequence.join(" ") + " ";
683 state.target_keymap = binding.keymap;
687 var c = state.continuation;
688 delete state.continuation;
690 window.minibuffer.pop_state();
693 c([state.key_sequence, binding]);
695 interactive("read-key-binding-key", null, function (I) {
696 read_key_binding_key(I.window, I.minibuffer.check_state(key_binding_reader), I.event);
699 minibuffer.prototype.read_key_binding = function () {
701 var s = new key_binding_reader((yield CONTINUATION), forward_keywords(arguments));
703 var result = yield SUSPEND;
704 yield co_return(result);