call_on_focused_field, modify_region: support richedit frames
[conkeror.git] / modules / input.js
blobdc78a7f9d7e0953210b31de37cf4bfb5294de436
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 require("window.js");
13 require("keymap.js");
14 require("interactive.js");
16 define_variable("key_bindings_ignore_capslock", false,
17     "When true, the case of characters in key bindings will be based "+
18     "only on whether shift was pressed--upper-case if yes, lower-case if "+
19     "no.  Effectively, this overrides the capslock key.  This option has "+
20     "no effect on ordinary typing in input fields.");
22 define_variable("keyboard_key_sequence_help_timeout", 0,
23     "Delay (in millseconds) before the current key sequence prefix is "+
24     "displayed in the minibuffer.");
27 /**
28  * event_clone is used to make a copy of an event which is safe to keep
29  * references to, because it will not reference any DOM trees directly
30  * or indirectly.
31  *
32  * A pertinent question would be, is this needed?  Are there instances
33  * in Conkeror when an event reference is stored across a navigation
34  * boundary or buffer/window closing?
35  */
36 function event_clone (event) {
37     this.type = event.type;
38     this.keyCode = event.keyCode;
39     this.charCode = event.charCode;
40     this.ctrlKey = event.ctrlKey;
41     this.metaKey = event.metaKey;
42     this.altKey = event.altKey;
43     this.shiftKey = event.shiftKey;
44     this.sticky_modifiers = event.sticky_modifiers;
48 /**
49  * event_kill stops an event from being processed by any further handlers.
50  */
51 function event_kill (event) {
52     event.preventDefault();
53     event.stopPropagation();
57 /**
58  * command_event is a special event type that tells input_handle_sequence
59  * to run the given command.
60  */
61 function command_event (command) {
62     this.type = "command";
63     this.command = command;
67 /**
68  * input_state makes an object that holds the state of a single key sequence.
69  * As a small measure of efficiency, these objects get recycled from one
70  * sequence to the next.
71  */
72 function input_state () {
73     this.fallthrough = {};
75 input_state.prototype = {
76     constructor: input_state,
77     continuation: null,
78     fallthrough: null
82 /**
83  * input_stack is a stack of input_states, which is to say a stack of
84  * recursed sequences.  input recursion happens, for example, when a
85  * minibuffer read takes place in the middle of another sequence.
86  */
87 function input_stack () {
88     this.push(new input_state());
90 input_stack.prototype = {
91     constructor: input_stack,
92     __proto__: Array.prototype,
94     help_timer: null,
95     help_displayed: false,
97     toString: function () {
98         return "[input_stack ("+this.length+")]";
99     },
100     get current () {
101         return this[this.length - 1];
102     },
103     begin_recursion: function () {
104         this.push(new input_state());
105     },
106     end_recursion: function () {
107         this.pop();
108     }
112 function input_help_timer_clear (window) {
113     if (window.input.help_timer != null) {
114         timer_cancel(window.input.help_timer);
115         window.input.help_timer = null;
116     }
120 function input_show_partial_sequence (window, I) {
121     if (window.input.help_displayed)
122         window.minibuffer.show(I.key_sequence.join(" "));
123     else {
124         window.input.help_timer = call_after_timeout(function () {
125             window.minibuffer.show(I.key_sequence.join(" "));
126             window.input.help_displayed = true;
127             window.input.help_timer = null;
128         }, keyboard_key_sequence_help_timeout);
129     }
133 define_window_local_hook("keypress_hook", RUN_HOOK_UNTIL_SUCCESS,
134     "A run-until-success hook available for special keypress "+
135     "handling.  Handlers receive as arguments the window, an "+
136     "interactive context, and the real keypress event.  The "+
137     "handler is responsible for stopping event propagation, if "+
138     "that is desired.");
142  * get_current_keymaps returns the keymap stack for the current focus
143  * context of the given window.  This is the top-level keymap stack, not
144  * the stack that represents any on-going key sequence.
145  */
146 function get_current_keymaps (window) {
147     var m = window.minibuffer;
148     var s = m.current_state;
149     if (m.active && s.keymaps)
150         return s.keymaps;
151     return window.buffers.current.keymaps;
156  * input_handle_sequence is the main handler for all event types which
157  * can be part of a sequence.  It is a coroutine procedure which gets
158  * started and resumed by various EventListeners, some of which have
159  * additional, special tasks.
160  */
161 function input_handle_sequence (event) {
162     try {
163         var window = this;
164         var state = window.input.current;
165         state.continuation = yield CONTINUATION;
166         var I = new interactive_context(window.buffers.current);
167         I.key_sequence = [];
168         I.sticky_modifiers = 0;
169         var keymaps = get_current_keymaps(window);
170 sequence:
171         while (true) {
172             switch (event.type) {
173             case "keydown":
174                 //try the fallthrough predicates in our current keymap
175                 if (keymap_lookup_fallthrough(keymaps[keymaps.length - 1], event)) {
176                     //XXX: need to take account of modifers, too!
177                     state.fallthrough[event.keyCode] = true;
178                 } else
179                     event_kill(event);
180                 break;
181             case "keypress":
182                 window.minibuffer.clear();
183                 window.input.help_displayed = false;
184                 input_help_timer_clear(window);
186                 // prepare the clone
187                 var clone = new event_clone(event);
188                 clone.sticky_modifiers = I.sticky_modifiers;
189                 I.sticky_modifiers = 0;
190                 if (key_bindings_ignore_capslock && clone.charCode) {
191                     let c = String.fromCharCode(clone.charCode);
192                     if (clone.shiftKey)
193                         clone.charCode = c.toUpperCase().charCodeAt(0);
194                     else
195                         clone.charCode = c.toLowerCase().charCodeAt(0);
196                 }
198                 // make the combo string
199                 var combo = format_key_combo(clone);
200                 var canabort = I.key_sequence.push(combo) > 1;
201                 I.combo = combo;
202                 I.event = clone;
204                 // make active keymaps visible to commands
205                 I.keymaps = keymaps;
207                 if (keypress_hook.run(window, I, event))
208                     break;
210                 var overlay_keymap = I.overlay_keymap;
212                 var binding =
213                     (canabort && keymap_lookup([sequence_abort_keymap], combo, event)) ||
214                     (overlay_keymap && keymap_lookup([overlay_keymap], combo, event)) ||
215                     keymap_lookup(keymaps, combo, event) ||
216                     keymap_lookup([sequence_help_keymap], combo, event);
218                 // kill event for any unbound key, or any bound key which
219                 // is not marked fallthrough
220                 if (!binding || !binding.fallthrough)
221                     event_kill(event);
223                 if (binding) {
224                     if (binding.browser_object != null)
225                         I.binding_browser_object = binding.browser_object;
226                     if (binding.constructor == Array) {
227                         keymaps = binding;
228                         input_show_partial_sequence(window, I);
229                     } else if (binding.command) {
230                         let command = binding.command;
231                         if (I.repeat == command)
232                             command = binding.repeat;
233                         yield call_interactively(I, command);
234                         if (typeof command == "string" &&
235                             interactive_commands.get(command).prefix)
236                         {
237                             keymaps = get_current_keymaps(window); //back to top keymap
238                             input_show_partial_sequence(window, I);
239                             if (binding.repeat)
240                                 I.repeat = command;
241                         } else {
242                             break sequence;
243                         }
244                     } else {
245                         break sequence; //reachable by keypress fallthroughs
246                     }
247                 } else {
248                     window.minibuffer.message(I.key_sequence.join(" ") + " is undefined");
249                     break sequence;
250                 }
251                 break;
252             case "command":
253                 let (command = event.command) {
254                     window.input.help_displayed = false;
255                     input_help_timer_clear(window);
256                     window.minibuffer.clear();
257                     yield call_interactively(I, command);
258                     if (! interactive_commands.get(command).prefix)
259                         break sequence;
260                 }
261                 break;
262             }
263             // should we expect more events?
264             event = null;
265             event = yield SUSPEND;
266         }
267     } catch (e) {
268         dump_error(e);
269     } finally {
270         // sequence is done
271         delete state.continuation;
272     }
276 function input_handle_keydown (event) {
277     if (event.keyCode == 0 ||
278         event.keyCode == vk_name_to_keycode.shift ||
279         event.keyCode == vk_name_to_keycode.control ||
280         event.keyCode == vk_name_to_keycode.alt ||
281         event.keyCode == vk_name_to_keycode.caps_lock)
282         return event_kill(event);
283     var window = this;
284     var state = window.input.current;
285     if (state.continuation)
286         state.continuation(event);
287     else
288         co_call(input_handle_sequence.call(window, event));
292 function input_handle_keypress (event) {
293     if (event.keyCode == 0 && event.charCode == 0 ||
294         event.keyCode == vk_name_to_keycode.caps_lock)
295         return event_kill(event);
296     var window = this;
297     var state = window.input.current;
298     if (state.continuation)
299         state.continuation(event);
300     else
301         co_call(input_handle_sequence.call(window, event));
305 function input_handle_keyup (event) {
306     if (event.keyCode == 0 ||
307         event.keyCode == vk_name_to_keycode.shift ||
308         event.keyCode == vk_name_to_keycode.control ||
309         event.keyCode == vk_name_to_keycode.alt ||
310         event.keyCode == vk_name_to_keycode.caps_lock)
311         return event_kill(event);
312     var window = this;
313     var state = window.input.current;
314     if (state.fallthrough[event.keyCode])
315         delete state.fallthrough[event.keyCode];
316     else
317         event_kill(event);
321 // handler for command_event special events
322 function input_handle_command (event) {
323     var window = this;
324     var state = window.input.current;
325     if (typeof event == 'string')
326         event = new command_event(event);
327     if (state.continuation)
328         state.continuation(event);
329     else
330         co_call(input_handle_sequence.call(window, event));
334 // handler for special abort event
335 function input_sequence_abort (message) {
336     var window = this;
337     window.input.help_displayed = false;
338     input_help_timer_clear(window);
339     window.minibuffer.clear();
340     if (message)
341         window.minibuffer.show(message);
342     delete window.input.current.continuation;
346 function input_initialize_window (window) {
347     window.input = new input_stack();
348     //window.addEventListener("keydown", input_handle_keydown, true);
349     window.addEventListener("keypress", input_handle_keypress, true);
350     //window.addEventListener("keyup", input_handle_keyup, true);
351     //TO-DO: mousedown, mouseup, click, dblclick
354 add_hook("window_initialize_hook", input_initialize_window);
357 interactive("sequence-abort",
358     "Abort an ongoing key sequence.",
359     function (I) { I.minibuffer.message("abort sequence"); });
361 provide("input");