keymap.js: remove an incorrect comment
[conkeror.git] / modules / keymap.js
blob7f8f0e5991ebd7bfead97768c1c595d7806abeee
1 /**
2  * (C) Copyright 2004-2007 Shawn Betts
3  * (C) Copyright 2007-2010 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 in_module(null);
12 /* Generate vk name table  */
13 var keycode_to_vk_name = [];
14 var vk_name_to_keycode = {};
15 let (KeyEvent = Ci.nsIDOMKeyEvent,
16      prefix = "DOM_VK_") {
17     for (var i in KeyEvent) {
18         /* Check if this is a key binding */
19         if (i.substr(0, prefix.length) == prefix) {
20             let name = i.substr(prefix.length).toLowerCase();
21             let code = KeyEvent[i];
22             keycode_to_vk_name[code] = name;
23             vk_name_to_keycode[name] = code;
24         }
25     }
30  * Modifiers
31  */
33 function modifier (in_event_p, set_in_event) {
34     this.in_event_p = in_event_p;
35     this.set_in_event = set_in_event;
38 var modifiers = {
39     A: new modifier(function (event) { return event.altKey; },
40                     function (event) { event.altKey = true; }),
41     C: new modifier(function (event) { return event.ctrlKey; },
42                     function (event) { event.ctrlKey = true; }),
43     M: new modifier(function (event) { return event.metaKey; },
44                     function (event) { event.metaKey = true; }),
45     S: new modifier(function (event) {
46                         if (event.shiftKey) {
47                             var name;
48                             if (event.keyCode)
49                                 name = keycode_to_vk_name[event.keyCode];
50                             return ((name && name.length > 1) ||
51                                     (event.charCode == 32) ||
52                                     (event.button != null));
53                         }
54                         return false;
55                     },
56                     function (event) { event.shiftKey = true; })
58 var modifier_order = ['C', 'M', 'S'];
60 // check the platform and guess whether we should treat Alt as Meta
61 if (get_os() == 'Darwin') {
62     // In OS X, alt is a shift-like modifier, in that we
63     // only care about it for non-character events.
64     modifiers.A = new modifier(
65         function (event) {
66             if (event.altKey) {
67                 var name;
68                 if (event.keyCode)
69                     name = keycode_to_vk_name[event.keyCode];
70                 return ((name && name.length > 1) ||
71                         (event.charCode == 32) ||
72                         (event.button != null));
73             }
74             return false;
75         },
76         function (event) { event.altKey = true; });
77     modifier_order = ['C', 'M', 'A', 'S'];
78 } else {
79     modifiers.M = modifiers.A;
85  * Combos
86  */
88 function format_key_combo (event) {
89     var combo = '';
90     for each (var M in modifier_order) {
91         if (modifiers[M].in_event_p(event) ||
92             (event.sticky_modifiers &&
93              event.sticky_modifiers.indexOf(M) != -1))
94         {
95             combo += (M + '-');
96         }
97     }
98     if (event.charCode) {
99         if (event.charCode == 32)
100             combo += 'space';
101         else
102             combo += String.fromCharCode(event.charCode);
103     } else if (event.keyCode) {
104         combo += keycode_to_vk_name[event.keyCode];
105     } else if (event.button != null) {
106         combo += "mouse" + (event.button + 1);
107     } else if (event.command) {
108         combo += "cmd" + event.command.toLowerCase();
109     }
110     return combo;
113 function unformat_key_combo (combo) {
114     var event = {
115         keyCode: 0,
116         charCode: 0,
117         button: null,
118         command: null,
119         altKey: false,
120         ctrlKey: false,
121         metaKey: false,
122         shiftKey: false
123     };
124     var M;
125     var i = 0;
126     var len = combo.length - 2;
127     var res;
128     while (i < len && combo[i+1] == '-') {
129         M = combo[i];
130         modifiers[M].set_in_event(event);
131         i+=2;
132     }
133     var key = combo.substring(i);
134     if (key.length == 1)
135         event.charCode = key.charCodeAt(0);
136     else if (key == 'space')
137         event.charCode = 32;
138     else if (key.substring(0, 3) == 'cmd')
139         event.command = key.substr(3, 1).toUpperCase() +
140             key.substring(4);
141     else if (vk_name_to_keycode[key])
142         event.keyCode = vk_name_to_keycode[key];
143     else if (key.substring(0, 5) == 'mouse')
144         event.button = parseInt(key.substring(5));
145     return event;
151  * Keymap datatype
152  */
154 define_keywords("$parent", "$help", "$name", "$anonymous",
155                 "$display_name", "$notify");
156 function keymap () {
157     keywords(arguments);
158     this.parent = arguments.$parent;
159     this.bindings = {};
160     this.predicate_bindings = [];
161     this.fallthrough = [];
162     this.help = arguments.$help;
163     this.name = arguments.$name;
164     this.display_name = arguments.$display_name;
165     this.notify = arguments.$notify;
166     this.anonymous = arguments.$anonymous;
169 function define_keymap (name) {
170     keywords(arguments);
171     this[name] = new keymap($name = name, forward_keywords(arguments));
177  * Key Match Predicates.
178  */
180 function define_key_match_predicate (name, description, predicate) {
181     conkeror[name] = predicate;
182     conkeror[name].name = name;
183     conkeror[name].description = description;
184     return conkeror[name];
187 define_key_match_predicate('match_any_key', 'any key',
188     function (event) true);
190 // should be renamed to match_any_unmodified_character
191 define_key_match_predicate('match_any_unmodified_character', 'any unmodified character',
192     function (event) {
193         // this predicate can be used for both keypress and keydown
194         // fallthroughs.
195         try {
196             return ((event.type == 'keypress' && event.charCode)
197                     || event.keyCode > 31)
198                 && !modifiers.A.in_event_p(event)
199                 && !event.metaKey
200                 && !event.ctrlKey
201                 && !event.sticky_modifiers;
202         } catch (e) { return false; }
203     });
205 define_key_match_predicate('match_checkbox_keys', 'checkbox keys',
206     function (event) {
207         return event.keyCode == 32
208             && !event.shiftKey
209             && !event.metaKey
210             && !event.altKey
211             && !event.ctrlKey;
212         //XXX: keycode fallthroughs don't support sticky modifiers
213     });
215 define_key_match_predicate('match_text_keys', 'text editing keys',
216     function (event) {
217         return ((event.type == 'keypress' && event.charCode)
218                 || event.keyCode == 13 || event.keyCode > 31)
219             && !event.ctrlKey
220             && !event.metaKey
221             && !modifiers.A.in_event_p(event);
222         //XXX: keycode fallthroughs don't support sticky modifiers
223     });
225 define_key_match_predicate('match_not_escape_key', 'any key but escape',
226     function (event) {
227         return event.keyCode != 27 ||
228              event.shiftKey ||
229              event.altKey ||
230              event.metaKey || // M-escape can also leave this mode, so we
231                               // need to use an accurate determination of
232                               // whether the "M" modifier was pressed,
233                               // which is not necessarily the same as
234                               // event.metaKey.
235              event.ctrlKey;
236     });
240  */
242 function format_key_spec (key) {
243     if (key instanceof Function) {
244         if (key.description)
245             return "<"+key.description+">";
246         if (key.name)
247             return "<"+key.name+">";
248         return "<anonymous match function>";
249     }
250     return key;
253 function format_binding_sequence (seq) {
254     return seq.map(function (x) {
255             return format_key_spec(x.key);
256         }).join(" ");
260 function keymap_lookup (keymaps, combo, event) {
261     var i = keymaps.length - 1;
262     if (i < 0)
263         return null;
264     var kmap = keymaps[i];
265     var new_kmaps;
266     while (true) {
267         var bindings = kmap.bindings;
268         var bind = bindings[combo];
269         if (new_kmaps) {
270             if (bind) {
271                 if (bind.keymap)
272                     new_kmaps.unshift(bind.keymap);
273                 else
274                     return new_kmaps;
275             }
276         } else if (bind) {
277             if (bind.keymap)
278                 new_kmaps = [bind.keymap];
279             else
280                 return bind;
281         } else {
282             var pred_binds = kmap.predicate_bindings;
283             for (var j = 0, pblen = pred_binds.length; j < pblen; ++j) {
284                 bind = pred_binds[j];
285                 if (bind.key(event))
286                     return bind;
287             }
288         }
289         if (kmap.parent)
290             kmap = kmap.parent;
291         else if (i > 0)
292             kmap = keymaps[--i];
293         else if (new_kmaps)
294             return new_kmaps
295         else
296             return null;
297     }
301 function keymap_lookup_fallthrough (keymap, event) {
302     var predicates = keymap.fallthrough;
303     for (var i = 0, plen = predicates.length; i < plen; ++i) {
304         if (predicates[i](event))
305             return true;
306     }
307     return false;
311 function for_each_key_binding (keymaps, callback) {
312     var binding_sequence = [];
313     var in_keymaps = [];
314     function helper (keymap) {
315         if (in_keymaps.indexOf(keymap) >= 0)
316             return;
317         in_keymaps.push(keymap);
318         var binding;
319         for each (binding in keymap.bindings) {
320             binding_sequence.push(binding);
321             callback(binding_sequence);
322             if (binding.keymap)
323                 helper(binding.keymap);
324             binding_sequence.pop();
325         }
326         for each (binding in keymap.predicate_bindings) {
327             binding_sequence.push(binding);
328             callback(binding_sequence);
329             if (binding.keymap)
330                 helper(binding.keymap);
331             binding_sequence.pop();
332         }
333         in_keymaps.pop();
334     }
335     //outer loop is to go down the parent-chain of keymaps
336     var i = keymaps.length - 1;
337     var keymap = keymaps[i];
338     while (true) {
339         helper(keymap);
340         if (keymap.parent)
341             keymap = keymap.parent;
342         else if (i > 0)
343             keymap = keymaps[--i];
344         else
345             break;
346     }
350 function keymap_lookup_command (keymaps, command) {
351     var list = [];
352     for_each_key_binding(keymaps, function (bind_seq) {
353             var bind = bind_seq[bind_seq.length - 1];
354             if (bind.command == command)
355                 list.push(format_binding_sequence(bind_seq));
356         });
357     return list;
366  * $fallthrough, $repeat and $browser_object are as for define_key.
368  * ref is the source code reference of the call to define_key.
370  * kmap is the keymap in which the binding is to be defined.
372  * seq is the key sequence being bound.  it may be necessary
373  * to auto-generate new keymaps to accomodate the key sequence.
375  * only one of new_command and new_keymap will be given.
376  * the one that is given is the thing being bound to.
377  */
378 define_keywords("$fallthrough", "$repeat", "$browser_object");
379 function define_key_internal (ref, kmap, seq, new_command, new_keymap) {
380     keywords(arguments);
381     var args = arguments;
382     var last_in_sequence; // flag to indicate the final key combo in the sequence.
383     var key; // current key combo as we iterate through the sequence.
384     var undefine_key = (new_command == null) &&
385         (new_keymap == null) &&
386         (! args.$fallthrough);
388     /* Replace `bind' with the binding specified by (cmd, fallthrough) */
389     function replace_binding (bind) {
390         if (last_in_sequence) {
391             bind.command = new_command;
392             bind.keymap = new_keymap;
393             bind.fallthrough = args.$fallthrough;
394             bind.source_code_reference = ref;
395             bind.repeat = args.$repeat;
396             bind.browser_object = args.$browser_object;
397         } else {
398             if (!bind.keymap)
399                 throw new Error("Key sequence has a non-keymap in prefix");
400             kmap = bind.keymap;
401         }
402     }
404     function make_binding () {
405         if (last_in_sequence) {
406             return { key: key,
407                      fallthrough: args.$fallthrough,
408                      command: new_command,
409                      keymap: new_keymap,
410                      source_code_reference: ref,
411                      repeat: args.$repeat,
412                      browser_object: args.$browser_object,
413                      bound_in: kmap };
414         } else {
415             let old_kmap = kmap;
416             kmap = new keymap($anonymous,
417                               $name = old_kmap.name + " " + format_key_spec(key));
418             kmap.bound_in = old_kmap;
419             return { key: key,
420                      keymap: kmap,
421                      source_code_reference: ref,
422                      bound_in: old_kmap };
423         }
424     }
426 sequence:
427     for (var i = 0, slen = seq.length; i < slen; ++i) {
428         key = seq[i];
429         last_in_sequence = (i == slen - 1);
431         if (typeof key == "function") { // it's a match predicate
432             // Check if the binding is already present in the keymap
433             var pred_binds = kmap.predicate_bindings;
434             for (var j = 0, pblen = pred_binds.length; j < pblen; j++) {
435                 if (pred_binds[j].key == key) {
436                     if (last_in_sequence && undefine_key)
437                         pred_binds.splice(j, 1);
438                     else
439                         replace_binding(pred_binds[j]);
440                     continue sequence;
441                 }
442             }
443             if (! undefine_key)
444                 pred_binds.push(make_binding());
445         } else {
446             // Check if the binding is already present in the keymap
447             var bindings = kmap.bindings;
448             var binding = bindings[key];
449             if (binding) {
450                 if (last_in_sequence && undefine_key)
451                     delete bindings[key];
452                 else
453                     replace_binding(binding);
454                 continue sequence;
455             }
456             if (! undefine_key)
457                 bindings[key] = make_binding();
458         }
459     }
463  * bind SEQ to a keymap or command CMD in keymap KMAP.
465  * keywords:
467  *  $fallthrough: specifies that the keypress event will fall through
468  *      to gecko.  Note, by this method, only keypress will fall through.
469  *      Keyup and keydown will still be blocked.
471  *  $repeat: (command name) shortcut command to call if a prefix
472  *      command key is pressed twice in a row.
474  *  $browser_object: (browser object) Override the default
475  *      browser-object for the command.
476  */
477 define_keywords("$fallthrough", "$repeat", "$browser_object");
478 function define_key (kmap, seq, cmd) {
479     keywords(arguments);
480     var orig_seq = seq;
481     try {
482         var ref = get_caller_source_code_reference();
484         if (typeof seq == "string" && seq.length > 1)
485             seq = seq.split(" ");
487         if (!(typeof seq == "object") || !(seq instanceof Array))
488             seq = [seq];
490         // normalize the order of modifiers in key combos
491         seq = seq.map(
492             function (k) {
493                 if (typeof(k) == "string")
494                     return format_key_combo(unformat_key_combo(k));
495                 else
496                     return k;
497             });
499         var new_command = null;
500         var new_keymap = null;
501         if (typeof cmd == "string" || typeof cmd == "function")
502             new_command = cmd;
503         else if (cmd instanceof keymap)
504             new_keymap = cmd;
505         else if (cmd != null)
506             throw new Error("Invalid `cmd' argument: " + cmd);
508         define_key_internal(ref, kmap, seq, new_command, new_keymap,
509                             forward_keywords(arguments));
511     } catch (e if (typeof e == "string")) {
512         dumpln("Warning: Error occurred while binding sequence: " + orig_seq);
513         dumpln(e);
514     }
518 function undefine_key (kmap, seq) {
519     define_key(kmap, seq);
524  * define_fallthrough
526  *   Takes a keymap and a predicate on an event.  Fallthroughs defined by
527  * these means will cause all three of keydown, keypress, and keyup to
528  * fall through to gecko, whereas those defined by the $fallthrough
529  * keyword to define_key only affect keypress events.
531  *   The limitations of this method are that only the keyCode is available
532  * to the predicate, not the charCode, and keymap inheritance is not
533  * available for these "bindings".
534  */
535 function define_fallthrough (keymap, predicate) {
536     keymap.fallthrough.push(predicate);
541  * Minibuffer Read Key Binding
542  */
544 define_keymap("key_binding_reader_keymap");
545 define_key(key_binding_reader_keymap, match_any_key, "read-key-binding-key");
547 define_keywords("$keymap");
548 function key_binding_reader (minibuffer, continuation) {
549     keywords(arguments, $prompt = "Describe key:");
550     minibuffer_input_state.call(this, minibuffer, key_binding_reader_keymap, arguments.$prompt);
551     this.continuation = continuation;
552     if (arguments.$keymap)
553         this.target_keymap = arguments.$keymap;
554     else
555         this.target_keymap = get_current_keymaps(this.minibuffer.window).slice();
556     this.key_sequence = [];
558 key_binding_reader.prototype = {
559     constructor: key_binding_reader,
560     __proto__: minibuffer_input_state.prototype,
561     destroy: function () {
562         if (this.continuation)
563             this.continuation.throw(abort());
564         minibuffer_input_state.prototype.destroy.call(this);
565     }
568 function invalid_key_binding (seq) {
569     var e = new Error(seq.join(" ") + " is undefined");
570     e.key_sequence = seq;
571     e.__proto__ = invalid_key_binding.prototype;
572     return e;
574 invalid_key_binding.prototype = {
575     constructor: invalid_key_binding,
576     __proto__: interactive_error.prototype
579 function read_key_binding_key (window, state, event) {
580     var combo = format_key_combo(event);
581     var binding = keymap_lookup(state.target_keymap, combo, event);
583     state.key_sequence.push(combo);
585     if (binding == null) {
586         var c = state.continuation;
587         delete state.continuation;
588         window.minibuffer.pop_state();
589         c.throw(invalid_key_binding(state.key_sequence));
590         return;
591     }
593     if (binding.constructor == Array) { //keymaps stack
594         window.minibuffer._restore_normal_state();
595         window.minibuffer._input_text = state.key_sequence.join(" ") + " ";
596         state.target_keymap = binding;
597         return;
598     }
600     var c = state.continuation;
601     delete state.continuation;
603     window.minibuffer.pop_state();
605     if (c != null)
606         c([state.key_sequence, binding]);
608 interactive("read-key-binding-key",
609     "Handle a keystroke in the key binding reader minibuffer state.",
610      function (I) {
611          read_key_binding_key(I.window,
612                               I.minibuffer.check_state(key_binding_reader),
613                               I.event);
614      });
616 minibuffer.prototype.read_key_binding = function () {
617     keywords(arguments);
618     var s = new key_binding_reader(this, (yield CONTINUATION), forward_keywords(arguments));
619     this.push_state(s);
620     var result = yield SUSPEND;
621     yield co_return(result);
624 provide("keymap");