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)};
339 define_keywords("$fallthrough", "$category");
340 function define_key_internal(ref, kmap, keys, new_command, new_keymap)
344 var args = arguments;
346 var parent_kmap = kmap.parent;
349 for (var i = 0; i < keys.length; ++i) {
351 var final_binding = (i == keys.length - 1);
353 /* Replace `bind' with the binding specified by (cmd, fallthrough) */
354 function replace_binding(bind)
357 bind.command = new_command;
358 bind.keymap = new_keymap;
359 bind.fallthrough = args.$fallthrough;
360 bind.source_code_reference = ref;
361 bind.category = args.$category;
364 throw new Error("Key sequence has a non-keymap in prefix");
369 function make_binding()
372 return {key: key, fallthrough: args.$fallthrough,
373 command: new_command, keymap: new_keymap,
374 source_code_reference: ref,
375 category: args.$category,
381 // Check for a corresponding binding a parent
382 kmap = new keymap($parent = parent_kmap);
383 kmap.bound_in = old_kmap;
384 return {key: key, keymap: kmap,
385 source_code_reference: ref,
390 // Check if the specified binding is already present in the kmap
391 if (key.match_function)
393 var pred_binds = kmap.predicate_bindings;
394 for (var i = 0; i < pred_binds.length; i++)
396 var cur_bind = pred_binds[i];
397 if (cur_bind.key.match_function == key.match_function)
399 replace_binding(cur_bind);
404 if (!final_binding && parent_kmap) {
405 var parent_pred_binds = parent_kmap.predicate_bindings;
407 for (var i = 0; i < parent_pred_binds.length; i++)
409 var cur_bind = parent_pred_binds[i];
410 if (cur_bind.key.match_function == key.match_function && cur_bind.keymap)
412 parent_kmap = cur_bind.keymap;
417 // Not already present, must be added
418 pred_binds.push(make_binding());
421 // This is a binding by keycode: look it up in the table
422 var keycode_binds = kmap.keycode_bindings;
423 var arr = keycode_binds[key.keyCode];
425 if (arr && arr[key.modifiers])
427 replace_binding(arr[key.modifiers]);
431 if (!final_binding && parent_kmap) {
432 var p_keycode_binds = parent_kmap.keycode_bindings;
434 var p_arr = p_keycode_binds[key.keyCode];
436 if (p_arr && (p_bind = p_arr[key.modifiers]) != null && p_bind.keymap)
437 parent_kmap = p_bind.keymap;
441 arr = (keycode_binds[key.keyCode] = []);
443 arr[key.modifiers] = make_binding();
448 // bind key to either the keymap or command in the keymap, kmap
449 define_keywords("$fallthrough", "$category");
450 function define_key(kmap, keys, cmd)
453 var orig_keys = keys;
455 var ref = get_caller_source_code_reference();
457 if (typeof(keys) == "string" && keys.length > 1)
458 keys = keys.split(" ");
460 if (!(typeof(keys) == "object") || ('is_kbd' in keys) || !(keys instanceof Array))
463 var new_command = null, new_keymap = null;
464 if (typeof(cmd) == "string" || typeof(cmd) == "function")
466 else if (cmd instanceof keymap)
468 else if (cmd != null)
469 throw new Error("Invalid `cmd' argument: " + cmd);
471 var args = arguments;
473 var input_keys = keys.map(function(x) kbd(x));
475 function helper(index, output_keys) {
476 if (index == input_keys.length) {
477 define_key_internal(ref, kmap, output_keys, new_command, new_keymap,
478 forward_keywords(args));
481 var key = input_keys[index];
482 for (let i = 0; i < key.length; ++i)
483 helper(index + 1, output_keys.concat(key[i]));
487 } catch (e if (typeof(e) == "string")) {
488 dumpln("Warning: Error occurred while binding keys: " + orig_keys);
490 dumpln("This may be due to an incorrect keyboard setup.");
491 dumpln("You may want to use the -keyboard-setup command-line option to fix your keyboard setup.");
495 define_keywords("$parent", "$help", "$name");
499 /* For efficiency, a table indexed by the key code, and then by
500 * the modifiers is used to lookup key bindings, rather than
501 * looping through all bindings in the key map to find one. The
502 * array keycode_bindings is indexed by the keyCode; if the
503 * corresponding element for a keyCode is non-null, it is itself
504 * an array indexed by the result of get_modifiers (i.e. from 0 to 7).
505 * As before, match_function-based bindings are stored as a simple
506 * list, predicate_bindings. */
507 this.parent = arguments.$parent;
508 this.keycode_bindings = [];
509 this.predicate_bindings = [];
510 this.help = arguments.$help;
511 this.name = arguments.$name;
514 function define_keymap(name) {
516 this[name] = new keymap($name = name, forward_keywords(arguments));
519 function copy_event(event)
522 ev.keyCode = event.keyCode;
523 ev.charCode = event.charCode;
524 ev.ctrlKey = event.ctrlKey;
525 ev.metaKey = event.metaKey;
526 ev.altKey = event.altKey;
527 ev.shiftKey = event.shiftKey;
528 ev.sticky_modifiers = event.sticky_modifiers;
532 function key_down_handler(event)
535 //window.dumpln("key down: " + conkeror.format_key_press(event.keyCode, conkeror.get_modifiers(event)));
537 var state = window.keyboard;
538 state.last_key_down_event = copy_event(event);
539 state.last_char_code = null;
540 state.last_key_code = null;
543 define_variable("keyboard_key_sequence_help_timeout", 0,
544 "Delay (in millseconds) before the current key sequence prefix is displayed in the minibuffer.");
546 define_window_local_hook("key_press_hook", RUN_HOOK_UNTIL_SUCCESS);
548 function key_press_handler(true_event)
550 function show_partial_key_sequence (window, state, ctx) {
551 if (!state.help_displayed)
553 state.help_timer_ID = window.setTimeout(function () {
554 window.minibuffer.show(ctx.key_sequence.join(" "));
555 state.help_displayed = true;
556 state.help_timer_ID = null;
557 }, keyboard_key_sequence_help_timeout);
560 window.minibuffer.show(ctx.key_sequence.join(" "));
564 var state = window.keyboard;
566 /* ASSERT(state.last_key_down_event != null); */
568 var event = state.last_key_down_event;
569 event.charCode = true_event.charCode;
571 // If the true_event includes a keyCode, we can just use that
572 if (true_event.keyCode)
573 event.keyCode = true_event.keyCode;
575 /* Filter out events from keys like the Windows/Super/Hyper key */
576 if (event.keyCode == 0)
579 /* Clear minibuffer message */
580 window.minibuffer.clear();
586 if (!state.current_context)
587 ctx = state.current_context = { window: window, key_sequence: [], sticky_modifiers: 0 };
589 ctx = state.current_context;
591 event.sticky_modifiers = ctx.sticky_modifiers;
592 ctx.sticky_modifiers = 0;
596 if (key_press_hook.run(window, ctx, true_event))
600 state.override_keymap ||
601 window.buffers.current.keymap;
604 state.active_keymap ||
607 var overlay_keymap = ctx.overlay_keymap;
610 (overlay_keymap && lookup_key_binding(overlay_keymap, event)) ||
611 lookup_key_binding(active_keymap, event) ;
613 // Should we stop this event from being processed by the gui?
615 // 1) we have a binding, and the binding's fallthrough property is not
618 // 2) we are in the middle of a key sequence, and we need to say that
619 // the key sequence given has no command.
621 if (!binding || !binding.fallthrough)
623 true_event.preventDefault();
624 true_event.stopPropagation();
627 // Finally, process the binding.
628 ctx.key_sequence.push(format_key_event(event));
630 if (binding.keymap) {
631 state.active_keymap = binding.keymap;
632 show_partial_key_sequence(window, state, ctx);
633 // We're going for another round
635 } else if (binding.command) {
636 call_interactively(ctx, binding.command);
637 if (interactive_commands.get(binding.command).prefix) {
638 state.active_keymap = null;
639 show_partial_key_sequence(window, state, ctx);
644 window.minibuffer.message(ctx.key_sequence.join(" ") + " is undefined");
647 // Clean up if we're done
650 if (state.help_timer_ID != null)
652 window.clearTimeout(state.help_timer_ID);
653 state.help_timer_ID = null;
655 state.help_displayed = false;
656 state.active_keymap = null;
657 state.current_context = null;
659 } catch(e) { dump_error(e);}
662 function keyboard(window)
664 this.window = window;
667 keyboard.prototype = {
668 last_key_down_event : null,
669 current_context : null,
670 active_keymap : null,
671 help_timer_ID : null,
672 help_displayed : false,
674 /* If this is non-null, it is used instead of the current buffer's
676 override_keymap : null,
678 set_override_keymap : function (keymap) {
679 /* Clear out any in-progress key sequence. */
680 this.active_keymap = null;
681 this.current_context = null;
682 if (this.help_timer_ID != null)
684 this.window.clearTimeout(this.help_timer_ID);
685 this.help_timer_ID = null;
687 this.override_keymap = keymap;
692 function keyboard_initialize_window(window)
694 window.keyboard = new keyboard(window);
696 window.addEventListener ("keydown", key_down_handler, true /* capture */,
697 false /* ignore untrusted events */);
698 window.addEventListener ("keypress", key_press_handler, true /* capture */,
699 false /* ignore untrusted events */);
702 add_hook("window_initialize_hook", keyboard_initialize_window);
704 function for_each_key_binding(keymap_or_buffer, callback) {
706 if (keymap_or_buffer instanceof conkeror.buffer) {
707 var buffer = keymap_or_buffer;
708 var window = buffer.window;
709 keymap = window.keyboard.override_keymap || buffer.keymap;
711 keymap = keymap_or_buffer;
713 var keymap_stack = [keymap];
714 var binding_stack = [];
715 function helper2(bind) {
716 binding_stack.push(bind);
717 callback(binding_stack);
718 if (bind.keymap && keymap_stack.indexOf(bind.keymap) == -1) {
719 keymap_stack.push(bind.keymap);
726 var unmodified_keys_masked = false;
727 var keycode_masks = [];
729 var keymap = keymap_stack[keymap_stack.length - 1];
730 for (var i in keymap.keycode_bindings) {
731 var b = keymap.keycode_bindings[i];
732 if (!(i in keycode_masks))
733 keycode_masks[i] = [];
735 if (unmodified_keys_masked && ((j & MOD_SHIFT) == j))
737 if (!keycode_masks[i][j]) {
739 keycode_masks[i][j] = true;
743 for (var i in keymap.predicate_bindings) {
744 var bind = keymap.predicate_bindings[i];
746 var p = bind.key.match_function;
747 if (p == match_any_key)
749 if (p == match_any_unmodified_key)
750 unmodified_keys_masked = true;
753 keymap_stack[keymap_stack.length - 1] = keymap.parent;
761 function find_command_in_keymap(keymap_or_buffer, command) {
764 for_each_key_binding(keymap_or_buffer, function (bind_seq) {
765 var bind = bind_seq[bind_seq.length - 1];
766 if (bind.command == command)
767 list.push(format_binding_sequence(bind_seq));
772 define_keymap("key_binding_reader_keymap");
773 define_key(key_binding_reader_keymap, match_any_key, "read-key-binding-key");
775 define_keywords("$buffer", "$keymap");
776 function key_binding_reader(continuation) {
777 keywords(arguments, $prompt = "Describe key:");
779 this.continuation = continuation;
781 if (arguments.$keymap)
782 this.target_keymap = arguments.$keymap;
784 var buffer = arguments.$buffer;
785 var window = buffer.window;
786 this.target_keymap = window.keyboard.override_keymap || buffer.keymap;
789 this.key_sequence = [];
791 minibuffer_input_state.call(this, key_binding_reader_keymap, arguments.$prompt);
793 key_binding_reader.prototype = {
794 __proto__: minibuffer_input_state.prototype,
795 destroy: function () {
796 if (this.continuation)
797 this.continuation.throw(abort());
801 function invalid_key_binding(seq) {
802 var e = new Error(seq.join(" ") + " is undefined");
803 e.key_sequence = seq;
804 e.__proto__ = invalid_key_binding.prototype;
807 invalid_key_binding.prototype = {
808 __proto__: interactive_error.prototype
811 function read_key_binding_key(window, state, event) {
812 var binding = lookup_key_binding(state.target_keymap, event);
814 state.key_sequence.push(format_key_event(event));
816 if (binding == null) {
817 var c = state.continuation;
818 delete state.continuation;
819 window.minibuffer.pop_state();
820 c.throw(invalid_key_binding(state.key_sequence));
824 if (binding.keymap) {
825 window.minibuffer._restore_normal_state();
826 window.minibuffer._input_text = state.key_sequence.join(" ") + " ";
827 state.target_keymap = binding.keymap;
831 var c = state.continuation;
832 delete state.continuation;
834 window.minibuffer.pop_state();
837 c([state.key_sequence, binding]);
839 interactive("read-key-binding-key", null, function (I) {
840 read_key_binding_key(I.window, I.minibuffer.check_state(key_binding_reader), I.event);
843 minibuffer.prototype.read_key_binding = function () {
845 var s = new key_binding_reader((yield CONTINUATION), forward_keywords(arguments));
847 var result = yield SUSPEND;
848 yield co_return(result);