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 charcode = keycode; // keycodes 0-9 are same as ascii
30 unshifted_table[keycode] = charcode;
33 function map(table, keycode, str) {
34 table[keycode] = str.charCodeAt(0);
37 map(unshifted_table, KeyEvent.DOM_VK_BACK_SLASH, "\\");
38 map(unshifted_table, KeyEvent.DOM_VK_OPEN_BRACKET, "[");
39 map(unshifted_table, KeyEvent.DOM_VK_CLOSE_BRACKET, "]");
40 map(unshifted_table, KeyEvent.DOM_VK_SEMICOLON, ";");
41 map(unshifted_table, KeyEvent.DOM_VK_COMMA, ",");
42 map(unshifted_table, KeyEvent.DOM_VK_PERIOD, ".");
43 map(unshifted_table, KeyEvent.DOM_VK_SLASH, "/");
44 map(unshifted_table, KeyEvent.DOM_VK_SUBTRACT, "-");
45 map(unshifted_table, KeyEvent.DOM_VK_EQUALS, "=");
47 map(shifted_table, KeyEvent.DOM_VK_SEMICOLON, ":");
48 map(shifted_table, KeyEvent.DOM_VK_1, "!");
49 map(shifted_table, KeyEvent.DOM_VK_2, "@");
50 map(shifted_table, KeyEvent.DOM_VK_3, "#");
51 map(shifted_table, KeyEvent.DOM_VK_4, "$");
52 map(shifted_table, KeyEvent.DOM_VK_5, "%");
53 map(shifted_table, KeyEvent.DOM_VK_6, "^");
54 map(shifted_table, KeyEvent.DOM_VK_7, "&");
55 map(shifted_table, KeyEvent.DOM_VK_8, "*");
56 map(shifted_table, KeyEvent.DOM_VK_9, "(");
57 map(shifted_table, KeyEvent.DOM_VK_0, ")");
58 map(shifted_table, KeyEvent.DOM_VK_EQUALS, "+");
59 map(shifted_table, KeyEvent.DOM_VK_SUBTRACT, "_");
60 map(shifted_table, KeyEvent.DOM_VK_COMMA, "<");
61 map(shifted_table, KeyEvent.DOM_VK_PERIOD, ">");
62 map(shifted_table, KeyEvent.DOM_VK_SLASH, "?");
64 return [unshifted_table, shifted_table];
67 /* Generate vk name table */
68 var keycode_to_vk_name = [];
69 var vk_name_to_keycode = {};
71 let KeyEvent = Ci.nsIDOMKeyEvent;
72 let prefix = "DOM_VK_";
75 /* Check if this is a key binding */
76 if (i.substr(0, prefix.length) == prefix)
78 let name = i.substr(prefix.length).toLowerCase();
79 let code = KeyEvent[i];
80 keycode_to_vk_name[code] = name;
81 vk_name_to_keycode[name] = code;
86 function get_charcode_mapping_table_from_preferences()
88 var vals = conkeror.get_pref("conkeror.charCodeMappingTable");
98 var unshifted_keycode_to_charcode = null;
99 var shifted_keycode_to_charcode = null;
100 var charcode_to_keycodes = null;
102 var keycode_to_name = null;
103 var shifted_keycode_to_name = null;
104 function load_charcode_mapping_table()
106 var tables = get_charcode_mapping_table_from_preferences();
108 tables = get_default_keycode_to_charcode_tables();
109 let [unshifted_table, shifted_table] = tables;
110 unshifted_keycode_to_charcode = unshifted_table;
111 shifted_keycode_to_charcode = shifted_table;
112 charcode_to_keycodes = [];
113 for each (let table in tables) {
114 var shifted = (table == tables[1]);
115 for (let x in table) {
116 let charcode = table[x];
117 if (charcode == null)
119 var obj = charcode_to_keycodes[charcode];
121 obj = charcode_to_keycodes[charcode] = [];
122 obj[obj.length] = [x, shifted];
126 keycode_to_name = keycode_to_vk_name.slice();
127 shifted_keycode_to_name = [];
128 for (let charcode in charcode_to_keycodes) {
129 let arr = charcode_to_keycodes[charcode];
132 let [keycode, shift] = arr[0];
133 let table = shift ? shifted_keycode_to_name : keycode_to_name;
134 table[keycode] = String.fromCharCode(charcode);
137 load_charcode_mapping_table();
139 interactive("keyboard-setup", null, function (I) {
140 make_chrome_window("chrome://conkeror/content/keyboard-setup.xul");
143 command_line_handler("keyboard-setup", true, function () {
144 make_chrome_window("chrome://conkeror/content/keyboard-setup.xul");
148 var abort_key = null;
150 const MOD_CTRL = 0x1;
151 const MOD_META = 0x2;
152 const MOD_SHIFT = 0x4;
154 // Note: For elements of the modifier_names array, an element at index
155 // i should correspond to the modifier mask (1 << i).
156 var modifier_names = ["C", "M", "S"];
158 function format_key_press(code, modifiers)
163 if ((modifiers & MOD_SHIFT) && (name = shifted_keycode_to_name[code])) {
164 modifiers &= ~MOD_SHIFT;
166 name = keycode_to_name[code] || ("<" + code + ">");
170 for (var i = 0; i < modifier_names.length; ++i)
172 if (modifiers & (1 << i))
173 out = out + modifier_names[i] + "-";
180 function format_key_spec(key) {
181 if (key.match_function) {
182 if (key.match_function == match_any_key)
184 if (key.match_function == match_any_unmodified_key)
185 return "<any-unmodified-key>";
186 return "<match-function>";
188 return format_key_press(key.keyCode, key.modifiers);
191 function format_key_event(event)
193 return format_key_press(event.keyCode, get_modifiers(event));
196 function format_binding_sequence(seq) {
197 return seq.map(function (x) {
198 return format_key_spec(x.key);
202 // Key Matching Functions. These are functions that may be passed to kbd
203 // in place of key code or char code. They take an event object as their
204 // argument and turn true if the event matches the class of keys that they
207 function match_any_key (event)
212 function meta_pressed (event)
214 return event.altKey || event.metaKey;
217 function get_modifiers(event)
219 // Shift is always included in the modifiers, if it is included in
221 return (event.ctrlKey ? MOD_CTRL:0) |
222 (meta_pressed(event) ? MOD_META:0) |
223 (event.shiftKey ? MOD_SHIFT: 0) |
224 event.sticky_modifiers;
227 /* This function is no longer used for normal keymap lookups. It is
228 * only used to check if the current key matches the abort key. */
229 function match_binding(key, event)
232 && event.keyCode == key.keyCode
233 && get_modifiers(event) == key.modifiers)
234 || (key.match_function && key.match_function (event));
237 function lookup_key_binding(kmap, event)
240 // Check if the key matches the keycode table
241 var mods = get_modifiers(event);
242 var keycode_binds = kmap.keycode_bindings;
245 if ((arr = keycode_binds[event.keyCode]) != null &&
246 (bind = arr[mods]) != null)
249 // Check if the key matches a predicate
250 var pred_binds = kmap.predicate_bindings;
251 for (var i = 0; i < pred_binds.length; ++i)
253 var bind = pred_binds[i];
254 if (bind.key.match_function(event))
262 function match_any_unmodified_key (event)
265 return event.charCode
266 && !meta_pressed(event)
268 && !event.sticky_modifiers;
269 } catch (e) {return false; }
272 function kbd (spec, mods)
278 results.is_kbd = true;
280 if (typeof spec == "function")
281 results[0] = {match_function: spec};
283 else if (typeof spec == "string")
285 /* Attempt to parse a key specification. In order to allow
286 * the user to specify the "-" key literally, special case the
287 * parsing of that. */
289 if (spec.substr(spec.length - 1) == "-")
291 parts = spec.substr(0, spec.length - 1).split("-");
294 parts = spec.split("-");
295 var parsed_modifiers = 0;
296 if (parts.length > 1)
298 // Attempt to parse modifiers
299 for (var i = 0; i < parts.length - 1; ++i)
301 var k = modifier_names.indexOf(parts[i]);
305 parsed_modifiers |= mod;
308 // Attempt to lookup keycode
309 var name = parts[parts.length - 1];
312 parsed_modifiers |= mods;
314 if (name.length == 1) {
315 // Charcode, handle specially
317 var codes = charcode_to_keycodes[name.charCodeAt(0)];
319 throw "Invalid key specification: " + spec;
321 for each (let [keycode, shift] in codes) {
322 results[results.length] = {keyCode: keycode, modifiers: parsed_modifiers | (shift ? MOD_SHIFT : 0)};
325 var code = vk_name_to_keycode[name];
327 throw "Invalid key specification: " + spec;
328 results[0] = {keyCode: code, modifiers: parsed_modifiers};
332 results[0] = {keyCode: spec, modifiers: ((mods != null)? mods : 0)};
337 define_keywords("$fallthrough", "$category");
338 function define_key_internal(ref, kmap, keys, new_command, new_keymap)
342 var args = arguments;
344 var parent_kmap = kmap.parent;
347 for (var i = 0; i < keys.length; ++i) {
349 var final_binding = (i == keys.length - 1);
351 /* Replace `bind' with the binding specified by (cmd, fallthrough) */
352 function replace_binding(bind)
355 bind.command = new_command;
356 bind.keymap = new_keymap;
357 bind.fallthrough = args.$fallthrough;
358 bind.source_code_reference = ref;
359 bind.category = args.$category;
362 throw new Error("Key sequence has a non-keymap in prefix");
367 function make_binding()
370 return {key: key, fallthrough: args.$fallthrough,
371 command: new_command, keymap: new_keymap,
372 source_code_reference: ref,
373 category: args.$category,
379 // Check for a corresponding binding a parent
380 kmap = new keymap($parent = parent_kmap);
381 kmap.bound_in = old_kmap;
382 return {key: key, keymap: kmap,
383 source_code_reference: ref,
388 // Check if the specified binding is already present in the kmap
389 if (key.match_function)
391 var pred_binds = kmap.predicate_bindings;
392 for (var i = 0; i < pred_binds.length; i++)
394 var cur_bind = pred_binds[i];
395 if (cur_bind.key.match_function == key.match_function)
397 replace_binding(cur_bind);
402 if (!final_binding && parent_kmap) {
403 var parent_pred_binds = parent_kmap.predicate_bindings;
405 for (var i = 0; i < parent_pred_binds.length; i++)
407 var cur_bind = parent_pred_binds[i];
408 if (cur_bind.key.match_function == key.match_function && cur_bind.keymap)
410 parent_kmap = cur_bind.keymap;
415 // Not already present, must be added
416 pred_binds.push(make_binding());
419 // This is a binding by keycode: look it up in the table
420 var keycode_binds = kmap.keycode_bindings;
421 var arr = keycode_binds[key.keyCode];
423 if (arr && arr[key.modifiers])
425 replace_binding(arr[key.modifiers]);
429 if (!final_binding && parent_kmap) {
430 var p_keycode_binds = parent_kmap.keycode_bindings;
432 var p_arr = p_keycode_binds[key.keyCode];
434 if (p_arr && (p_bind = p_arr[key.modifiers]) != null && p_bind.keymap)
435 parent_kmap = p_bind.keymap;
439 arr = (keycode_binds[key.keyCode] = []);
441 arr[key.modifiers] = make_binding();
446 // bind key to either the keymap or command in the keymap, kmap
447 define_keywords("$fallthrough", "$category");
448 function define_key(kmap, keys, cmd)
450 var orig_keys = keys;
452 var ref = get_caller_source_code_reference();
454 if (typeof(keys) == "string" && keys.length > 1)
455 keys = keys.split(" ");
457 if (!(typeof(keys) == "object") || ('is_kbd' in keys) || !(keys instanceof Array))
460 var new_command = null, new_keymap = null;
461 if (typeof(cmd) == "string" || typeof(cmd) == "function")
463 else if (cmd instanceof keymap)
465 else if (cmd != null)
466 throw new Error("Invalid `cmd' argument: " + cmd);
468 var args = arguments;
470 var input_keys = keys.map(function(x) kbd(x));
472 function helper(index, output_keys) {
473 if (index == input_keys.length) {
474 define_key_internal(ref, kmap, output_keys, new_command, new_keymap,
475 forward_keywords(args));
478 var key = input_keys[index];
479 for (let i = 0; i < key.length; ++i)
480 helper(index + 1, output_keys.concat(key[i]));
484 } catch (e if (typeof(e) == "string")) {
485 dumpln("Warning: Error occurred while binding keys: " + orig_keys);
487 dumpln("This may be due to an incorrect keyboard setup.");
488 dumpln("You may want to use the -keyboard-setup command-line option to fix your keyboard setup.");
492 define_keywords("$parent", "$help", "$name");
496 /* For efficiency, a table indexed by the key code, and then by
497 * the modifiers is used to lookup key bindings, rather than
498 * looping through all bindings in the key map to find one. The
499 * array keycode_bindings is indexed by the keyCode; if the
500 * corresponding element for a keyCode is non-null, it is itself
501 * an array indexed by the result of get_modifiers (i.e. from 0 to 7).
502 * As before, match_function-based bindings are stored as a simple
503 * list, predicate_bindings. */
504 this.parent = arguments.$parent;
505 this.keycode_bindings = [];
506 this.predicate_bindings = [];
507 this.help = arguments.$help;
508 this.name = arguments.$name;
511 function define_keymap(name) {
512 this[name] = new keymap($name = name, forward_keywords(arguments));
515 function copy_event(event)
518 ev.keyCode = event.keyCode;
519 ev.charCode = event.charCode;
520 ev.ctrlKey = event.ctrlKey;
521 ev.metaKey = event.metaKey;
522 ev.altKey = event.altKey;
523 ev.shiftKey = event.shiftKey;
524 ev.sticky_modifiers = event.sticky_modifiers;
528 function key_down_handler(event)
531 //window.dumpln("key down: " + conkeror.format_key_press(event.keyCode, conkeror.get_modifiers(event)));
533 var state = window.keyboard;
534 state.last_key_down_event = copy_event(event);
535 state.last_char_code = null;
536 state.last_key_code = null;
539 define_variable("keyboard_key_sequence_help_timeout", 0,
540 "Delay (in millseconds) before the current key sequence prefix is displayed in the minibuffer.");
542 define_window_local_hook("key_press_hook", RUN_HOOK_UNTIL_SUCCESS);
544 function key_press_handler(true_event)
546 function show_partial_key_sequence (window, state, ctx) {
547 if (!state.help_displayed)
549 state.help_timer_ID = window.setTimeout(function () {
550 window.minibuffer.show(ctx.key_sequence.join(" "));
551 state.help_displayed = true;
552 state.help_timer_ID = null;
553 }, keyboard_key_sequence_help_timeout);
556 window.minibuffer.show(ctx.key_sequence.join(" "));
560 var state = window.keyboard;
562 /* ASSERT(state.last_key_down_event != null); */
564 var event = state.last_key_down_event;
565 event.charCode = true_event.charCode;
567 // If the true_event includes a keyCode, we can just use that
568 if (true_event.keyCode)
569 event.keyCode = true_event.keyCode;
571 /* Filter out events from keys like the Windows/Super/Hyper key */
572 if (event.keyCode == 0)
575 /* Clear minibuffer message */
576 window.minibuffer.clear();
582 if (!state.current_context)
583 ctx = state.current_context = { window: window, key_sequence: [], sticky_modifiers: 0 };
585 ctx = state.current_context;
587 event.sticky_modifiers = ctx.sticky_modifiers;
588 ctx.sticky_modifiers = 0;
592 if (key_press_hook.run(window, ctx, true_event))
596 state.override_keymap ||
597 window.buffers.current.keymap;
600 state.active_keymap ||
603 var overlay_keymap = ctx.overlay_keymap;
606 (overlay_keymap && lookup_key_binding(overlay_keymap, event)) ||
607 lookup_key_binding(active_keymap, event) ;
609 // Should we stop this event from being processed by the gui?
611 // 1) we have a binding, and the binding's fallthrough property is not
614 // 2) we are in the middle of a key sequence, and we need to say that
615 // the key sequence given has no command.
617 if (!binding || !binding.fallthrough)
619 true_event.preventDefault();
620 true_event.stopPropagation();
623 // Finally, process the binding.
624 ctx.key_sequence.push(format_key_event(event));
626 if (binding.keymap) {
627 state.active_keymap = binding.keymap;
628 show_partial_key_sequence(window, state, ctx);
629 // We're going for another round
631 } else if (binding.command) {
632 call_interactively(ctx, binding.command);
633 if (interactive_commands.get(binding.command).prefix) {
634 state.active_keymap = null;
635 show_partial_key_sequence(window, state, ctx);
640 window.minibuffer.message(ctx.key_sequence.join(" ") + " is undefined");
643 // Clean up if we're done
646 if (state.help_timer_ID != null)
648 window.clearTimeout(state.help_timer_ID);
649 state.help_timer_ID = null;
651 state.help_displayed = false;
652 state.active_keymap = null;
653 state.current_context = null;
655 } catch(e) { dump_error(e);}
658 function keyboard(window)
660 this.window = window;
663 keyboard.prototype = {
664 last_key_down_event : null,
665 current_context : null,
666 active_keymap : null,
667 help_timer_ID : null,
668 help_displayed : false,
670 /* If this is non-null, it is used instead of the current buffer's
672 override_keymap : null,
674 set_override_keymap : function (keymap) {
675 /* Clear out any in-progress key sequence. */
676 this.active_keymap = null;
677 this.current_context = null;
678 if (this.help_timer_ID != null)
680 this.window.clearTimeout(this.help_timer_ID);
681 this.help_timer_ID = null;
683 this.override_keymap = keymap;
688 function keyboard_initialize_window(window)
690 window.keyboard = new keyboard(window);
692 window.addEventListener ("keydown", key_down_handler, true /* capture */,
693 false /* ignore untrusted events */);
694 window.addEventListener ("keypress", key_press_handler, true /* capture */,
695 false /* ignore untrusted events */);
698 add_hook("window_initialize_hook", keyboard_initialize_window);
700 function for_each_key_binding(keymap_or_buffer, callback) {
702 if (keymap_or_buffer instanceof conkeror.buffer) {
703 var buffer = keymap_or_buffer;
704 var window = buffer.window;
705 keymap = window.keyboard.override_keymap || buffer.keymap;
707 keymap = keymap_or_buffer;
709 var keymap_stack = [keymap];
710 var binding_stack = [];
711 function helper2(bind) {
712 binding_stack.push(bind);
713 callback(binding_stack);
714 if (bind.keymap && keymap_stack.indexOf(bind.keymap) == -1) {
715 keymap_stack.push(bind.keymap);
722 var unmodified_keys_masked = false;
723 var keycode_masks = [];
725 var keymap = keymap_stack[keymap_stack.length - 1];
726 for (var i in keymap.keycode_bindings) {
727 var b = keymap.keycode_bindings[i];
728 if (!(i in keycode_masks))
729 keycode_masks[i] = [];
731 if (unmodified_keys_masked && ((j & MOD_SHIFT) == j))
733 if (!keycode_masks[i][j]) {
735 keycode_masks[i][j] = true;
739 for (var i in keymap.predicate_bindings) {
740 var bind = keymap.predicate_bindings[i];
742 var p = bind.key.match_function;
743 if (p == match_any_key)
745 if (p == match_any_unmodified_key)
746 unmodified_keys_masked = true;
749 keymap_stack[keymap_stack.length - 1] = keymap.parent;
757 function find_command_in_keymap(keymap_or_buffer, command) {
760 for_each_key_binding(keymap_or_buffer, function (bind_seq) {
761 var bind = bind_seq[bind_seq.length - 1];
762 if (bind.command == command)
763 list.push(format_binding_sequence(bind_seq));
768 define_keymap("key_binding_reader_keymap");
769 define_key(key_binding_reader_keymap, match_any_key, "read-key-binding-key");
771 define_keywords("$buffer", "$keymap");
772 function key_binding_reader(continuation) {
773 keywords(arguments, $prompt = "Describe key:");
775 this.continuation = continuation;
777 if (arguments.$keymap)
778 this.target_keymap = arguments.$keymap;
780 var buffer = arguments.$buffer;
781 var window = buffer.window;
782 this.target_keymap = window.keyboard.override_keymap || buffer.keymap;
785 this.key_sequence = [];
787 minibuffer_input_state.call(this, key_binding_reader_keymap, arguments.$prompt);
789 key_binding_reader.prototype = {
790 __proto__: minibuffer_input_state.prototype,
791 destroy: function () {
792 if (this.continuation)
793 this.continuation.throw(abort());
797 function invalid_key_binding(seq) {
798 var e = new Error(seq.join(" ") + " is undefined");
799 e.key_sequence = seq;
800 e.__proto__ = invalid_key_binding.prototype;
803 invalid_key_binding.prototype = {
804 __proto__: interactive_error.prototype
807 function read_key_binding_key(window, state, event) {
808 var binding = lookup_key_binding(state.target_keymap, event);
810 state.key_sequence.push(format_key_event(event));
812 if (binding == null) {
813 var c = state.continuation;
814 delete state.continuation;
815 window.minibuffer.pop_state();
816 c.throw(invalid_key_binding(state.key_sequence));
820 if (binding.keymap) {
821 window.minibuffer._restore_normal_state();
822 window.minibuffer._input_text = state.key_sequence.join(" ") + " ";
823 state.target_keymap = binding.keymap;
827 var c = state.continuation;
828 delete state.continuation;
830 window.minibuffer.pop_state();
833 c([state.key_sequence, binding]);
835 interactive("read-key-binding-key", null, function (I) {
836 read_key_binding_key(I.window, I.minibuffer.check_state(key_binding_reader), I.event);
839 minibuffer.prototype.read_key_binding = function () {
841 var s = new key_binding_reader((yield CONTINUATION), forward_keywords(arguments));
843 var result = yield SUSPEND;
844 yield co_return(result);