history.js: Add clear-history and clear-form-history commands.
[conkeror.git] / modules / keymap.js
blob3aad1380374c807d7a22db127352172e66d80058
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.superKey)
45                             return true;
46                         return ("getModifierState" in event &&
47                                 event.getModifierState("OS"));
48                     },
49                     function (event) { event.superKey = true; }),
50     S: new modifier(function (event) {
51                         if (event.shiftKey) {
52                             var name;
53                             if (event.keyCode)
54                                 name = keycode_to_vk_name[event.keyCode];
55                             return ((name && name.length > 1) ||
56                                     (event.charCode == 32) ||
57                                     (event.button != null));
58                         }
59                         return false;
60                     },
61                     function (event) { event.shiftKey = true; })
63 var modifier_order = ['C', 'M', 's', 'S'];
65 // check the platform and guess whether we should treat Alt as Meta
66 if (get_os() == 'Darwin') {
67     // In OS X, alt is a shift-like modifier, in that we
68     // only care about it for non-character events.
69     modifiers.A = new modifier(
70         function (event) {
71             if (event.altKey) {
72                 var name;
73                 if (event.keyCode)
74                     name = keycode_to_vk_name[event.keyCode];
75                 return ((name && name.length > 1) ||
76                         (event.charCode == 32) ||
77                         (event.button != null));
78             }
79             return false;
80         },
81         function (event) { event.altKey = true; });
82     modifier_order = ['C', 'M', 'A', 'S'];
83 } else {
84     modifiers.M = modifiers.A;
90  * Combos
91  */
93 function format_key_combo (event) {
94     var combo = '';
95     for each (var M in modifier_order) {
96         if (modifiers[M].in_event_p(event) ||
97             (event.sticky_modifiers &&
98              event.sticky_modifiers.indexOf(M) != -1))
99         {
100             combo += (M + '-');
101         }
102     }
103     if (event.charCode) {
104         if (event.charCode == 32)
105             combo += 'space';
106         else
107             combo += String.fromCharCode(event.charCode);
108     } else if (event.keyCode) {
109         combo += keycode_to_vk_name[event.keyCode];
110     } else if (event.button != null) {
111         combo += "mouse" + (event.button + 1);
112     } else if (event.command) {
113         combo += "cmd" + event.command.toLowerCase();
114     }
115     return combo;
118 function unformat_key_combo (combo) {
119     var event = {
120         keyCode: 0,
121         charCode: 0,
122         button: null,
123         command: null,
124         altKey: false,
125         ctrlKey: false,
126         metaKey: false,
127         shiftKey: false,
128         superKey: false
129     };
130     var M;
131     var i = 0;
132     var len = combo.length - 2;
133     var res;
134     while (i < len && combo[i+1] == '-') {
135         M = combo[i];
136         modifiers[M].set_in_event(event);
137         i+=2;
138     }
139     var key = combo.substring(i);
140     if (key.length == 1)
141         event.charCode = key.charCodeAt(0);
142     else if (key == 'space')
143         event.charCode = 32;
144     else if (key.substring(0, 3) == 'cmd')
145         event.command = key.substr(3, 1).toUpperCase() +
146             key.substring(4);
147     else if (vk_name_to_keycode[key])
148         event.keyCode = vk_name_to_keycode[key];
149     else if (key.substring(0, 5) == 'mouse')
150         event.button = parseInt(key.substring(5));
151     return event;
157  * Keymap datatype
158  */
160 define_keywords("$parent", "$help", "$name", "$anonymous",
161                 "$display_name", "$notify");
162 function keymap () {
163     keywords(arguments);
164     this.parent = arguments.$parent;
165     this.bindings = {};
166     this.predicate_bindings = [];
167     this.fallthrough = [];
168     this.help = arguments.$help;
169     this.name = arguments.$name;
170     this.display_name = arguments.$display_name;
171     this.notify = arguments.$notify;
172     this.anonymous = arguments.$anonymous;
174 keymap.prototype = {
175     constructor: keymap,
176     toString: function () "#<keymap>"
179 function define_keymap (name) {
180     keywords(arguments);
181     this[name] = new keymap($name = name, forward_keywords(arguments));
187  * Key Match Predicates.
188  */
190 function define_key_match_predicate (name, description, predicate) {
191     conkeror[name] = predicate;
192     conkeror[name].name = name;
193     conkeror[name].description = description;
194     return conkeror[name];
197 define_key_match_predicate('match_any_key', 'any key',
198     function (event) true);
200 // should be renamed to match_any_unmodified_character
201 define_key_match_predicate('match_any_unmodified_character', 'any unmodified character',
202     function (event) {
203         // this predicate can be used for both keypress and keydown
204         // fallthroughs.
205         try {
206             return ((event.type == 'keypress' && event.charCode)
207                     || event.keyCode > 31)
208                 && !modifiers.A.in_event_p(event)
209                 && !event.metaKey
210                 && !event.ctrlKey
211                 && !modifiers.s.in_event_p(event)
212                 && !event.sticky_modifiers;
213         } catch (e) { return false; }
214     });
216 define_key_match_predicate('match_checkbox_keys', 'checkbox keys',
217     function (event) {
218         return event.keyCode == 32
219             && !event.shiftKey
220             && !event.metaKey
221             && !event.altKey
222             && !event.ctrlKey
223             && !modifiers.s.in_event_p(event);
224         //XXX: keycode fallthroughs don't support sticky modifiers
225     });
227 define_key_match_predicate('match_text_keys', 'text editing keys',
228     function (event) {
229         return ((event.type == 'keypress' && event.charCode)
230                 || event.keyCode == 13 || event.keyCode > 31)
231             && !event.ctrlKey
232             && !event.metaKey
233             && !modifiers.A.in_event_p(event)
234             && !modifiers.s.in_event_p(event);
235         //XXX: keycode fallthroughs don't support sticky modifiers
236     });
238 define_key_match_predicate('match_not_escape_key', 'any key but escape',
239     function (event) {
240         return event.keyCode != 27 ||
241              event.shiftKey ||
242              event.altKey ||
243              event.metaKey || // M-escape can also leave this mode, so we
244                               // need to use an accurate determination of
245                               // whether the "M" modifier was pressed,
246                               // which is not necessarily the same as
247                               // event.metaKey.
248              event.ctrlKey ||
249              modifiers.s.in_event_p(event);
250     });
254  */
256 function format_key_spec (key) {
257     if (key instanceof Function) {
258         if (key.description)
259             return "<"+key.description+">";
260         if (key.name)
261             return "<"+key.name+">";
262         return "<anonymous match function>";
263     }
264     return key;
267 function format_binding_sequence (seq) {
268     return seq.map(function (x) {
269             return format_key_spec(x.key);
270         }).join(" ");
274 function keymap_lookup (keymaps, combo, event) {
275     var i = keymaps.length - 1;
276     if (i < 0)
277         return null;
278     var kmap = keymaps[i];
279     var new_kmaps;
280     while (true) {
281         var bindings = kmap.bindings;
282         var bind = bindings[combo];
283         if (new_kmaps) {
284             if (bind) {
285                 if (bind.keymap)
286                     new_kmaps.unshift(bind.keymap);
287                 else
288                     return new_kmaps;
289             }
290         } else if (bind) {
291             if (bind.keymap)
292                 new_kmaps = [bind.keymap];
293             else
294                 return bind;
295         } else {
296             var pred_binds = kmap.predicate_bindings;
297             for (var j = 0, pblen = pred_binds.length; j < pblen; ++j) {
298                 bind = pred_binds[j];
299                 if (bind.key(event))
300                     return bind;
301             }
302         }
303         if (kmap.parent)
304             kmap = kmap.parent;
305         else if (i > 0)
306             kmap = keymaps[--i];
307         else if (new_kmaps)
308             return new_kmaps
309         else
310             return null;
311     }
315 function keymap_lookup_fallthrough (keymap, event) {
316     var predicates = keymap.fallthrough;
317     for (var i = 0, plen = predicates.length; i < plen; ++i) {
318         if (predicates[i](event))
319             return true;
320     }
321     return false;
325 function for_each_key_binding (keymaps, callback) {
326     var binding_sequence = [];
327     var in_keymaps = [];
328     function helper (keymap) {
329         if (in_keymaps.indexOf(keymap) >= 0)
330             return;
331         in_keymaps.push(keymap);
332         var binding;
333         for each (binding in keymap.bindings) {
334             binding_sequence.push(binding);
335             callback(binding_sequence);
336             if (binding.keymap)
337                 helper(binding.keymap);
338             binding_sequence.pop();
339         }
340         for each (binding in keymap.predicate_bindings) {
341             binding_sequence.push(binding);
342             callback(binding_sequence);
343             if (binding.keymap)
344                 helper(binding.keymap);
345             binding_sequence.pop();
346         }
347         in_keymaps.pop();
348     }
349     //outer loop is to go down the parent-chain of keymaps
350     var i = keymaps.length - 1;
351     var keymap = keymaps[i];
352     while (true) {
353         helper(keymap);
354         if (keymap.parent)
355             keymap = keymap.parent;
356         else if (i > 0)
357             keymap = keymaps[--i];
358         else
359             break;
360     }
364 function keymap_lookup_command (keymaps, command) {
365     var list = [];
366     for_each_key_binding(keymaps, function (bind_seq) {
367             var bind = bind_seq[bind_seq.length - 1];
368             if (bind.command && bind.command == command)
369                 list.push(format_binding_sequence(bind_seq));
370         });
371     return list;
380  * $fallthrough, $repeat and $browser_object are as for define_key.
382  * ref is the source code reference of the call to define_key.
384  * kmap is the keymap in which the binding is to be defined.
386  * seq is the key sequence being bound.  it may be necessary
387  * to auto-generate new keymaps to accomodate the key sequence.
389  * only one of new_command and new_keymap will be given.
390  * the one that is given is the thing being bound to.
391  */
392 define_keywords("$fallthrough", "$repeat", "$browser_object");
393 function define_key_internal (ref, kmap, seq, new_command, new_keymap) {
394     keywords(arguments);
395     var args = arguments;
396     var last_in_sequence; // flag to indicate the final key combo in the sequence.
397     var key; // current key combo as we iterate through the sequence.
398     var undefine_key = (new_command == null) &&
399         (new_keymap == null) &&
400         (! args.$fallthrough);
402     /* Replace `bind' with the binding specified by (cmd, fallthrough) */
403     function replace_binding (bind) {
404         if (last_in_sequence) {
405             bind.command = new_command;
406             bind.keymap = new_keymap;
407             bind.fallthrough = args.$fallthrough;
408             bind.source_code_reference = ref;
409             bind.repeat = args.$repeat;
410             bind.browser_object = args.$browser_object;
411         } else {
412             if (!bind.keymap)
413                 throw new Error("Key sequence has a non-keymap in prefix");
414             kmap = bind.keymap;
415         }
416     }
418     function make_binding () {
419         if (last_in_sequence) {
420             return { key: key,
421                      fallthrough: args.$fallthrough,
422                      command: new_command,
423                      keymap: new_keymap,
424                      source_code_reference: ref,
425                      repeat: args.$repeat,
426                      browser_object: args.$browser_object,
427                      bound_in: kmap };
428         } else {
429             let old_kmap = kmap;
430             kmap = new keymap($anonymous,
431                               $name = old_kmap.name + " " + format_key_spec(key));
432             kmap.bound_in = old_kmap;
433             return { key: key,
434                      keymap: kmap,
435                      source_code_reference: ref,
436                      bound_in: old_kmap };
437         }
438     }
440 sequence:
441     for (var i = 0, slen = seq.length; i < slen; ++i) {
442         key = seq[i];
443         last_in_sequence = (i == slen - 1);
445         if (typeof key == "function") { // it's a match predicate
446             // Check if the binding is already present in the keymap
447             var pred_binds = kmap.predicate_bindings;
448             for (var j = 0, pblen = pred_binds.length; j < pblen; j++) {
449                 if (pred_binds[j].key == key) {
450                     if (last_in_sequence && undefine_key)
451                         pred_binds.splice(j, 1);
452                     else
453                         replace_binding(pred_binds[j]);
454                     continue sequence;
455                 }
456             }
457             if (! undefine_key)
458                 pred_binds.push(make_binding());
459         } else {
460             // Check if the binding is already present in the keymap
461             var bindings = kmap.bindings;
462             var binding = bindings[key];
463             if (binding) {
464                 if (last_in_sequence && undefine_key)
465                     delete bindings[key];
466                 else
467                     replace_binding(binding);
468                 continue sequence;
469             }
470             if (! undefine_key)
471                 bindings[key] = make_binding();
472         }
473     }
477  * bind SEQ to a keymap or command CMD in keymap KMAP.
479  * keywords:
481  *  $fallthrough: specifies that the keypress event will fall through
482  *      to gecko.  Note, by this method, only keypress will fall through.
483  *      Keyup and keydown will still be blocked.
485  *  $repeat: (command name) shortcut command to call if a prefix
486  *      command key is pressed twice in a row.
488  *  $browser_object: (browser object) Override the default
489  *      browser-object for the command.
490  */
491 define_keywords("$fallthrough", "$repeat", "$browser_object");
492 function define_key (kmap, seq, cmd) {
493     keywords(arguments);
494     var orig_seq = seq;
495     try {
496         var ref = get_caller_source_code_reference();
498         if (typeof seq == "string" && seq.length > 1)
499             seq = seq.split(" ");
501         if (! array_p(seq))
502             seq = [seq];
504         // normalize the order of modifiers in key combos
505         seq = seq.map(
506             function (k) {
507                 if (typeof(k) == "string")
508                     return format_key_combo(unformat_key_combo(k));
509                 else
510                     return k;
511             });
513         var new_command = null;
514         var new_keymap = null;
515         if (typeof cmd == "string" || typeof cmd == "function")
516             new_command = cmd;
517         else if (cmd instanceof keymap)
518             new_keymap = cmd;
519         else if (cmd != null)
520             throw new Error("Invalid `cmd' argument: " + cmd);
522         define_key_internal(ref, kmap, seq, new_command, new_keymap,
523                             forward_keywords(arguments));
525     } catch (e if (typeof e == "string")) {
526         dumpln("Warning: Error occurred while binding sequence: " + orig_seq);
527         dumpln(e);
528     }
532 function undefine_key (kmap, seq) {
533     define_key(kmap, seq);
538  * define_fallthrough
540  *   Takes a keymap and a predicate on an event.  Fallthroughs defined by
541  * these means will cause all three of keydown, keypress, and keyup to
542  * fall through to gecko, whereas those defined by the $fallthrough
543  * keyword to define_key only affect keypress events.
545  *   The limitations of this method are that only the keyCode is available
546  * to the predicate, not the charCode, and keymap inheritance is not
547  * available for these "bindings".
548  */
549 function define_fallthrough (keymap, predicate) {
550     keymap.fallthrough.push(predicate);
555  * Minibuffer Read Key Binding
556  */
558 define_keymap("key_binding_reader_keymap");
559 define_key(key_binding_reader_keymap, match_any_key, "read-key-binding-key");
561 define_keywords("$keymap");
562 function key_binding_reader (minibuffer) {
563     keywords(arguments, $prompt = "Describe key:");
564     minibuffer_input_state.call(this, minibuffer, key_binding_reader_keymap, arguments.$prompt);
565     this.deferred = Promise.defer();
566     this.promise = make_simple_cancelable(this.deferred);
567     if (arguments.$keymap)
568         this.target_keymap = arguments.$keymap;
569     else
570         this.target_keymap = get_current_keymaps(this.minibuffer.window).slice();
571     this.key_sequence = [];
573 key_binding_reader.prototype = {
574     constructor: key_binding_reader,
575     __proto__: minibuffer_input_state.prototype,
576     destroy: function () {
577         this.promise.cancel();
578         minibuffer_input_state.prototype.destroy.call(this);
579     }
582 function invalid_key_binding (seq) {
583     var e = new Error(seq.join(" ") + " is undefined");
584     e.key_sequence = seq;
585     e.__proto__ = invalid_key_binding.prototype;
586     return e;
588 invalid_key_binding.prototype = {
589     constructor: invalid_key_binding,
590     __proto__: interactive_error.prototype
593 function read_key_binding_key (window, state, event) {
594     var combo = format_key_combo(event);
595     var binding = keymap_lookup(state.target_keymap, combo, event);
597     state.key_sequence.push(combo);
599     if (binding == null) {
600         state.deferred.reject(invalid_key_binding(state.key_sequence));
601         window.minibuffer.pop_state();
602         return;
603     }
605     if (array_p(binding)) { //keymaps stack
606         window.minibuffer._restore_normal_state();
607         window.minibuffer._input_text = state.key_sequence.join(" ") + " ";
608         state.target_keymap = binding;
609         return;
610     }
612     state.deferred.resolve([state.key_sequence, binding]);
613     window.minibuffer.pop_state();
615 interactive("read-key-binding-key",
616     "Handle a keystroke in the key binding reader minibuffer state.",
617      function (I) {
618          read_key_binding_key(I.window,
619                               I.minibuffer.check_state(key_binding_reader),
620                               I.event);
621      });
623 minibuffer.prototype.read_key_binding = function () {
624     keywords(arguments);
625     var s = new key_binding_reader(this, forward_keywords(arguments));
626     this.push_state(s);
627     yield co_return(yield s.promise);
630 provide("keymap");