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