jsx module system
[conkeror.git] / modules / keymap.js
blob648ddedde885b540408dda9dc32bb70a38920edc
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;
167 function define_keymap (name) {
168     keywords(arguments);
169     this[name] = new keymap($name = name, forward_keywords(arguments));
175  * Key Match Predicates.
176  */
178 function define_key_match_predicate (name, description, predicate) {
179     conkeror[name] = predicate;
180     conkeror[name].name = name;
181     conkeror[name].description = description;
182     return conkeror[name];
185 define_key_match_predicate('match_any_key', 'any key',
186     function (event) true);
188 // should be renamed to match_any_unmodified_character
189 define_key_match_predicate('match_any_unmodified_character', 'any unmodified character',
190     function (event) {
191         // this predicate can be used for both keypress and keydown
192         // fallthroughs.
193         try {
194             return ((event.type == 'keypress' && event.charCode)
195                     || event.keyCode > 31)
196                 && !modifiers.A.in_event_p(event)
197                 && !event.metaKey
198                 && !event.ctrlKey
199                 && !event.sticky_modifiers;
200         } catch (e) { return false; }
201     });
203 define_key_match_predicate('match_checkbox_keys', 'checkbox keys',
204     function (event) {
205         return event.keyCode == 32
206             && !event.shiftKey
207             && !event.metaKey
208             && !event.altKey
209             && !event.ctrlKey;
210         //XXX: keycode fallthroughs don't support sticky modifiers
211     });
213 define_key_match_predicate('match_text_keys', 'text editing keys',
214     function (event) {
215         return ((event.type == 'keypress' && event.charCode)
216                 || event.keyCode == 13 || event.keyCode > 31)
217             && !event.ctrlKey
218             && !event.metaKey
219             && !modifiers.A.in_event_p(event);
220         //XXX: keycode fallthroughs don't support sticky modifiers
221     });
223 define_key_match_predicate('match_not_escape_key', 'any key but escape',
224     function (event) {
225         return event.keyCode != 27 ||
226              event.shiftKey ||
227              event.altKey ||
228              event.metaKey || // M-escape can also leave this mode, so we
229                               // need to use an accurate determination of
230                               // whether the "M" modifier was pressed,
231                               // which is not necessarily the same as
232                               // event.metaKey.
233              event.ctrlKey;
234     });
238  */
240 function format_key_spec (key) {
241     if (key instanceof Function) {
242         if (key.description)
243             return "<"+key.description+">";
244         if (key.name)
245             return "<"+key.name+">";
246         return "<anonymous match function>";
247     }
248     return key;
251 function format_binding_sequence (seq) {
252     return seq.map(function (x) {
253             return format_key_spec(x.key);
254         }).join(" ");
258 function keymap_lookup (keymaps, combo, event) {
259     var i = keymaps.length - 1;
260     if (i < 0)
261         return null;
262     var kmap = keymaps[i];
263     var new_kmaps;
264     while (true) {
265         var bindings = kmap.bindings;
266         var bind = bindings[combo];
267         if (new_kmaps) {
268             if (bind) {
269                 if (bind.keymap)
270                     new_kmaps.unshift(bind.keymap);
271                 else
272                     return new_kmaps;
273             }
274         } else if (bind) {
275             if (bind.keymap)
276                 new_kmaps = [bind.keymap];
277             else
278                 return bind;
279         } else {
280             var pred_binds = kmap.predicate_bindings;
281             for (var j = 0, pblen = pred_binds.length; j < pblen; ++j) {
282                 bind = pred_binds[j];
283                 if (bind.key(event))
284                     return bind;
285             }
286         }
287         if (kmap.parent)
288             kmap = kmap.parent;
289         else if (i > 0)
290             kmap = keymaps[--i];
291         else if (new_kmaps)
292             return new_kmaps
293         else
294             return null;
295     }
299 function keymap_lookup_fallthrough (keymap, event) {
300     var predicates = keymap.fallthrough;
301     for (var i = 0, plen = predicates.length; i < plen; ++i) {
302         if (predicates[i](event))
303             return true;
304     }
305     return false;
309 function for_each_key_binding (keymaps, callback) {
310     var binding_sequence = [];
311     var in_keymaps = [];
312     function helper (keymap) {
313         if (in_keymaps.indexOf(keymap) >= 0)
314             return;
315         in_keymaps.push(keymap);
316         var binding;
317         for each (binding in keymap.bindings) {
318             binding_sequence.push(binding);
319             callback(binding_sequence);
320             if (binding.keymap)
321                 helper(binding.keymap);
322             binding_sequence.pop();
323         }
324         for each (binding in keymap.predicate_bindings) {
325             binding_sequence.push(binding);
326             callback(binding_sequence);
327             if (binding.keymap)
328                 helper(binding.keymap);
329             binding_sequence.pop();
330         }
331         in_keymaps.pop();
332     }
333     //outer loop is to go down the parent-chain of keymaps
334     var i = keymaps.length - 1;
335     var keymap = keymaps[i];
336     while (true) {
337         helper(keymap);
338         if (keymap.parent)
339             keymap = keymap.parent;
340         else if (i > 0)
341             keymap = keymaps[--i];
342         else
343             break;
344     }
348 function keymap_lookup_command (keymaps, command) {
349     var list = [];
350     for_each_key_binding(keymaps, function (bind_seq) {
351             var bind = bind_seq[bind_seq.length - 1];
352             if (bind.command == command)
353                 list.push(format_binding_sequence(bind_seq));
354         });
355     return list;
364  * $fallthrough, $repeat and $browser_object are as for define_key.
366  * ref is the source code reference of the call to define_key.
368  * kmap is the keymap in which the binding is to be defined.
370  * seq is the key sequence being bound.  it may be necessary
371  * to auto-generate new keymaps to accomodate the key sequence.
373  * only one of new_command and new_keymap will be given.
374  * the one that is given is the thing being bound to.
375  */
376 define_keywords("$fallthrough", "$repeat", "$browser_object");
377 function define_key_internal (ref, kmap, seq, new_command, new_keymap) {
378     keywords(arguments);
379     var args = arguments;
380     var last_in_sequence; // flag to indicate the final key combo in the sequence.
381     var key; // current key combo as we iterate through the sequence.
382     var undefine_key = (new_command == null) &&
383         (new_keymap == null) &&
384         (! args.$fallthrough);
386     /* Replace `bind' with the binding specified by (cmd, fallthrough) */
387     function replace_binding (bind) {
388         if (last_in_sequence) {
389             bind.command = new_command;
390             bind.keymap = new_keymap;
391             bind.fallthrough = args.$fallthrough;
392             bind.source_code_reference = ref;
393             bind.repeat = args.$repeat;
394             bind.browser_object = args.$browser_object;
395         } else {
396             if (!bind.keymap)
397                 throw new Error("Key sequence has a non-keymap in prefix");
398             kmap = bind.keymap;
399         }
400     }
402     function make_binding () {
403         if (last_in_sequence) {
404             return { key: key,
405                      fallthrough: args.$fallthrough,
406                      command: new_command,
407                      keymap: new_keymap,
408                      source_code_reference: ref,
409                      repeat: args.$repeat,
410                      browser_object: args.$browser_object,
411                      bound_in: kmap };
412         } else {
413             let old_kmap = kmap;
414             kmap = new keymap($anonymous,
415                               $name = old_kmap.name + " " + format_key_spec(key));
416             kmap.bound_in = old_kmap;
417             return { key: key,
418                      keymap: kmap,
419                      source_code_reference: ref,
420                      bound_in: old_kmap };
421         }
422     }
424 sequence:
425     for (var i = 0, slen = seq.length; i < slen; ++i) {
426         key = seq[i];
427         last_in_sequence = (i == slen - 1);
429         if (typeof key == "function") { // it's a match predicate
430             // Check if the binding is already present in the keymap
431             var pred_binds = kmap.predicate_bindings;
432             for (var j = 0, pblen = pred_binds.length; j < pblen; j++) {
433                 if (pred_binds[j].key == key) {
434                     if (last_in_sequence && undefine_key)
435                         pred_binds.splice(j, 1);
436                     else
437                         replace_binding(pred_binds[j]);
438                     continue sequence;
439                 }
440             }
441             if (! undefine_key)
442                 pred_binds.push(make_binding());
443         } else {
444             // Check if the binding is already present in the keymap
445             var bindings = kmap.bindings;
446             var binding = bindings[key];
447             if (binding) {
448                 if (last_in_sequence && undefine_key)
449                     delete bindings[key];
450                 else
451                     replace_binding(binding);
452                 continue sequence;
453             }
454             if (! undefine_key)
455                 bindings[key] = make_binding();
456         }
457     }
461  * bind SEQ to a keymap or command CMD in keymap KMAP.
463  * keywords:
465  *  $fallthrough: specifies that the keypress event will fall through
466  *      to gecko.  Note, by this method, only keypress will fall through.
467  *      Keyup and keydown will still be blocked.
469  *  $repeat: (command name) shortcut command to call if a prefix
470  *      command key is pressed twice in a row.
472  *  $browser_object: (browser object) Override the default
473  *      browser-object for the command.
474  */
475 define_keywords("$fallthrough", "$repeat", "$browser_object");
476 function define_key (kmap, seq, cmd) {
477     keywords(arguments);
478     var orig_seq = seq;
479     try {
480         var ref = get_caller_source_code_reference();
482         if (typeof seq == "string" && seq.length > 1)
483             seq = seq.split(" ");
485         if (!(typeof seq == "object") || !(seq instanceof Array))
486             seq = [seq];
488         // normalize the order of modifiers in key combos
489         seq = seq.map(
490             function (k) {
491                 if (typeof(k) == "string")
492                     return format_key_combo(unformat_key_combo(k));
493                 else
494                     return k;
495             });
497         var new_command = null;
498         var new_keymap = null;
499         if (typeof cmd == "string" || typeof cmd == "function")
500             new_command = cmd;
501         else if (cmd instanceof keymap)
502             new_keymap = cmd;
503         else if (cmd != null)
504             throw new Error("Invalid `cmd' argument: " + cmd);
506         define_key_internal(ref, kmap, seq, new_command, new_keymap,
507                             forward_keywords(arguments));
509     } catch (e if (typeof e == "string")) {
510         dumpln("Warning: Error occurred while binding sequence: " + orig_seq);
511         dumpln(e);
512     }
516 function undefine_key (kmap, seq) {
517     define_key(kmap, seq);
522  * define_fallthrough
524  *   Takes a keymap and a predicate on an event.  Fallthroughs defined by
525  * these means will cause all three of keydown, keypress, and keyup to
526  * fall through to gecko, whereas those defined by the $fallthrough
527  * keyword to define_key only affect keypress events.
529  *   The limitations of this method are that only the keyCode is available
530  * to the predicate, not the charCode, and keymap inheritance is not
531  * available for these "bindings".
532  */
533 function define_fallthrough (keymap, predicate) {
534     keymap.fallthrough.push(predicate);
539  * Minibuffer Read Key Binding
540  */
542 define_keymap("key_binding_reader_keymap");
543 define_key(key_binding_reader_keymap, match_any_key, "read-key-binding-key");
545 define_keywords("$keymap");
546 function key_binding_reader (minibuffer, continuation) {
547     keywords(arguments, $prompt = "Describe key:");
548     minibuffer_input_state.call(this, minibuffer, key_binding_reader_keymap, arguments.$prompt);
549     this.continuation = continuation;
550     if (arguments.$keymap)
551         this.target_keymap = arguments.$keymap;
552     else
553         this.target_keymap = get_current_keymaps(this.minibuffer.window).slice();
554     this.key_sequence = [];
556 key_binding_reader.prototype = {
557     constructor: key_binding_reader,
558     __proto__: minibuffer_input_state.prototype,
559     destroy: function () {
560         if (this.continuation)
561             this.continuation.throw(abort());
562         minibuffer_input_state.prototype.destroy.call(this);
563     }
566 function invalid_key_binding (seq) {
567     var e = new Error(seq.join(" ") + " is undefined");
568     e.key_sequence = seq;
569     e.__proto__ = invalid_key_binding.prototype;
570     return e;
572 invalid_key_binding.prototype = {
573     constructor: invalid_key_binding,
574     __proto__: interactive_error.prototype
577 function read_key_binding_key (window, state, event) {
578     var combo = format_key_combo(event);
579     var binding = keymap_lookup(state.target_keymap, combo, event);
581     state.key_sequence.push(combo);
583     if (binding == null) {
584         var c = state.continuation;
585         delete state.continuation;
586         window.minibuffer.pop_state();
587         c.throw(invalid_key_binding(state.key_sequence));
588         return;
589     }
591     if (binding.constructor == Array) { //keymaps stack
592         window.minibuffer._restore_normal_state();
593         window.minibuffer._input_text = state.key_sequence.join(" ") + " ";
594         state.target_keymap = binding;
595         return;
596     }
598     var c = state.continuation;
599     delete state.continuation;
601     window.minibuffer.pop_state();
603     if (c != null)
604         c([state.key_sequence, binding]);
606 interactive("read-key-binding-key",
607     "Handle a keystroke in the key binding reader minibuffer state.",
608      function (I) {
609          read_key_binding_key(I.window,
610                               I.minibuffer.check_state(key_binding_reader),
611                               I.event);
612      });
614 minibuffer.prototype.read_key_binding = function () {
615     keywords(arguments);
616     var s = new key_binding_reader(this, (yield CONTINUATION), forward_keywords(arguments));
617     this.push_state(s);
618     var result = yield SUSPEND;
619     yield co_return(result);
622 provide("keymap");