Debian package: Calling a symlinked xulrunner-stub
[conkeror.git] / modules / keyboard.js
blob111eba1003d9fa520897cb195b9cf93943c8c607
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.shiftKey);
57                     },
58                     function (event) { event.shiftKey = true; })
60 var modifier_order = ['C', 'M', 'S'];
62 // check the platform and guess whether we should treat Alt as Meta
63 if (get_os() == 'Darwin') {
64     // In OS X, alt is a shift-like modifier, in that we
65     // only care about it for non-character events.
66     modifiers.A = new modifier(
67         function (event) {
68             return (event.keyCode &&
69                     event.charCode == 0 &&
70                     event.altKey);
71         },
72         function (event) { event.altKey = true; });
73     modifier_order = ['C', 'M', 'A', 'S'];
74 } else {
75     modifiers.M = modifiers.A;
81  * Keymap datatype
82  */
84 define_keywords("$parent", "$help", "$name", "$anonymous");
85 function keymap () {
86     keywords(arguments);
87     this.parent = arguments.$parent;
88     this.bindings = {};
89     this.predicate_bindings = [];
90     this.help = arguments.$help;
91     this.name = arguments.$name;
92     this.anonymous = arguments.$anonymous;
95 function define_keymap (name) {
96     keywords(arguments);
97     this[name] = new keymap($name = name, forward_keywords(arguments));
103  * Key Match Predicates.
105  *  Predicate bindings are tried for a match after the ordinary key-combo
106  * bindings.  They are predicate functions on the keypress event object.
107  * When such a predicate returns a true value, its associated command,
108  * keymap, or fallthrough declaration is performed.
109  */
111 function define_key_match_predicate (name, description, predicate) {
112     conkeror[name] = predicate;
113     conkeror[name].name = name;
114     conkeror[name].description = description;
117 define_key_match_predicate('match_any_key', 'any key',
118     function (event) { return true; });
120 define_key_match_predicate('match_any_unmodified_key', 'any unmodified key',
121     function (event) {
122         //XXX: the meaning of "unmodified" is platform dependent. for
123         // example, on OS X, Alt is used in combination with the
124         // character keys to type an alternate character.  A possible
125         // solution is to set the altKey property of the event to null
126         // for all keypress events on OS X.
127         try {
128             return event.charCode
129                 && !event.altKey
130                 && !event.metaKey
131                 && !event.ctrlKey
132                 && !event.sticky_modifiers;
133         } catch (e) {return false; }
134     });
138  */
140 function format_key_spec (key) {
141     if (key instanceof Function) {
142         if (key.description)
143             return "<"+key.description+">";
144         if (key.name)
145             return "<"+key.name+">";
146         return "<anonymous match function>";
147     }
148     return key;
151 function format_binding_sequence (seq) {
152     return seq.map(function (x) {
153             return format_key_spec(x.key);
154         }).join(" ");
158 function lookup_key_binding (kmap, combo, event) {
159     do {
160         // Check if the key matches the keycode table
161         // var mods = get_modifiers(event);
162         var bindings = kmap.bindings;
163         var bind;
164         if ((bind = bindings[combo]) != null)
165             return bind;
167         // Check if the key matches a predicate
168         var pred_binds = kmap.predicate_bindings;
169         for (var i = 0; i < pred_binds.length; ++i) {
170             bind = pred_binds[i];
171             if (bind.key(event))
172                 return bind;
173         }
174         kmap = kmap.parent;
175     } while (kmap);
176     return null;
181  * $fallthrough, $repeat and $browser_object are as for define_key.
183  * ref is the source code reference of the call to define_key.
185  * kmap is the keymap in which the binding is to be defined.
187  * keys is the key sequence being bound.  it may be necessary
188  * to auto-generate new keymaps to accomodate the key sequence.
190  * only one of new_command and new_keymap will be given.
191  * the one that is given is the thing being bound to.
192  */
193 define_keywords("$fallthrough", "$repeat", "$browser_object");
194 function define_key_internal (ref, kmap, keys, new_command, new_keymap) {
195     keywords(arguments);
196     var args = arguments;
197     var parent_kmap = kmap.parent;
198     var final_binding; // flag to indicate the final key combo in the sequence.
199     var key; // current key combo as we iterate through the sequence.
200     var undefine_key = (new_command == null) &&
201         (new_keymap == null) &&
202         (! args.$fallthrough);
204     /* Replace `bind' with the binding specified by (cmd, fallthrough) */
205     function replace_binding (bind) {
206         if (final_binding) {
207             bind.command = new_command;
208             bind.keymap = new_keymap;
209             bind.fallthrough = args.$fallthrough;
210             bind.source_code_reference = ref;
211             bind.repeat = args.$repeat;
212             bind.browser_object = args.$browser_object;
213         } else {
214             if (!bind.keymap)
215                 throw new Error("Key sequence has a non-keymap in prefix");
216             kmap = bind.keymap;
217         }
218     }
220     function make_binding () {
221         if (final_binding) {
222             return {key: key,
223                     fallthrough: args.$fallthrough,
224                     command: new_command,
225                     keymap: new_keymap,
226                     source_code_reference: ref,
227                     repeat: args.$repeat,
228                     browser_object: args.$browser_object,
229                     bound_in: kmap};
230         } else {
231             let old_kmap = kmap;
232             // Check for a corresponding binding a parent
233             kmap = new keymap($parent = parent_kmap, $anonymous,
234                               $name = old_kmap.name + " " + format_key_spec(key));
235             kmap.bound_in = old_kmap;
236             return {key: key,
237                     keymap: kmap,
238                     source_code_reference: ref,
239                     bound_in: old_kmap};
240         }
241     }
243 outer:
244     for (var i = 0; i < keys.length; ++i) {
245         key = keys[i];
246         final_binding = (i == keys.length - 1);
248         // Check if the specified binding is already present in the kmap
249         if (typeof(key) == "function") { // it's a match predicate
250             var pred_binds = kmap.predicate_bindings;
251             for (var j = 0; j < pred_binds.length; j++) {
252                 if (pred_binds[j].key == key) {
253                     if (final_binding && undefine_key) {
254                         delete pred_binds[j];
255                     } else {
256                         replace_binding(pred_binds[j]);
257                     }
258                     continue outer;
259                 }
260             }
262             if (!final_binding && parent_kmap) {
263                 var parent_pred_binds = parent_kmap.predicate_bindings;
264                 parent_kmap = null;
265                 for (j = 0; j < parent_pred_binds.length; j++) {
266                     if (parent_pred_binds[j].key == key &&
267                         parent_pred_binds[j].keymap)
268                     {
269                         parent_kmap = parent_pred_binds[j].keymap;
270                         break;
271                     }
272                 }
273             }
274             // Not already present, must be added
275             pred_binds.push(make_binding());
276         } else {
277             var bindings = kmap.bindings;
278             var binding = bindings[key];
280             if (binding) {
281                 if (final_binding && undefine_key) {
282                     delete bindings[key];
283                 } else {
284                     replace_binding(binding);
285                 }
286                 continue outer;
287             }
289             if (!final_binding) {
290                 let temp_parent = parent_kmap;
291                 parent_kmap = null;
292                 while (temp_parent) {
293                     let p_bindings = temp_parent.bindings;
294                     let p_binding = p_bindings[key];
295                     if (p_binding && p_binding.keymap) {
296                         parent_kmap = p_binding.keymap;
297                         break;
298                     } else {
299                         temp_parent = temp_parent.parent;
300                     }
301                 }
302             }
304             bindings[key] = make_binding();
305         }
306     }
309 // bind key to either the keymap or command in the keymap, kmap
310 // keywords:
312 //  $fallthrough: (bool) let this key be process by the web page
313 //      or gecko.
315 //  $repeat: (commnand name) shortcut command to call if a prefix
316 //      command key is pressed twice in a row.
318 //  $browser_object: (browser object) Override the default
319 //      browser-object for the command.
321 define_keywords("$fallthrough", "$repeat", "$browser_object");
322 function define_key (kmap, keys, cmd) {
323     keywords(arguments);
324     var orig_keys = keys;
325     try {
326         var ref = get_caller_source_code_reference();
328         if (typeof(keys) == "string" && keys.length > 1)
329             keys = keys.split(" ");
331         if (!(typeof(keys) == "object") || !(keys instanceof Array))
332             keys = [keys];
334         // normalize the order of modifiers in string key combos
335         keys = keys.map(
336             function (k) {
337                 if (typeof(k) == "string")
338                     return format_key_combo(unformat_key_combo(k));
339                 else
340                     return k;
341             });
343         var new_command = null, new_keymap = null;
344         if (typeof(cmd) == "string" || typeof(cmd) == "function")
345             new_command = cmd;
346         else if (cmd instanceof keymap)
347             new_keymap = cmd;
348         else if (cmd != null)
349             throw new Error("Invalid `cmd' argument: " + cmd);
351         define_key_internal(ref, kmap, keys, new_command, new_keymap,
352                             forward_keywords(arguments));
354     } catch (e if (typeof(e) == "string")) {
355         dumpln("Warning: Error occurred while binding keys: " + orig_keys);
356         dumpln(e);
357     }
361 function undefine_key (kmap, keys) {
362     define_key(kmap, keys);
367  * Keypress Handler
368  */
370 define_variable("keyboard_key_sequence_help_timeout", 0,
371                 "Delay (in millseconds) before the current key sequence "+
372                 "prefix is displayed in the minibuffer.");
374 define_window_local_hook("keypress_hook", RUN_HOOK_UNTIL_SUCCESS);
378 function copy_event (event) {
379     var ev = {};
380     ev.keyCode = event.keyCode;
381     ev.charCode = event.charCode;
382     ev.ctrlKey = event.ctrlKey;
383     ev.metaKey = event.metaKey;
384     ev.altKey = event.altKey;
385     ev.shiftKey = event.shiftKey;
386     ev.sticky_modifiers = event.sticky_modifiers;
387     return ev;
390 function show_partial_key_sequence (window, state, ctx) {
391     if (!state.help_displayed)
392     {
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     }
400     else
401         window.minibuffer.show(ctx.key_sequence.join(" "));
404 function format_key_combo (event) {
405     var combo = '';
406     for each (var M in modifier_order) {
407         if (modifiers[M].in_event_p(event) ||
408             (event.sticky_modifiers &&
409              event.sticky_modifiers.indexOf(M) != -1))
410         {
411             combo += (M + '-');
412         }
413     }
414     if (event.charCode) {
415         if (event.charCode == 32) {
416             combo += 'space';
417         } else {
418             combo += String.fromCharCode(event.charCode);
419         }
420     } else if (event.keyCode) {
421         combo += keycode_to_vk_name[event.keyCode];
422     }
423     return combo;
426 function unformat_key_combo (combo) {
427     var event = {
428         keyCode: 0,
429         charCode: 0,
430         altKey: false,
431         ctrlKey: false,
432         metaKey: false,
433         shiftKey: false
434     };
435     var M;
436     var i = 0;
437     var len = combo.length - 2;
438     while (i < len && combo[i+1] == '-') {
439         M = combo[i];
440         modifiers[M].set_in_event(event);
441         i+=2;
442     }
443     var key = combo.substring(i);
444     if (key.length == 1) {
445         event.charCode = key.charCodeAt(0);
446     } else if (key == 'space') {
447         event.charCode = 32;
448     } else {
449         event.keyCode = vk_name_to_keycode[key];
450     }
451     return event;
454 function keypress_handler (true_event) {
455     try {
456         var window = this;
457         var state = window.keyboard;
459         var event = copy_event(true_event);
461         /* Filter out events from keys like the Windows/Super/Hyper key */
462         if (event.keyCode == 0 && event.charCode == 0 ||
463             event.keyCode == vk_name_to_keycode.caps_lock)
464             return;
466         if (key_bindings_ignore_capslock && event.charCode) {
467             let c = String.fromCharCode(event.charCode);
468             if (event.shiftKey)
469                 event.charCode = c.toUpperCase().charCodeAt(0);
470             else
471                 event.charCode = c.toLowerCase().charCodeAt(0);
472         }
474         /* Clear minibuffer message */
475         window.minibuffer.clear();
477         var binding = null;
478         var done = true; // flag for end of key sequence
480         if (! state.current_context) {
481             state.current_context = new interactive_context(window.buffers.current);
482             state.current_context.key_sequence = [];
483             state.current_context.sticky_modifiers = 0;
484         }
485         var I = state.current_context;
487         event.sticky_modifiers = I.sticky_modifiers;
488         I.sticky_modifiers = 0;
490         var combo = format_key_combo(event);
491         I.key_sequence.push(combo);
492         I.combo = combo;
493         I.event = event;
495         // keypress_hook is used, for example, by key aliases
496         if (keypress_hook.run(window, I, true_event))
497             return;
499         var top_keymap =
500             state.override_keymap ||
501             window.buffers.current.keymap;
503         var active_keymap =
504             state.active_keymap ||
505             top_keymap;
507         var overlay_keymap = I.overlay_keymap;
509         binding =
510             (overlay_keymap && lookup_key_binding(overlay_keymap, combo, event)) ||
511             lookup_key_binding(active_keymap, combo, event);
513         // Should we stop this event from being processed by the gui?
514         //
515         // 1) we have a binding, and the binding's fallthrough property is not
516         //    true.
517         //
518         // 2) we are in the middle of a key sequence, and we need to say that
519         //    the key sequence given has no command.
520         //
521         if (!binding || !binding.fallthrough) {
522             true_event.preventDefault();
523             true_event.stopPropagation();
524         }
526         // Finally, process the binding.
527         if (binding) {
528             if (binding.browser_object != null)
529                 I.binding_browser_object = binding.browser_object;
530             if (binding.keymap) {
531                 state.active_keymap = binding.keymap;
532                 show_partial_key_sequence(window, state, I);
533                 // We're going for another round
534                 done = false;
535             } else if (binding.command) {
536                 let command = binding.command;
537                 if (I.repeat == command)
538                     command = binding.repeat;
539                 call_interactively(I, command);
540                 if (typeof(command) == "string" &&
541                     interactive_commands.get(command).prefix)
542                 {
543                     state.active_keymap = null;
544                     show_partial_key_sequence(window, state, I);
545                     if (binding.repeat)
546                         I.repeat = command;
547                     done = false;
548                 }
549             }
550         } else {
551             window.minibuffer.message(I.key_sequence.join(" ") + " is undefined");
552         }
554         // Clean up if we're done
555         if (done) {
556             if (state.help_timer_ID != null) {
557                 window.clearTimeout(state.help_timer_ID);
558                 state.help_timer_ID = null;
559             }
560             state.help_displayed = false;
561             state.active_keymap = null;
562             state.current_context = null;
563         }
564     } catch (e) { dump_error(e); }
567 function keyboard(window)
569     this.window = window;
572 keyboard.prototype = {
573     last_key_down_event : null,
574     current_context : null,
575     active_keymap : null,
576     help_timer_ID : null,
577     help_displayed : false,
579     /* If this is non-null, it is used instead of the current buffer's
580      * keymap. */
581     override_keymap : null,
583     set_override_keymap : function (keymap) {
584         /* Clear out any in-progress key sequence. */
585         this.active_keymap = null;
586         this.current_context = null;
587         if (this.help_timer_ID != null)
588         {
589             this.window.clearTimeout(this.help_timer_ID);
590             this.help_timer_ID = null;
591         }
592         this.override_keymap = keymap;
593     }
597 function keyboard_initialize_window(window)
599     window.keyboard = new keyboard(window);
601     window.addEventListener ("keypress", keypress_handler, true /* capture */,
602                             false /* ignore untrusted events */);
605 add_hook("window_initialize_hook", keyboard_initialize_window);
607 function for_each_key_binding(keymap_or_buffer, callback) {
608     var keymap;
609     if (keymap_or_buffer instanceof conkeror.buffer) {
610         var buffer = keymap_or_buffer;
611         var window = buffer.window;
612         keymap = window.keyboard.override_keymap || buffer.keymap;
613     } else {
614         keymap = keymap_or_buffer;
615     }
616     var keymap_stack = [keymap];
617     var binding_stack = [];
618     function helper2(bind) {
619         binding_stack.push(bind);
620         callback(binding_stack);
621         if (bind.keymap && keymap_stack.indexOf(bind.keymap) == -1) {
622             keymap_stack.push(bind.keymap);
623             helper();
624             keymap_stack.pop();
625         }
626         binding_stack.pop();
627     }
628     function helper() {
629         while (true) {
630             var keymap = keymap_stack[keymap_stack.length - 1];
631             for (var i in keymap.bindings) {
632                 var b = keymap.bindings[i];
633                 helper2(b);
634             }
635             for (i in  keymap.predicate_bindings) {
636                 var bind = keymap.predicate_bindings[i];
637                 helper2(bind);
638                 var p = bind.key;
639                 if (p == match_any_key)
640                     return;
641             }
642             if (keymap.parent)
643                 keymap_stack[keymap_stack.length - 1] = keymap.parent;
644             else
645                 break;
646         }
647     }
648     helper();
651 function find_command_in_keymap(keymap_or_buffer, command) {
652     var list = [];
654     for_each_key_binding(keymap_or_buffer, function (bind_seq) {
655             var bind = bind_seq[bind_seq.length - 1];
656             if (bind.command == command)
657                 list.push(format_binding_sequence(bind_seq));
658         });
659     return list;
662 define_keymap("key_binding_reader_keymap");
663 define_key(key_binding_reader_keymap, match_any_key, "read-key-binding-key");
665 define_keywords("$buffer", "$keymap");
666 function key_binding_reader(continuation) {
667     keywords(arguments, $prompt = "Describe key:");
669     this.continuation = continuation;
671     if (arguments.$keymap)
672         this.target_keymap = arguments.$keymap;
673     else {
674         var buffer = arguments.$buffer;
675         var window = buffer.window;
676         this.target_keymap = window.keyboard.override_keymap || buffer.keymap;
677     }
679     this.key_sequence = [];
681     minibuffer_input_state.call(this, key_binding_reader_keymap, arguments.$prompt);
683 key_binding_reader.prototype = {
684     __proto__: minibuffer_input_state.prototype,
685     destroy: function () {
686         if (this.continuation)
687             this.continuation.throw(abort());
688     }
691 function invalid_key_binding(seq) {
692     var e = new Error(seq.join(" ") + " is undefined");
693     e.key_sequence = seq;
694     e.__proto__ = invalid_key_binding.prototype;
695     return e;
697 invalid_key_binding.prototype = {
698     __proto__: interactive_error.prototype
701 function read_key_binding_key(window, state, event) {
702     var combo = format_key_combo(event);
703     var binding = lookup_key_binding(state.target_keymap, combo, event);
705     state.key_sequence.push(combo);
707     if (binding == null) {
708         var c = state.continuation;
709         delete state.continuation;
710         window.minibuffer.pop_state();
711         c.throw(invalid_key_binding(state.key_sequence));
712         return;
713     }
715     if (binding.keymap) {
716         window.minibuffer._restore_normal_state();
717         window.minibuffer._input_text = state.key_sequence.join(" ") + " ";
718         state.target_keymap = binding.keymap;
719         return;
720     }
722     var c = state.continuation;
723     delete state.continuation;
725     window.minibuffer.pop_state();
727     if (c != null)
728         c([state.key_sequence, binding]);
730 interactive("read-key-binding-key", null, function (I) {
731     read_key_binding_key(I.window, I.minibuffer.check_state(key_binding_reader), I.event);
734 minibuffer.prototype.read_key_binding = function () {
735     keywords(arguments);
736     var s = new key_binding_reader((yield CONTINUATION), forward_keywords(arguments));
737     this.push_state(s);
738     var result = yield SUSPEND;
739     yield co_return(result);