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", 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 } catch (e) {return false; }
271 function kbd (spec, mods)
277 results.is_kbd = true;
279 if (typeof spec == "function")
280 results[0] = {match_function: spec};
282 else if (typeof spec == "string")
284 /* Attempt to parse a key specification. In order to allow
285 * the user to specify the "-" key literally, special case the
286 * parsing of that. */
288 if (spec.substr(spec.length - 1) == "-")
290 parts = spec.substr(0, spec.length - 1).split("-");
293 parts = spec.split("-");
294 var parsed_modifiers = 0;
295 if (parts.length > 1)
297 // Attempt to parse modifiers
298 for (var i = 0; i < parts.length - 1; ++i)
300 var k = modifier_names.indexOf(parts[i]);
304 parsed_modifiers |= mod;
307 // Attempt to lookup keycode
308 var name = parts[parts.length - 1];
311 parsed_modifiers |= mods;
313 if (name.length == 1) {
314 // Charcode, handle specially
316 var codes = charcode_to_keycodes[name.charCodeAt(0)];
318 throw "Invalid key specification: " + spec;
320 for each (let [keycode, shift] in codes) {
321 results[results.length] = {keyCode: keycode, modifiers: parsed_modifiers | (shift ? MOD_SHIFT : 0)};
324 var code = vk_name_to_keycode[name];
326 throw "Invalid key specification: " + spec;
327 results[0] = {keyCode: code, modifiers: parsed_modifiers};
331 results[0] = {keyCode: spec, modifiers: ((mods != null)? mods : 0)};
336 define_keywords("$fallthrough", "$hook", "$category");
337 function define_key_internal(ref, kmap, keys, new_command, new_keymap)
341 var args = arguments;
343 var parent_kmap = kmap.parent;
346 for (var i = 0; i < keys.length; ++i) {
348 var final_binding = (i == keys.length - 1);
350 /* Replace `bind' with the binding specified by (cmd, fallthrough) */
351 function replace_binding(bind)
354 bind.command = new_command;
355 bind.keymap = new_keymap;
356 bind.fallthrough = args.$fallthrough;
357 bind.hook = args.$hook;
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, hook: args.$hook,
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", "$hook", "$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)
548 var state = window.keyboard;
550 /* ASSERT(state.last_key_down_event != null); */
552 var event = state.last_key_down_event;
553 event.charCode = true_event.charCode;
555 // If the true_event includes a keyCode, we can just use that
556 if (true_event.keyCode)
557 event.keyCode = true_event.keyCode;
559 /* Filter out events from keys like the Windows/Super/Hyper key */
560 if (event.keyCode == 0)
563 /* Clear minibuffer message */
564 window.minibuffer.clear();
570 if (!state.current_context)
571 ctx = state.current_context = { window: window, key_sequence: [], sticky_modifiers: 0 };
573 ctx = state.current_context;
575 event.sticky_modifiers = ctx.sticky_modifiers;
576 ctx.sticky_modifiers = 0;
580 if (key_press_hook.run(window, ctx, true_event))
584 state.active_keymap ||
585 state.override_keymap ||
586 window.buffers.current.keymap;
587 var overlay_keymap = ctx.overlay_keymap;
590 lookup_key_binding(active_keymap, event) ||
591 (overlay_keymap && lookup_key_binding(overlay_keymap, event));
593 ctx.overlay_keymap = null;
595 // Should we stop this event from being processed by the gui?
597 // 1) we have a binding, and the binding's fallthrough property is not
600 // 2) we are in the middle of a key sequence, and we need to say that
601 // the key sequence given has no command.
603 if (!binding || !binding.fallthrough)
605 true_event.preventDefault();
606 true_event.stopPropagation();
609 // Finally, process the binding.
610 ctx.key_sequence.push(format_key_event(event));
612 if (binding.keymap) {
614 binding.hook.call(null, ctx, active_keymap, overlay_keymap);
615 state.active_keymap = binding.keymap;
616 if (!state.help_displayed)
618 state.help_timer_ID = window.setTimeout(function () {
619 window.minibuffer.show(ctx.key_sequence.join(" "));
620 state.help_displayed = true;
621 state.help_timer_ID = null;
622 }, keyboard_key_sequence_help_timeout);
625 window.minibuffer.show(ctx.key_sequence.join(" "));
627 // We're going for another round
629 } else if (binding.command) {
630 call_interactively(ctx, binding.command);
633 window.minibuffer.message(ctx.key_sequence.join(" ") + " is undefined");
636 // Clean up if we're done
639 if (state.help_timer_ID != null)
641 window.clearTimeout(state.help_timer_ID);
642 state.help_timer_ID = null;
644 state.help_displayed = false;
645 state.active_keymap = null;
646 state.current_context = null;
648 } catch(e) { dump_error(e);}
651 function keyboard(window)
653 this.window = window;
656 keyboard.prototype = {
657 last_key_down_event : null,
658 current_context : null,
659 active_keymap : null,
660 help_timer_ID : null,
661 help_displayed : false,
663 /* If this is non-null, it is used instead of the current buffer's
665 override_keymap : null,
667 set_override_keymap : function (keymap) {
668 /* Clear out any in-progress key sequence. */
669 this.active_keymap = null;
670 this.current_context = null;
671 if (this.help_timer_ID != null)
673 this.window.clearTimeout(this.help_timer_ID);
674 this.help_timer_ID = null;
676 this.override_keymap = keymap;
681 function keyboard_initialize_window(window)
683 window.keyboard = new keyboard(window);
685 window.addEventListener ("keydown", key_down_handler, true /* capture */,
686 false /* ignore untrusted events */);
687 window.addEventListener ("keypress", key_press_handler, true /* capture */,
688 false /* ignore untrusted events */);
691 add_hook("window_initialize_hook", keyboard_initialize_window);
693 function for_each_key_binding(keymap_or_buffer, callback) {
695 if (keymap_or_buffer instanceof conkeror.buffer) {
696 var buffer = keymap_or_buffer;
697 var window = buffer.window;
698 keymap = window.keyboard.override_keymap || buffer.keymap;
700 keymap = keymap_or_buffer;
702 var keymap_stack = [keymap];
703 var binding_stack = [];
704 function helper2(bind) {
705 binding_stack.push(bind);
706 callback(binding_stack);
707 if (bind.keymap && keymap_stack.indexOf(bind.keymap) == -1) {
708 keymap_stack.push(bind.keymap);
715 var unmodified_keys_masked = false;
716 var keycode_masks = [];
718 var keymap = keymap_stack[keymap_stack.length - 1];
719 for (var i in keymap.keycode_bindings) {
720 var b = keymap.keycode_bindings[i];
721 if (!(i in keycode_masks))
722 keycode_masks[i] = [];
724 if (unmodified_keys_masked && ((j & MOD_SHIFT) == j))
726 if (!keycode_masks[i][j]) {
728 keycode_masks[i][j] = true;
732 for (var i in keymap.predicate_bindings) {
733 var bind = keymap.predicate_bindings[i];
735 var p = bind.key.match_function;
736 if (p == match_any_key)
738 if (p == match_any_unmodified_key)
739 unmodified_keys_masked = true;
742 keymap_stack[keymap_stack.length - 1] = keymap.parent;
750 function find_command_in_keymap(keymap_or_buffer, command) {
753 for_each_key_binding(keymap_or_buffer, function (bind_seq) {
754 var bind = bind_seq[bind_seq.length - 1];
755 if (bind.command == command)
756 list.push(format_binding_sequence(bind_seq));
761 define_keymap("key_binding_reader_keymap");
762 define_key(key_binding_reader_keymap, match_any_key, "read-key-binding-key");
764 define_keywords("$buffer", "$keymap");
765 function key_binding_reader(continuation) {
766 keywords(arguments, $prompt = "Describe key:");
768 this.continuation = continuation;
770 if (arguments.$keymap)
771 this.target_keymap = arguments.$keymap;
773 var buffer = arguments.$buffer;
774 var window = buffer.window;
775 this.target_keymap = window.keyboard.override_keymap || buffer.keymap;
778 this.key_sequence = [];
780 minibuffer_input_state.call(this, key_binding_reader_keymap, arguments.$prompt);
782 key_binding_reader.prototype = {
783 __proto__: minibuffer_input_state.prototype,
784 destroy: function () {
785 if (this.continuation)
786 this.continuation.throw(abort());
790 function invalid_key_binding(seq) {
791 var e = new Error(seq.join(" ") + " is undefined");
792 e.key_sequence = seq;
793 e.__proto__ = invalid_key_binding.prototype;
796 invalid_key_binding.prototype = {
797 __proto__: interactive_error.prototype
800 function read_key_binding_key(window, state, event) {
801 var binding = lookup_key_binding(state.target_keymap, event);
803 state.key_sequence.push(format_key_event(event));
805 if (binding == null) {
806 var c = state.continuation;
807 delete state.continuation;
808 window.minibuffer.pop_state();
809 c.throw(invalid_key_binding(state.key_sequence));
813 if (binding.keymap) {
814 window.minibuffer._restore_normal_state();
815 window.minibuffer._input_text = state.key_sequence.join(" ") + " ";
816 state.target_keymap = binding.keymap;
820 var c = state.continuation;
821 delete state.continuation;
823 window.minibuffer.pop_state();
826 c([state.key_sequence, binding]);
828 interactive("read-key-binding-key", function (I) {
829 read_key_binding_key(I.window, I.minibuffer.check_state(key_binding_reader), I.event);
832 minibuffer.prototype.read_key_binding = function () {
834 var s = new key_binding_reader((yield CONTINUATION), forward_keywords(arguments));
836 var result = yield SUSPEND;
837 yield co_return(result);