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 && parent_kmap) {
271 var p_bindings = parent_kmap.bindings;
273 var p_binding = p_bindings[key];
274 if (p_binding && p_binding.keymap)
275 parent_kmap = p_binding.keymap;
278 bindings[key] = make_binding();
283 // bind key to either the keymap or command in the keymap, kmap
286 // $fallthrough: (bool) let this key be process by the web page
289 // $repeat: (commnand name) shortcut command to call if a prefix
290 // command key is pressed twice in a row.
292 define_keywords("$fallthrough", "$repeat");
293 function define_key(kmap, keys, cmd)
296 var orig_keys = keys;
298 var ref = get_caller_source_code_reference();
300 if (typeof(keys) == "string" && keys.length > 1)
301 keys = keys.split(" ");
303 if (!(typeof(keys) == "object") || !(keys instanceof Array))
306 // normalize the order of modifiers in string key combos
309 if (typeof(k) == "string")
310 return format_key_combo(unformat_key_combo(k));
315 var new_command = null, new_keymap = null;
316 if (typeof(cmd) == "string" || typeof(cmd) == "function")
318 else if (cmd instanceof keymap)
320 else if (cmd != null)
321 throw new Error("Invalid `cmd' argument: " + cmd);
323 define_key_internal(ref, kmap, keys, new_command, new_keymap,
324 forward_keywords(arguments));
326 } catch (e if (typeof(e) == "string")) {
327 dumpln("Warning: Error occurred while binding keys: " + orig_keys);
338 define_variable("keyboard_key_sequence_help_timeout", 0,
339 "Delay (in millseconds) before the current key sequence "+
340 "prefix is displayed in the minibuffer.");
342 define_window_local_hook("keypress_hook", RUN_HOOK_UNTIL_SUCCESS);
346 function copy_event (event) {
348 ev.keyCode = event.keyCode;
349 ev.charCode = event.charCode;
350 ev.ctrlKey = event.ctrlKey;
351 ev.metaKey = event.metaKey;
352 ev.altKey = event.altKey;
353 ev.shiftKey = event.shiftKey;
354 ev.sticky_modifiers = event.sticky_modifiers;
358 function show_partial_key_sequence (window, state, ctx) {
359 if (!state.help_displayed)
361 state.help_timer_ID = window.setTimeout(
363 window.minibuffer.show(ctx.key_sequence.join(" "));
364 state.help_displayed = true;
365 state.help_timer_ID = null;
366 }, keyboard_key_sequence_help_timeout);
369 window.minibuffer.show(ctx.key_sequence.join(" "));
372 function format_key_combo (event) {
374 for each (var M in modifier_order) {
375 if (modifiers[M].in_event_p(event) ||
376 (event.sticky_modifiers &&
377 event.sticky_modifiers.indexOf(M) != -1))
382 if (event.charCode) {
383 if (event.charCode == 32) {
386 combo += String.fromCharCode(event.charCode);
388 } else if (event.keyCode) {
389 combo += keycode_to_vk_name[event.keyCode];
394 function unformat_key_combo (combo) {
405 while (combo[i+1] == '-') {
407 modifiers[M].set_in_event(event);
410 var key = combo.substring(i);
411 if (key.length == 1) {
412 event.charCode = key.charCodeAt(0);
413 } else if (key == 'space') {
416 event.keyCode = vk_name_to_keycode[key];
421 function keypress_handler (true_event) {
424 var state = window.keyboard;
426 var event = copy_event(true_event);
428 /* Filter out events from keys like the Windows/Super/Hyper key */
429 if (event.keyCode == 0 && event.charCode == 0)
432 /* Clear minibuffer message */
433 window.minibuffer.clear();
436 var done = true; // flag for end of key sequence
439 if (!state.current_context)
440 ctx = state.current_context = { window: window, key_sequence: [], sticky_modifiers: 0 };
442 ctx = state.current_context;
444 event.sticky_modifiers = ctx.sticky_modifiers;
445 ctx.sticky_modifiers = 0;
447 var combo = format_key_combo(event);
451 // keypress_hook is used, for example, by key aliases
452 if (keypress_hook.run(window, ctx, true_event))
456 state.override_keymap ||
457 window.buffers.current.keymap;
460 state.active_keymap ||
463 var overlay_keymap = ctx.overlay_keymap;
466 (overlay_keymap && lookup_key_binding(overlay_keymap, combo, event)) ||
467 lookup_key_binding(active_keymap, combo, event);
469 // Should we stop this event from being processed by the gui?
471 // 1) we have a binding, and the binding's fallthrough property is not
474 // 2) we are in the middle of a key sequence, and we need to say that
475 // the key sequence given has no command.
477 if (!binding || !binding.fallthrough)
479 true_event.preventDefault();
480 true_event.stopPropagation();
483 // Finally, process the binding.
484 ctx.key_sequence.push(combo);
486 if (binding.keymap) {
487 state.active_keymap = binding.keymap;
488 show_partial_key_sequence(window, state, ctx);
489 // We're going for another round
491 } else if (binding.command) {
492 let command = binding.command;
493 if (ctx.repeat == command)
494 command = binding.repeat;
495 call_interactively(ctx, command);
496 if (typeof(command) == "string" &&
497 interactive_commands.get(command).prefix)
499 state.active_keymap = null;
500 show_partial_key_sequence(window, state, ctx);
502 ctx.repeat = command;
507 window.minibuffer.message(ctx.key_sequence.join(" ") + " is undefined");
510 // Clean up if we're done
513 if (state.help_timer_ID != null)
515 window.clearTimeout(state.help_timer_ID);
516 state.help_timer_ID = null;
518 state.help_displayed = false;
519 state.active_keymap = null;
520 state.current_context = null;
522 } catch(e) { dump_error(e);}
525 function keyboard(window)
527 this.window = window;
530 keyboard.prototype = {
531 last_key_down_event : null,
532 current_context : null,
533 active_keymap : null,
534 help_timer_ID : null,
535 help_displayed : false,
537 /* If this is non-null, it is used instead of the current buffer's
539 override_keymap : null,
541 set_override_keymap : function (keymap) {
542 /* Clear out any in-progress key sequence. */
543 this.active_keymap = null;
544 this.current_context = null;
545 if (this.help_timer_ID != null)
547 this.window.clearTimeout(this.help_timer_ID);
548 this.help_timer_ID = null;
550 this.override_keymap = keymap;
555 function keyboard_initialize_window(window)
557 window.keyboard = new keyboard(window);
559 window.addEventListener ("keypress", keypress_handler, true /* capture */,
560 false /* ignore untrusted events */);
563 add_hook("window_initialize_hook", keyboard_initialize_window);
565 function for_each_key_binding(keymap_or_buffer, callback) {
567 if (keymap_or_buffer instanceof conkeror.buffer) {
568 var buffer = keymap_or_buffer;
569 var window = buffer.window;
570 keymap = window.keyboard.override_keymap || buffer.keymap;
572 keymap = keymap_or_buffer;
574 var keymap_stack = [keymap];
575 var binding_stack = [];
576 function helper2(bind) {
577 binding_stack.push(bind);
578 callback(binding_stack);
579 if (bind.keymap && keymap_stack.indexOf(bind.keymap) == -1) {
580 keymap_stack.push(bind.keymap);
588 var keymap = keymap_stack[keymap_stack.length - 1];
589 for (var i in keymap.bindings) {
590 var b = keymap.bindings[i];
593 for (i in keymap.predicate_bindings) {
594 var bind = keymap.predicate_bindings[i];
597 if (p == match_any_key)
601 keymap_stack[keymap_stack.length - 1] = keymap.parent;
609 function find_command_in_keymap(keymap_or_buffer, command) {
612 for_each_key_binding(keymap_or_buffer, function (bind_seq) {
613 var bind = bind_seq[bind_seq.length - 1];
614 if (bind.command == command)
615 list.push(format_binding_sequence(bind_seq));
620 define_keymap("key_binding_reader_keymap");
621 define_key(key_binding_reader_keymap, match_any_key, "read-key-binding-key");
623 define_keywords("$buffer", "$keymap");
624 function key_binding_reader(continuation) {
625 keywords(arguments, $prompt = "Describe key:");
627 this.continuation = continuation;
629 if (arguments.$keymap)
630 this.target_keymap = arguments.$keymap;
632 var buffer = arguments.$buffer;
633 var window = buffer.window;
634 this.target_keymap = window.keyboard.override_keymap || buffer.keymap;
637 this.key_sequence = [];
639 minibuffer_input_state.call(this, key_binding_reader_keymap, arguments.$prompt);
641 key_binding_reader.prototype = {
642 __proto__: minibuffer_input_state.prototype,
643 destroy: function () {
644 if (this.continuation)
645 this.continuation.throw(abort());
649 function invalid_key_binding(seq) {
650 var e = new Error(seq.join(" ") + " is undefined");
651 e.key_sequence = seq;
652 e.__proto__ = invalid_key_binding.prototype;
655 invalid_key_binding.prototype = {
656 __proto__: interactive_error.prototype
659 function read_key_binding_key(window, state, event) {
660 var combo = format_key_combo(event);
661 var binding = lookup_key_binding(state.target_keymap, combo, event);
663 state.key_sequence.push(combo);
665 if (binding == null) {
666 var c = state.continuation;
667 delete state.continuation;
668 window.minibuffer.pop_state();
669 c.throw(invalid_key_binding(state.key_sequence));
673 if (binding.keymap) {
674 window.minibuffer._restore_normal_state();
675 window.minibuffer._input_text = state.key_sequence.join(" ") + " ";
676 state.target_keymap = binding.keymap;
680 var c = state.continuation;
681 delete state.continuation;
683 window.minibuffer.pop_state();
686 c([state.key_sequence, binding]);
688 interactive("read-key-binding-key", null, function (I) {
689 read_key_binding_key(I.window, I.minibuffer.check_state(key_binding_reader), I.event);
692 minibuffer.prototype.read_key_binding = function () {
694 var s = new key_binding_reader((yield CONTINUATION), forward_keywords(arguments));
696 var result = yield SUSPEND;
697 yield co_return(result);