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 function get_default_keycode_to_charcode_tables() {
15 const KeyEvent = Ci.nsIDOMKeyEvent;
17 var unshifted_table = [];
18 var shifted_table = [];
19 for (var i = 0; i < 26; ++i) {
20 var keycode = KeyEvent.DOM_VK_A + i;
21 var charcode = keycode; // keycodes A-Z are same as ascii
22 shifted_table[keycode] = charcode;
24 unshifted_table[keycode] = "a".charCodeAt(0) + i;
27 for (var i = 0; i <= 9; ++i) {
28 var keycode = KeyEvent.DOM_VK_0 + i;
29 var numpad_keycode = KeyEvent.DOM_VK_NUMPAD0 + i;
30 var charcode = keycode; // keycodes 0-9 are same as ascii
31 unshifted_table[keycode] = charcode;
32 unshifted_table[numpad_keycode] = charcode;
35 function map(table, keycode, str) {
36 table[keycode] = str.charCodeAt(0);
39 map(unshifted_table, KeyEvent.DOM_VK_BACK_SLASH, "\\");
40 map(unshifted_table, KeyEvent.DOM_VK_OPEN_BRACKET, "[");
41 map(unshifted_table, KeyEvent.DOM_VK_CLOSE_BRACKET, "]");
42 map(unshifted_table, KeyEvent.DOM_VK_SEMICOLON, ";");
43 map(unshifted_table, KeyEvent.DOM_VK_COMMA, ",");
44 map(unshifted_table, KeyEvent.DOM_VK_PERIOD, ".");
45 map(unshifted_table, KeyEvent.DOM_VK_SLASH, "/");
46 map(unshifted_table, KeyEvent.DOM_VK_SUBTRACT, "-");
47 map(unshifted_table, KeyEvent.DOM_VK_EQUALS, "=");
49 map(shifted_table, KeyEvent.DOM_VK_SEMICOLON, ":");
50 map(shifted_table, KeyEvent.DOM_VK_1, "!");
51 map(shifted_table, KeyEvent.DOM_VK_2, "@");
52 map(shifted_table, KeyEvent.DOM_VK_3, "#");
53 map(shifted_table, KeyEvent.DOM_VK_4, "$");
54 map(shifted_table, KeyEvent.DOM_VK_5, "%");
55 map(shifted_table, KeyEvent.DOM_VK_6, "^");
56 map(shifted_table, KeyEvent.DOM_VK_7, "&");
57 map(shifted_table, KeyEvent.DOM_VK_8, "*");
58 map(shifted_table, KeyEvent.DOM_VK_9, "(");
59 map(shifted_table, KeyEvent.DOM_VK_0, ")");
60 map(shifted_table, KeyEvent.DOM_VK_EQUALS, "+");
61 map(shifted_table, KeyEvent.DOM_VK_SUBTRACT, "_");
62 map(shifted_table, KeyEvent.DOM_VK_COMMA, "<");
63 map(shifted_table, KeyEvent.DOM_VK_PERIOD, ">");
64 map(shifted_table, KeyEvent.DOM_VK_SLASH, "?");
66 return [unshifted_table, shifted_table];
69 /* Generate vk name table */
70 var keycode_to_vk_name = [];
71 var vk_name_to_keycode = {};
73 let KeyEvent = Ci.nsIDOMKeyEvent;
74 let prefix = "DOM_VK_";
77 /* Check if this is a key binding */
78 if (i.substr(0, prefix.length) == prefix)
80 let name = i.substr(prefix.length).toLowerCase();
81 let code = KeyEvent[i];
82 keycode_to_vk_name[code] = name;
83 vk_name_to_keycode[name] = code;
88 function get_charcode_mapping_table_from_preferences()
90 var vals = conkeror.get_pref("conkeror.charCodeMappingTable");
100 var unshifted_keycode_to_charcode = null;
101 var shifted_keycode_to_charcode = null;
102 var charcode_to_keycodes = null;
104 var keycode_to_name = null;
105 var shifted_keycode_to_name = null;
106 function load_charcode_mapping_table()
108 var tables = get_charcode_mapping_table_from_preferences();
110 tables = get_default_keycode_to_charcode_tables();
111 let [unshifted_table, shifted_table] = tables;
112 unshifted_keycode_to_charcode = unshifted_table;
113 shifted_keycode_to_charcode = shifted_table;
114 charcode_to_keycodes = [];
115 for each (let table in tables) {
116 var shifted = (table == tables[1]);
117 for (let x in table) {
118 let charcode = table[x];
119 if (charcode == null)
121 var obj = charcode_to_keycodes[charcode];
123 obj = charcode_to_keycodes[charcode] = [];
124 obj[obj.length] = [x, shifted];
128 keycode_to_name = keycode_to_vk_name.slice();
129 shifted_keycode_to_name = [];
130 for (let charcode in charcode_to_keycodes) {
131 let arr = charcode_to_keycodes[charcode];
134 let [keycode, shift] = arr[0];
135 let table = shift ? shifted_keycode_to_name : keycode_to_name;
136 table[keycode] = String.fromCharCode(charcode);
139 load_charcode_mapping_table();
141 interactive("keyboard-setup", null, function (I) {
142 make_chrome_window("chrome://conkeror/content/keyboard-setup.xul");
145 command_line_handler("keyboard-setup", true, function () {
146 make_chrome_window("chrome://conkeror/content/keyboard-setup.xul");
150 var abort_key = null;
152 const MOD_CTRL = 0x1;
153 const MOD_META = 0x2;
154 const MOD_SHIFT = 0x4;
156 // Note: For elements of the modifier_names array, an element at index
157 // i should correspond to the modifier mask (1 << i).
158 var modifier_names = ["C", "M", "S"];
160 function format_key_press(code, modifiers)
165 if ((modifiers & MOD_SHIFT) && (name = shifted_keycode_to_name[code])) {
166 modifiers &= ~MOD_SHIFT;
168 name = keycode_to_name[code] || ("<" + code + ">");
172 for (var i = 0; i < modifier_names.length; ++i)
174 if (modifiers & (1 << i))
175 out = out + modifier_names[i] + "-";
182 function format_key_spec(key) {
183 if (key.match_function) {
184 if (key.match_function == match_any_key)
186 if (key.match_function == match_any_unmodified_key)
187 return "<any-unmodified-key>";
188 return "<match-function>";
190 return format_key_press(key.keyCode, key.modifiers);
193 function format_key_event(event)
195 return format_key_press(event.keyCode, get_modifiers(event));
198 function format_binding_sequence(seq) {
199 return seq.map(function (x) {
200 return format_key_spec(x.key);
204 // Key Matching Functions. These are functions that may be passed to kbd
205 // in place of key code or char code. They take an event object as their
206 // argument and turn true if the event matches the class of keys that they
209 function match_any_key (event)
214 function meta_pressed (event)
216 return event.altKey || event.metaKey;
219 function get_modifiers(event)
221 // Shift is always included in the modifiers, if it is included in
223 return (event.ctrlKey ? MOD_CTRL:0) |
224 (meta_pressed(event) ? MOD_META:0) |
225 (event.shiftKey ? MOD_SHIFT: 0) |
226 event.sticky_modifiers;
229 /* This function is no longer used for normal keymap lookups. It is
230 * only used to check if the current key matches the abort key. */
231 function match_binding(key, event)
234 && event.keyCode == key.keyCode
235 && get_modifiers(event) == key.modifiers)
236 || (key.match_function && key.match_function (event));
239 function lookup_key_binding(kmap, event)
242 // Check if the key matches the keycode table
243 var mods = get_modifiers(event);
244 var keycode_binds = kmap.keycode_bindings;
247 if ((arr = keycode_binds[event.keyCode]) != null &&
248 (bind = arr[mods]) != null)
251 // Check if the key matches a predicate
252 var pred_binds = kmap.predicate_bindings;
253 for (var i = 0; i < pred_binds.length; ++i)
255 var bind = pred_binds[i];
256 if (bind.key.match_function(event))
264 function match_any_unmodified_key (event)
267 return event.charCode
268 && !meta_pressed(event)
270 && !event.sticky_modifiers;
271 } catch (e) {return false; }
274 function kbd (spec, mods)
280 results.is_kbd = true;
282 if (typeof spec == "function")
283 results[0] = {match_function: spec};
285 else if (typeof spec == "string")
287 /* Attempt to parse a key specification. In order to allow
288 * the user to specify the "-" key literally, special case the
289 * parsing of that. */
291 if (spec.substr(spec.length - 1) == "-")
293 parts = spec.substr(0, spec.length - 1).split("-");
296 parts = spec.split("-");
297 var parsed_modifiers = 0;
298 if (parts.length > 1)
300 // Attempt to parse modifiers
301 for (var i = 0; i < parts.length - 1; ++i)
303 var k = modifier_names.indexOf(parts[i]);
307 parsed_modifiers |= mod;
310 // Attempt to lookup keycode
311 var name = parts[parts.length - 1];
314 parsed_modifiers |= mods;
316 if (name.length == 1) {
317 // Charcode, handle specially
319 var codes = charcode_to_keycodes[name.charCodeAt(0)];
321 throw "Invalid key specification: " + spec;
323 for each (let [keycode, shift] in codes) {
324 results[results.length] = {keyCode: keycode, modifiers: parsed_modifiers | (shift ? MOD_SHIFT : 0)};
327 var code = vk_name_to_keycode[name];
329 throw "Invalid key specification: " + spec;
330 results[0] = {keyCode: code, modifiers: parsed_modifiers};
334 results[0] = {keyCode: spec, modifiers: ((mods != null)? mods : 0)};
340 define_keywords("$fallthrough", "$category", "$repeat");
341 function define_key_internal(ref, kmap, keys, new_command, new_keymap)
345 var args = arguments;
347 var parent_kmap = kmap.parent;
350 for (var i = 0; i < keys.length; ++i) {
352 var final_binding = (i == keys.length - 1);
354 /* Replace `bind' with the binding specified by (cmd, fallthrough) */
355 function replace_binding(bind)
358 bind.command = new_command;
359 bind.keymap = new_keymap;
360 bind.fallthrough = args.$fallthrough;
361 bind.source_code_reference = ref;
362 bind.category = args.$category;
363 bind.repeat = args.$repeat;
366 throw new Error("Key sequence has a non-keymap in prefix");
371 function make_binding()
374 return {key: key, fallthrough: args.$fallthrough,
375 command: new_command, keymap: new_keymap,
376 source_code_reference: ref,
377 category: args.$category,
378 repeat: args.$repeat,
384 // Check for a corresponding binding a parent
385 kmap = new keymap($parent = parent_kmap);
386 kmap.bound_in = old_kmap;
387 return {key: key, keymap: kmap,
388 source_code_reference: ref,
393 // Check if the specified binding is already present in the kmap
394 if (key.match_function)
396 var pred_binds = kmap.predicate_bindings;
397 for (var i = 0; i < pred_binds.length; i++)
399 var cur_bind = pred_binds[i];
400 if (cur_bind.key.match_function == key.match_function)
402 replace_binding(cur_bind);
407 if (!final_binding && parent_kmap) {
408 var parent_pred_binds = parent_kmap.predicate_bindings;
410 for (var i = 0; i < parent_pred_binds.length; i++)
412 var cur_bind = parent_pred_binds[i];
413 if (cur_bind.key.match_function == key.match_function && cur_bind.keymap)
415 parent_kmap = cur_bind.keymap;
420 // Not already present, must be added
421 pred_binds.push(make_binding());
424 // This is a binding by keycode: look it up in the table
425 var keycode_binds = kmap.keycode_bindings;
426 var arr = keycode_binds[key.keyCode];
428 if (arr && arr[key.modifiers])
430 replace_binding(arr[key.modifiers]);
434 if (!final_binding && parent_kmap) {
435 var p_keycode_binds = parent_kmap.keycode_bindings;
437 var p_arr = p_keycode_binds[key.keyCode];
439 if (p_arr && (p_bind = p_arr[key.modifiers]) != null && p_bind.keymap)
440 parent_kmap = p_bind.keymap;
444 arr = (keycode_binds[key.keyCode] = []);
446 arr[key.modifiers] = make_binding();
451 // bind key to either the keymap or command in the keymap, kmap
452 define_keywords("$fallthrough", "$category", "$repeat");
453 function define_key(kmap, keys, cmd)
456 var orig_keys = keys;
458 var ref = get_caller_source_code_reference();
460 if (typeof(keys) == "string" && keys.length > 1)
461 keys = keys.split(" ");
463 if (!(typeof(keys) == "object") || ('is_kbd' in keys) || !(keys instanceof Array))
466 var new_command = null, new_keymap = null;
467 if (typeof(cmd) == "string" || typeof(cmd) == "function")
469 else if (cmd instanceof keymap)
471 else if (cmd != null)
472 throw new Error("Invalid `cmd' argument: " + cmd);
474 var args = arguments;
476 var input_keys = keys.map(function(x) { return kbd(x); });
478 function helper(index, output_keys) {
479 if (index == input_keys.length) {
480 define_key_internal(ref, kmap, output_keys, new_command, new_keymap,
481 forward_keywords(args));
484 var key = input_keys[index];
485 for (let i = 0; i < key.length; ++i)
486 helper(index + 1, output_keys.concat(key[i]));
490 } catch (e if (typeof(e) == "string")) {
491 dumpln("Warning: Error occurred while binding keys: " + orig_keys);
493 dumpln("This may be due to an incorrect keyboard setup.");
494 dumpln("You may want to use the -keyboard-setup command-line option to fix your keyboard setup.");
498 define_keywords("$parent", "$help", "$name");
502 /* For efficiency, a table indexed by the key code, and then by
503 * the modifiers is used to lookup key bindings, rather than
504 * looping through all bindings in the key map to find one. The
505 * array keycode_bindings is indexed by the keyCode; if the
506 * corresponding element for a keyCode is non-null, it is itself
507 * an array indexed by the result of get_modifiers (i.e. from 0 to 7).
508 * As before, match_function-based bindings are stored as a simple
509 * list, predicate_bindings. */
510 this.parent = arguments.$parent;
511 this.keycode_bindings = [];
512 this.predicate_bindings = [];
513 this.help = arguments.$help;
514 this.name = arguments.$name;
517 function define_keymap(name) {
519 this[name] = new keymap($name = name, forward_keywords(arguments));
522 function copy_event(event)
525 ev.keyCode = event.keyCode;
526 ev.charCode = event.charCode;
527 ev.ctrlKey = event.ctrlKey;
528 ev.metaKey = event.metaKey;
529 ev.altKey = event.altKey;
530 ev.shiftKey = event.shiftKey;
531 ev.sticky_modifiers = event.sticky_modifiers;
535 function key_down_handler(event)
538 //window.dumpln("key down: " + conkeror.format_key_press(event.keyCode, conkeror.get_modifiers(event)));
540 var state = window.keyboard;
541 state.last_key_down_event = copy_event(event);
542 state.last_char_code = null;
543 state.last_key_code = null;
546 define_variable("keyboard_key_sequence_help_timeout", 0,
547 "Delay (in millseconds) before the current key sequence prefix is displayed in the minibuffer.");
549 define_window_local_hook("key_press_hook", RUN_HOOK_UNTIL_SUCCESS);
551 function key_press_handler(true_event)
553 function show_partial_key_sequence (window, state, ctx) {
554 if (!state.help_displayed)
556 state.help_timer_ID = window.setTimeout(function () {
557 window.minibuffer.show(ctx.key_sequence.join(" "));
558 state.help_displayed = true;
559 state.help_timer_ID = null;
560 }, keyboard_key_sequence_help_timeout);
563 window.minibuffer.show(ctx.key_sequence.join(" "));
567 var state = window.keyboard;
569 /* ASSERT(state.last_key_down_event != null); */
571 var event = state.last_key_down_event;
572 event.charCode = true_event.charCode;
574 // If the true_event includes a keyCode, we can just use that
575 if (true_event.keyCode)
576 event.keyCode = true_event.keyCode;
578 /* Filter out events from keys like the Windows/Super/Hyper key */
579 if (event.keyCode == 0)
582 /* Clear minibuffer message */
583 window.minibuffer.clear();
589 if (!state.current_context)
590 ctx = state.current_context = { window: window, key_sequence: [], sticky_modifiers: 0 };
592 ctx = state.current_context;
594 event.sticky_modifiers = ctx.sticky_modifiers;
595 ctx.sticky_modifiers = 0;
599 if (key_press_hook.run(window, ctx, true_event))
603 state.override_keymap ||
604 window.buffers.current.keymap;
607 state.active_keymap ||
610 var overlay_keymap = ctx.overlay_keymap;
613 (overlay_keymap && lookup_key_binding(overlay_keymap, event)) ||
614 lookup_key_binding(active_keymap, event);
616 // Should we stop this event from being processed by the gui?
618 // 1) we have a binding, and the binding's fallthrough property is not
621 // 2) we are in the middle of a key sequence, and we need to say that
622 // the key sequence given has no command.
624 if (!binding || !binding.fallthrough)
626 true_event.preventDefault();
627 true_event.stopPropagation();
630 // Finally, process the binding.
631 ctx.key_sequence.push(format_key_event(event));
633 if (binding.keymap) {
634 state.active_keymap = binding.keymap;
635 show_partial_key_sequence(window, state, ctx);
636 // We're going for another round
638 } else if (binding.command) {
639 let command = binding.command;
640 if (ctx.repeat == command)
641 command = binding.repeat;
642 call_interactively(ctx, command);
643 if (typeof(command) == "string" &&
644 interactive_commands.get(command).prefix)
646 state.active_keymap = null;
647 show_partial_key_sequence(window, state, ctx);
649 ctx.repeat = command;
654 window.minibuffer.message(ctx.key_sequence.join(" ") + " is undefined");
657 // Clean up if we're done
660 if (state.help_timer_ID != null)
662 window.clearTimeout(state.help_timer_ID);
663 state.help_timer_ID = null;
665 state.help_displayed = false;
666 state.active_keymap = null;
667 state.current_context = null;
669 } catch(e) { dump_error(e);}
672 function keyboard(window)
674 this.window = window;
677 keyboard.prototype = {
678 last_key_down_event : null,
679 current_context : null,
680 active_keymap : null,
681 help_timer_ID : null,
682 help_displayed : false,
684 /* If this is non-null, it is used instead of the current buffer's
686 override_keymap : null,
688 set_override_keymap : function (keymap) {
689 /* Clear out any in-progress key sequence. */
690 this.active_keymap = null;
691 this.current_context = null;
692 if (this.help_timer_ID != null)
694 this.window.clearTimeout(this.help_timer_ID);
695 this.help_timer_ID = null;
697 this.override_keymap = keymap;
702 function keyboard_initialize_window(window)
704 window.keyboard = new keyboard(window);
706 window.addEventListener ("keydown", key_down_handler, true /* capture */,
707 false /* ignore untrusted events */);
708 window.addEventListener ("keypress", key_press_handler, true /* capture */,
709 false /* ignore untrusted events */);
712 add_hook("window_initialize_hook", keyboard_initialize_window);
714 function for_each_key_binding(keymap_or_buffer, callback) {
716 if (keymap_or_buffer instanceof conkeror.buffer) {
717 var buffer = keymap_or_buffer;
718 var window = buffer.window;
719 keymap = window.keyboard.override_keymap || buffer.keymap;
721 keymap = keymap_or_buffer;
723 var keymap_stack = [keymap];
724 var binding_stack = [];
725 function helper2(bind) {
726 binding_stack.push(bind);
727 callback(binding_stack);
728 if (bind.keymap && keymap_stack.indexOf(bind.keymap) == -1) {
729 keymap_stack.push(bind.keymap);
736 var unmodified_keys_masked = false;
737 var keycode_masks = [];
739 var keymap = keymap_stack[keymap_stack.length - 1];
740 for (var i in keymap.keycode_bindings) {
741 var b = keymap.keycode_bindings[i];
742 if (!(i in keycode_masks))
743 keycode_masks[i] = [];
745 if (unmodified_keys_masked && ((j & MOD_SHIFT) == j))
747 if (!keycode_masks[i][j]) {
749 keycode_masks[i][j] = true;
753 for (var i in keymap.predicate_bindings) {
754 var bind = keymap.predicate_bindings[i];
756 var p = bind.key.match_function;
757 if (p == match_any_key)
759 if (p == match_any_unmodified_key)
760 unmodified_keys_masked = true;
763 keymap_stack[keymap_stack.length - 1] = keymap.parent;
771 function find_command_in_keymap(keymap_or_buffer, command) {
774 for_each_key_binding(keymap_or_buffer, function (bind_seq) {
775 var bind = bind_seq[bind_seq.length - 1];
776 if (bind.command == command)
777 list.push(format_binding_sequence(bind_seq));
782 define_keymap("key_binding_reader_keymap");
783 define_key(key_binding_reader_keymap, match_any_key, "read-key-binding-key");
785 define_keywords("$buffer", "$keymap");
786 function key_binding_reader(continuation) {
787 keywords(arguments, $prompt = "Describe key:");
789 this.continuation = continuation;
791 if (arguments.$keymap)
792 this.target_keymap = arguments.$keymap;
794 var buffer = arguments.$buffer;
795 var window = buffer.window;
796 this.target_keymap = window.keyboard.override_keymap || buffer.keymap;
799 this.key_sequence = [];
801 minibuffer_input_state.call(this, key_binding_reader_keymap, arguments.$prompt);
803 key_binding_reader.prototype = {
804 __proto__: minibuffer_input_state.prototype,
805 destroy: function () {
806 if (this.continuation)
807 this.continuation.throw(abort());
811 function invalid_key_binding(seq) {
812 var e = new Error(seq.join(" ") + " is undefined");
813 e.key_sequence = seq;
814 e.__proto__ = invalid_key_binding.prototype;
817 invalid_key_binding.prototype = {
818 __proto__: interactive_error.prototype
821 function read_key_binding_key(window, state, event) {
822 var binding = lookup_key_binding(state.target_keymap, event);
824 state.key_sequence.push(format_key_event(event));
826 if (binding == null) {
827 var c = state.continuation;
828 delete state.continuation;
829 window.minibuffer.pop_state();
830 c.throw(invalid_key_binding(state.key_sequence));
834 if (binding.keymap) {
835 window.minibuffer._restore_normal_state();
836 window.minibuffer._input_text = state.key_sequence.join(" ") + " ";
837 state.target_keymap = binding.keymap;
841 var c = state.continuation;
842 delete state.continuation;
844 window.minibuffer.pop_state();
847 c([state.key_sequence, binding]);
849 interactive("read-key-binding-key", null, function (I) {
850 read_key_binding_key(I.window, I.minibuffer.check_state(key_binding_reader), I.event);
853 minibuffer.prototype.read_key_binding = function () {
855 var s = new key_binding_reader((yield CONTINUATION), forward_keywords(arguments));
857 var result = yield SUSPEND;
858 yield co_return(result);