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