Debian: Bump Standards-Version to 3.8.3
[conkeror.git] / modules / keyboard.js
blobc80a2c555af190f07cd74e9143a7fbc846360a57
1 /**
2  * (C) Copyright 2004-2007 Shawn Betts
3  * (C) Copyright 2007-2009 John J. Foerch
4  * (C) Copyright 2007-2008 Jeremy Maitin-Shepard
5  *
6  * Use, modification, and distribution are subject to the terms specified in the
7  * COPYING file.
8 **/
10 require("window.js");
11 require("command-line.js");
13 define_variable("key_bindings_ignore_capslock", false,
14     "When true, the case of characters in key bindings will be based "+
15     "only on whether shift was pressed--upper-case if yes, lower-case if "+
16     "no.  Effectively, this overrides the capslock key.  This option has "+
17     "no effect on ordinary typing in input fields.");
19 /* Generate vk name table  */
20 var keycode_to_vk_name = [];
21 var vk_name_to_keycode = {};
23     let KeyEvent = Ci.nsIDOMKeyEvent;
24     let prefix = "DOM_VK_";
25     for (var i in KeyEvent) {
26         /* Check if this is a key binding */
27         if (i.substr(0, prefix.length) == prefix) {
28             let name = i.substr(prefix.length).toLowerCase();
29             let code = KeyEvent[i];
30             keycode_to_vk_name[code] = name;
31             vk_name_to_keycode[name] = code;
32         }
33     }
38  * Modifiers
39  */
41 function modifier (in_event_p, set_in_event) {
42     this.in_event_p = in_event_p;
43     this.set_in_event = set_in_event;
46 var modifiers = {
47     A: new modifier(function (event) { return event.altKey; },
48                     function (event) { event.altKey = true; }),
49     C: new modifier(function (event) { return event.ctrlKey; },
50                     function (event) { event.ctrlKey = true; }),
51     M: new modifier(function (event) { return event.metaKey; },
52                     function (event) { event.metaKey = true; }),
53     S: new modifier(function (event) {
54                         return (((event.keyCode &&
55                                   event.charCode == 0) ||
56                                  event.charCode == 32) &&
57                                 event.shiftKey);
58                     },
59                     function (event) { event.shiftKey = true; })
61 var modifier_order = ['C', 'M', 'S'];
63 // check the platform and guess whether we should treat Alt as Meta
64 if (get_os() == 'Darwin') {
65     // In OS X, alt is a shift-like modifier, in that we
66     // only care about it for non-character events.
67     modifiers.A = new modifier(
68         function (event) {
69             return (event.keyCode &&
70                     event.charCode == 0 &&
71                     event.altKey);
72         },
73         function (event) { event.altKey = true; });
74     modifier_order = ['C', 'M', 'A', 'S'];
75 } else {
76     modifiers.M = modifiers.A;
82  * Keymap datatype
83  */
85 define_keywords("$parent", "$help", "$name", "$anonymous");
86 function keymap () {
87     keywords(arguments);
88     this.parent = arguments.$parent;
89     this.bindings = {};
90     this.predicate_bindings = [];
91     this.help = arguments.$help;
92     this.name = arguments.$name;
93     this.anonymous = arguments.$anonymous;
96 function define_keymap (name) {
97     keywords(arguments);
98     this[name] = new keymap($name = name, forward_keywords(arguments));
104  * Key Match Predicates.
106  *  Predicate bindings are tried for a match after the ordinary key-combo
107  * bindings.  They are predicate functions on the keypress event object.
108  * When such a predicate returns a true value, its associated command,
109  * keymap, or fallthrough declaration is performed.
110  */
112 function define_key_match_predicate (name, description, predicate) {
113     conkeror[name] = predicate;
114     conkeror[name].name = name;
115     conkeror[name].description = description;
118 define_key_match_predicate('match_any_key', 'any key',
119     function (event) { return true; });
121 define_key_match_predicate('match_any_unmodified_key', 'any unmodified key',
122     function (event) {
123         //XXX: the meaning of "unmodified" is platform dependent. for
124         // example, on OS X, Alt is used in combination with the
125         // character keys to type an alternate character.  A possible
126         // solution is to set the altKey property of the event to null
127         // for all keypress events on OS X.
128         try {
129             return event.charCode
130                 && !event.altKey
131                 && !event.metaKey
132                 && !event.ctrlKey
133                 && !event.sticky_modifiers;
134         } catch (e) {return false; }
135     });
139  */
141 function format_key_spec (key) {
142     if (key instanceof Function) {
143         if (key.description)
144             return "<"+key.description+">";
145         if (key.name)
146             return "<"+key.name+">";
147         return "<anonymous match function>";
148     }
149     return key;
152 function format_binding_sequence (seq) {
153     return seq.map(function (x) {
154             return format_key_spec(x.key);
155         }).join(" ");
159 function lookup_key_binding (kmap, combo, event) {
160     do {
161         // Check if the key matches the keycode table
162         // var mods = get_modifiers(event);
163         var bindings = kmap.bindings;
164         var bind;
165         if ((bind = bindings[combo]) != null)
166             return bind;
168         // Check if the key matches a predicate
169         var pred_binds = kmap.predicate_bindings;
170         for (var i = 0; i < pred_binds.length; ++i) {
171             bind = pred_binds[i];
172             if (bind.key(event))
173                 return bind;
174         }
175         kmap = kmap.parent;
176     } while (kmap);
177     return null;
182  * $fallthrough, $repeat and $browser_object are as for define_key.
184  * ref is the source code reference of the call to define_key.
186  * kmap is the keymap in which the binding is to be defined.
188  * keys is the key sequence being bound.  it may be necessary
189  * to auto-generate new keymaps to accomodate the key sequence.
191  * only one of new_command and new_keymap will be given.
192  * the one that is given is the thing being bound to.
193  */
194 define_keywords("$fallthrough", "$repeat", "$browser_object");
195 function define_key_internal (ref, kmap, keys, new_command, new_keymap) {
196     keywords(arguments);
197     var args = arguments;
198     var parent_kmap = kmap.parent;
199     var final_binding; // flag to indicate the final key combo in the sequence.
200     var key; // current key combo as we iterate through the sequence.
201     var undefine_key = (new_command == null) &&
202         (new_keymap == null) &&
203         (! args.$fallthrough);
205     /* Replace `bind' with the binding specified by (cmd, fallthrough) */
206     function replace_binding (bind) {
207         if (final_binding) {
208             bind.command = new_command;
209             bind.keymap = new_keymap;
210             bind.fallthrough = args.$fallthrough;
211             bind.source_code_reference = ref;
212             bind.repeat = args.$repeat;
213             bind.browser_object = args.$browser_object;
214         } else {
215             if (!bind.keymap)
216                 throw new Error("Key sequence has a non-keymap in prefix");
217             kmap = bind.keymap;
218         }
219     }
221     function make_binding () {
222         if (final_binding) {
223             return {key: key,
224                     fallthrough: args.$fallthrough,
225                     command: new_command,
226                     keymap: new_keymap,
227                     source_code_reference: ref,
228                     repeat: args.$repeat,
229                     browser_object: args.$browser_object,
230                     bound_in: kmap};
231         } else {
232             let old_kmap = kmap;
233             // Check for a corresponding binding a parent
234             kmap = new keymap($parent = parent_kmap, $anonymous,
235                               $name = old_kmap.name + " " + format_key_spec(key));
236             kmap.bound_in = old_kmap;
237             return {key: key,
238                     keymap: kmap,
239                     source_code_reference: ref,
240                     bound_in: old_kmap};
241         }
242     }
244 outer:
245     for (var i = 0; i < keys.length; ++i) {
246         key = keys[i];
247         final_binding = (i == keys.length - 1);
249         // Check if the specified binding is already present in the kmap
250         if (typeof(key) == "function") { // it's a match predicate
251             var pred_binds = kmap.predicate_bindings;
252             for (var j = 0; j < pred_binds.length; j++) {
253                 if (pred_binds[j].key == key) {
254                     if (final_binding && undefine_key) {
255                         delete pred_binds[j];
256                     } else {
257                         replace_binding(pred_binds[j]);
258                     }
259                     continue outer;
260                 }
261             }
263             if (!final_binding && parent_kmap) {
264                 var parent_pred_binds = parent_kmap.predicate_bindings;
265                 parent_kmap = null;
266                 for (j = 0; j < parent_pred_binds.length; j++) {
267                     if (parent_pred_binds[j].key == key &&
268                         parent_pred_binds[j].keymap)
269                     {
270                         parent_kmap = parent_pred_binds[j].keymap;
271                         break;
272                     }
273                 }
274             }
275             // Not already present, must be added
276             pred_binds.push(make_binding());
277         } else {
278             var bindings = kmap.bindings;
279             var binding = bindings[key];
281             if (binding) {
282                 if (final_binding && undefine_key) {
283                     delete bindings[key];
284                 } else {
285                     replace_binding(binding);
286                 }
287                 continue outer;
288             }
290             if (!final_binding) {
291                 let temp_parent = parent_kmap;
292                 parent_kmap = null;
293                 while (temp_parent) {
294                     let p_bindings = temp_parent.bindings;
295                     let p_binding = p_bindings[key];
296                     if (p_binding && p_binding.keymap) {
297                         parent_kmap = p_binding.keymap;
298                         break;
299                     } else {
300                         temp_parent = temp_parent.parent;
301                     }
302                 }
303             }
305             bindings[key] = make_binding();
306         }
307     }
310 // bind key to either the keymap or command in the keymap, kmap
311 // keywords:
313 //  $fallthrough: (bool) let this key be process by the web page
314 //      or gecko.
316 //  $repeat: (commnand name) shortcut command to call if a prefix
317 //      command key is pressed twice in a row.
319 //  $browser_object: (browser object) Override the default
320 //      browser-object for the command.
322 define_keywords("$fallthrough", "$repeat", "$browser_object");
323 function define_key (kmap, keys, cmd) {
324     keywords(arguments);
325     var orig_keys = keys;
326     try {
327         var ref = get_caller_source_code_reference();
329         if (typeof(keys) == "string" && keys.length > 1)
330             keys = keys.split(" ");
332         if (!(typeof(keys) == "object") || !(keys instanceof Array))
333             keys = [keys];
335         // normalize the order of modifiers in string key combos
336         keys = keys.map(
337             function (k) {
338                 if (typeof(k) == "string")
339                     return format_key_combo(unformat_key_combo(k));
340                 else
341                     return k;
342             });
344         var new_command = null, new_keymap = null;
345         if (typeof(cmd) == "string" || typeof(cmd) == "function")
346             new_command = cmd;
347         else if (cmd instanceof keymap)
348             new_keymap = cmd;
349         else if (cmd != null)
350             throw new Error("Invalid `cmd' argument: " + cmd);
352         define_key_internal(ref, kmap, keys, new_command, new_keymap,
353                             forward_keywords(arguments));
355     } catch (e if (typeof(e) == "string")) {
356         dumpln("Warning: Error occurred while binding keys: " + orig_keys);
357         dumpln(e);
358     }
362 function undefine_key (kmap, keys) {
363     define_key(kmap, keys);
368  * Keypress Handler
369  */
371 define_variable("keyboard_key_sequence_help_timeout", 0,
372                 "Delay (in millseconds) before the current key sequence "+
373                 "prefix is displayed in the minibuffer.");
375 define_window_local_hook("keypress_hook", RUN_HOOK_UNTIL_SUCCESS);
379 function copy_event (event) {
380     var ev = {};
381     ev.keyCode = event.keyCode;
382     ev.charCode = event.charCode;
383     ev.ctrlKey = event.ctrlKey;
384     ev.metaKey = event.metaKey;
385     ev.altKey = event.altKey;
386     ev.shiftKey = event.shiftKey;
387     ev.sticky_modifiers = event.sticky_modifiers;
388     return ev;
391 function show_partial_key_sequence (window, state, ctx) {
392     if (!state.help_displayed) {
393         state.help_timer_ID = window.setTimeout(
394             function () {
395                 window.minibuffer.show(ctx.key_sequence.join(" "));
396                 state.help_displayed = true;
397                 state.help_timer_ID = null;
398             }, keyboard_key_sequence_help_timeout);
399     } else
400         window.minibuffer.show(ctx.key_sequence.join(" "));
403 function format_key_combo (event) {
404     var combo = '';
405     for each (var M in modifier_order) {
406         if (modifiers[M].in_event_p(event) ||
407             (event.sticky_modifiers &&
408              event.sticky_modifiers.indexOf(M) != -1))
409         {
410             combo += (M + '-');
411         }
412     }
413     if (event.charCode) {
414         if (event.charCode == 32) {
415             combo += 'space';
416         } else {
417             combo += String.fromCharCode(event.charCode);
418         }
419     } else if (event.keyCode) {
420         combo += keycode_to_vk_name[event.keyCode];
421     }
422     return combo;
425 function unformat_key_combo (combo) {
426     var event = {
427         keyCode: 0,
428         charCode: 0,
429         altKey: false,
430         ctrlKey: false,
431         metaKey: false,
432         shiftKey: false
433     };
434     var M;
435     var i = 0;
436     var len = combo.length - 2;
437     while (i < len && combo[i+1] == '-') {
438         M = combo[i];
439         modifiers[M].set_in_event(event);
440         i+=2;
441     }
442     var key = combo.substring(i);
443     if (key.length == 1) {
444         event.charCode = key.charCodeAt(0);
445     } else if (key == 'space') {
446         event.charCode = 32;
447     } else {
448         event.keyCode = vk_name_to_keycode[key];
449     }
450     return event;
453 function keypress_handler (true_event) {
454     try {
455         var window = this;
456         var state = window.keyboard;
458         var event = copy_event(true_event);
460         /* Filter out events from keys like the Windows/Super/Hyper key */
461         if (event.keyCode == 0 && event.charCode == 0 ||
462             event.keyCode == vk_name_to_keycode.caps_lock)
463             return;
465         if (key_bindings_ignore_capslock && event.charCode) {
466             let c = String.fromCharCode(event.charCode);
467             if (event.shiftKey)
468                 event.charCode = c.toUpperCase().charCodeAt(0);
469             else
470                 event.charCode = c.toLowerCase().charCodeAt(0);
471         }
473         /* Clear minibuffer message */
474         window.minibuffer.clear();
476         var binding = null;
477         var done = true; // flag for end of key sequence
479         if (! state.current_context) {
480             state.current_context = new interactive_context(window.buffers.current);
481             state.current_context.key_sequence = [];
482             state.current_context.sticky_modifiers = 0;
483         }
484         var I = state.current_context;
486         event.sticky_modifiers = I.sticky_modifiers;
487         I.sticky_modifiers = 0;
489         var combo = format_key_combo(event);
490         I.key_sequence.push(combo);
491         I.combo = combo;
492         I.event = event;
494         // keypress_hook is used, for example, by key aliases
495         if (keypress_hook.run(window, I, true_event))
496             return;
498         var top_keymap =
499             state.override_keymap ||
500             window.buffers.current.keymap;
502         var active_keymap =
503             state.active_keymap ||
504             top_keymap;
506         var overlay_keymap = I.overlay_keymap;
508         binding =
509             (overlay_keymap && lookup_key_binding(overlay_keymap, combo, event)) ||
510             lookup_key_binding(active_keymap, combo, event);
512         // Should we stop this event from being processed by the gui?
513         //
514         // 1) we have a binding, and the binding's fallthrough property is not
515         //    true.
516         //
517         // 2) we are in the middle of a key sequence, and we need to say that
518         //    the key sequence given has no command.
519         //
520         if (!binding || !binding.fallthrough) {
521             true_event.preventDefault();
522             true_event.stopPropagation();
523         }
525         // Finally, process the binding.
526         if (binding) {
527             if (binding.browser_object != null)
528                 I.binding_browser_object = binding.browser_object;
529             if (binding.keymap) {
530                 state.active_keymap = binding.keymap;
531                 show_partial_key_sequence(window, state, I);
532                 // We're going for another round
533                 done = false;
534             } else if (binding.command) {
535                 let command = binding.command;
536                 if (I.repeat == command)
537                     command = binding.repeat;
538                 call_interactively(I, command);
539                 if (typeof(command) == "string" &&
540                     interactive_commands.get(command).prefix)
541                 {
542                     state.active_keymap = null;
543                     show_partial_key_sequence(window, state, I);
544                     if (binding.repeat)
545                         I.repeat = command;
546                     done = false;
547                 }
548             }
549         } else {
550             window.minibuffer.message(I.key_sequence.join(" ") + " is undefined");
551         }
553         // Clean up if we're done
554         if (done) {
555             if (state.help_timer_ID != null) {
556                 window.clearTimeout(state.help_timer_ID);
557                 state.help_timer_ID = null;
558             }
559             state.help_displayed = false;
560             state.active_keymap = null;
561             state.current_context = null;
562         }
563     } catch (e) { dump_error(e); }
566 function keyboard (window) {
567     this.window = window;
570 keyboard.prototype = {
571     last_key_down_event : null,
572     current_context : null,
573     active_keymap : null,
574     help_timer_ID : null,
575     help_displayed : false,
577     /* If this is non-null, it is used instead of the current buffer's
578      * keymap. */
579     override_keymap : null,
581     set_override_keymap : function (keymap) {
582         /* Clear out any in-progress key sequence. */
583         this.active_keymap = null;
584         this.current_context = null;
585         if (this.help_timer_ID != null) {
586             this.window.clearTimeout(this.help_timer_ID);
587             this.help_timer_ID = null;
588         }
589         this.override_keymap = keymap;
590     }
594 function keyboard_initialize_window (window) {
595     window.keyboard = new keyboard(window);
596     window.addEventListener("keypress", keypress_handler, true /* capture */,
597                             false /* ignore untrusted events */);
600 add_hook("window_initialize_hook", keyboard_initialize_window);
602 function for_each_key_binding (keymap_or_buffer, callback) {
603     var keymap;
604     if (keymap_or_buffer instanceof conkeror.buffer) {
605         var buffer = keymap_or_buffer;
606         var window = buffer.window;
607         keymap = window.keyboard.override_keymap || buffer.keymap;
608     } else {
609         keymap = keymap_or_buffer;
610     }
611     var keymap_stack = [keymap];
612     var binding_stack = [];
613     function helper2 (bind) {
614         binding_stack.push(bind);
615         callback(binding_stack);
616         if (bind.keymap && keymap_stack.indexOf(bind.keymap) == -1) {
617             keymap_stack.push(bind.keymap);
618             helper();
619             keymap_stack.pop();
620         }
621         binding_stack.pop();
622     }
623     function helper () {
624         while (true) {
625             var keymap = keymap_stack[keymap_stack.length - 1];
626             for (var i in keymap.bindings) {
627                 var b = keymap.bindings[i];
628                 helper2(b);
629             }
630             for (i in  keymap.predicate_bindings) {
631                 var bind = keymap.predicate_bindings[i];
632                 helper2(bind);
633                 var p = bind.key;
634                 if (p == match_any_key)
635                     return;
636             }
637             if (keymap.parent)
638                 keymap_stack[keymap_stack.length - 1] = keymap.parent;
639             else
640                 break;
641         }
642     }
643     helper();
646 function find_command_in_keymap (keymap_or_buffer, command) {
647     var list = [];
649     for_each_key_binding(keymap_or_buffer, function (bind_seq) {
650             var bind = bind_seq[bind_seq.length - 1];
651             if (bind.command == command)
652                 list.push(format_binding_sequence(bind_seq));
653         });
654     return list;
657 define_keymap("key_binding_reader_keymap");
658 define_key(key_binding_reader_keymap, match_any_key, "read-key-binding-key");
660 define_keywords("$buffer", "$keymap");
661 function key_binding_reader (continuation) {
662     keywords(arguments, $prompt = "Describe key:");
664     this.continuation = continuation;
666     if (arguments.$keymap)
667         this.target_keymap = arguments.$keymap;
668     else {
669         var buffer = arguments.$buffer;
670         var window = buffer.window;
671         this.target_keymap = window.keyboard.override_keymap || buffer.keymap;
672     }
674     this.key_sequence = [];
676     minibuffer_input_state.call(this, key_binding_reader_keymap, arguments.$prompt);
678 key_binding_reader.prototype = {
679     __proto__: minibuffer_input_state.prototype,
680     destroy: function () {
681         if (this.continuation)
682             this.continuation.throw(abort());
683     }
686 function invalid_key_binding (seq) {
687     var e = new Error(seq.join(" ") + " is undefined");
688     e.key_sequence = seq;
689     e.__proto__ = invalid_key_binding.prototype;
690     return e;
692 invalid_key_binding.prototype = {
693     __proto__: interactive_error.prototype
696 function read_key_binding_key (window, state, event) {
697     var combo = format_key_combo(event);
698     var binding = lookup_key_binding(state.target_keymap, combo, event);
700     state.key_sequence.push(combo);
702     if (binding == null) {
703         var c = state.continuation;
704         delete state.continuation;
705         window.minibuffer.pop_state();
706         c.throw(invalid_key_binding(state.key_sequence));
707         return;
708     }
710     if (binding.keymap) {
711         window.minibuffer._restore_normal_state();
712         window.minibuffer._input_text = state.key_sequence.join(" ") + " ";
713         state.target_keymap = binding.keymap;
714         return;
715     }
717     var c = state.continuation;
718     delete state.continuation;
720     window.minibuffer.pop_state();
722     if (c != null)
723         c([state.key_sequence, binding]);
725 interactive("read-key-binding-key", null, function (I) {
726     read_key_binding_key(I.window, I.minibuffer.check_state(key_binding_reader), I.event);
729 minibuffer.prototype.read_key_binding = function () {
730     keywords(arguments);
731     var s = new key_binding_reader((yield CONTINUATION), forward_keywords(arguments));
732     this.push_state(s);
733     var result = yield SUSPEND;
734     yield co_return(result);