whitespace
[conkeror.git] / modules / input.js
blobde22e968ea538de4a6708172e37aced630f79e77
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.button = event.button;
41     this.command = event.command;
42     this.ctrlKey = event.ctrlKey;
43     this.metaKey = event.metaKey;
44     this.altKey = event.altKey;
45     this.shiftKey = event.shiftKey;
46     this.sticky_modifiers = event.sticky_modifiers;
50 /**
51  * event_kill stops an event from being processed by any further handlers.
52  */
53 function event_kill (event) {
54     event.preventDefault();
55     event.stopPropagation();
59 /**
60  * command_event is a special event type that tells input_handle_sequence
61  * to run the given command.
62  */
63 function command_event (command) {
64     this.type = "command";
65     this.command = command;
69 /**
70  * input_state makes an object that holds the state of a single key sequence.
71  * As a small measure of efficiency, these objects get recycled from one
72  * sequence to the next.
73  */
74 function input_state () {
75     this.fallthrough = {};
77 input_state.prototype = {
78     constructor: input_state,
79     continuation: null,
80     fallthrough: null
84 /**
85  * input_stack is a stack of input_states, which is to say a stack of
86  * recursed sequences.  input recursion happens, for example, when a
87  * minibuffer read takes place in the middle of another sequence.
88  */
89 function input_stack () {
90     this.array = [new input_state()];
92 input_stack.prototype = {
93     constructor: input_stack,
95     array: null,
96     help_timer: null,
97     help_displayed: false,
99     toString: function () {
100         return "[input_stack ("+this.array.length+")]";
101     },
102     get current () {
103         return this.array[this.array.length - 1];
104     },
105     begin_recursion: function () {
106         this.array.push(new input_state());
107     },
108     end_recursion: function () {
109         this.array.pop();
110     }
114 function input_help_timer_clear (window) {
115     if (window.input.help_timer != null) {
116         timer_cancel(window.input.help_timer);
117         window.input.help_timer = null;
118     }
122 function input_show_partial_sequence (window, I) {
123     if (window.input.help_displayed)
124         window.minibuffer.show(I.key_sequence.join(" "));
125     else {
126         window.input.help_timer = call_after_timeout(function () {
127             window.minibuffer.show(I.key_sequence.join(" "));
128             window.input.help_displayed = true;
129             window.input.help_timer = null;
130         }, keyboard_key_sequence_help_timeout);
131     }
135 define_window_local_hook("keypress_hook", RUN_HOOK_UNTIL_SUCCESS,
136     "A run-until-success hook available for special keypress "+
137     "handling.  Handlers receive as arguments the window, an "+
138     "interactive context, and the real keypress event.  The "+
139     "handler is responsible for stopping event propagation, if "+
140     "that is desired.");
144  * get_current_keymaps returns the keymap stack for the current focus
145  * context of the given window.  This is the top-level keymap stack, not
146  * the stack that represents any on-going key sequence.
147  */
148 function get_current_keymaps (window) {
149     var m = window.minibuffer;
150     var s = m.current_state;
151     if (m.active && s.keymaps)
152         return s.keymaps;
153     return window.buffers.current.keymaps;
158  * input_handle_sequence is the main handler for all event types which
159  * can be part of a sequence.  It is a coroutine procedure which gets
160  * started and resumed by various EventListeners, some of which have
161  * additional, special tasks.
162  */
163 function input_handle_sequence (event) {
164     try {
165         var window = this;
166         var state = window.input.current;
167         state.continuation = yield CONTINUATION;
168         var I = new interactive_context(window.buffers.current);
169         I.key_sequence = [];
170         I.sticky_modifiers = 0;
171         var keymaps = get_current_keymaps(window);
172 sequence:
173         while (true) {
174             switch (event.type) {
175             case "keydown":
176                 //try the fallthrough predicates in our current keymap
177                 if (keymap_lookup_fallthrough(keymaps[keymaps.length - 1], event)) {
178                     //XXX: need to take account of modifers, too!
179                     state.fallthrough[event.keyCode] = true;
180                 } else
181                     event_kill(event);
182                 break;
183             case "keypress":
184             case "AppCommand":
185                 window.minibuffer.clear();
186                 window.input.help_displayed = false;
187                 input_help_timer_clear(window);
189                 // prepare the clone
190                 var clone = new event_clone(event);
191                 clone.sticky_modifiers = I.sticky_modifiers;
192                 I.sticky_modifiers = 0;
193                 if (key_bindings_ignore_capslock && clone.charCode) {
194                     let c = String.fromCharCode(clone.charCode);
195                     if (clone.shiftKey)
196                         clone.charCode = c.toUpperCase().charCodeAt(0);
197                     else
198                         clone.charCode = c.toLowerCase().charCodeAt(0);
199                 }
201                 // make the combo string
202                 var combo = format_key_combo(clone);
203                 var canabort = I.key_sequence.push(combo) > 1;
204                 I.combo = combo;
205                 I.event = clone;
207                 // make active keymaps visible to commands
208                 I.keymaps = keymaps;
210                 if (keypress_hook.run(window, I, event))
211                     break;
213                 var overlay_keymap = I.overlay_keymap;
215                 var binding =
216                     (canabort && keymap_lookup([sequence_abort_keymap], combo, event)) ||
217                     (overlay_keymap && keymap_lookup([overlay_keymap], combo, event)) ||
218                     keymap_lookup(keymaps, combo, event) ||
219                     keymap_lookup([sequence_help_keymap], combo, event);
221                 // kill event for any unbound key, or any bound key which
222                 // is not marked fallthrough
223                 if (!binding || !binding.fallthrough)
224                     event_kill(event);
226                 if (binding) {
227                     if (binding.browser_object !== undefined)
228                         I.binding_browser_object = binding.browser_object;
229                     if (binding.constructor == Array) {
230                         keymaps = binding;
231                         input_show_partial_sequence(window, I);
232                     } else if (binding.command) {
233                         let command = binding.command;
234                         if (I.repeat == command)
235                             command = binding.repeat;
236                         yield call_interactively(I, command);
237                         if (typeof command == "string" &&
238                             interactive_commands.get(command).prefix)
239                         {
240                             keymaps = get_current_keymaps(window); //back to top keymap
241                             input_show_partial_sequence(window, I);
242                             if (binding.repeat)
243                                 I.repeat = command;
244                         } else {
245                             break sequence;
246                         }
247                     } else {
248                         break sequence; //reachable by keypress fallthroughs
249                     }
250                 } else {
251                     window.minibuffer.message(I.key_sequence.join(" ") + " is undefined");
252                     break sequence;
253                 }
254                 break;
255             case "command":
256                 let (command = event.command) {
257                     window.input.help_displayed = false;
258                     input_help_timer_clear(window);
259                     window.minibuffer.clear();
260                     yield call_interactively(I, command);
261                     if (! interactive_commands.get(command).prefix)
262                         break sequence;
263                 }
264                 break;
265             }
266             // should we expect more events?
267             event = null;
268             event = yield SUSPEND;
269         }
270     } catch (e) {
271         dump_error(e);
272     } finally {
273         // sequence is done
274         delete state.continuation;
275     }
279 function input_handle_keydown (event) {
280     if (event.keyCode == 0 ||
281         event.keyCode == vk_name_to_keycode.shift ||
282         event.keyCode == vk_name_to_keycode.control ||
283         event.keyCode == vk_name_to_keycode.alt ||
284         event.keyCode == vk_name_to_keycode.caps_lock)
285         return event_kill(event);
286     var window = this;
287     var state = window.input.current;
288     if (state.continuation)
289         state.continuation(event);
290     else
291         co_call(input_handle_sequence.call(window, event));
295 function input_handle_keypress (event) {
296     if (event.keyCode == 0 && event.charCode == 0 ||
297         event.keyCode == vk_name_to_keycode.caps_lock)
298         return event_kill(event);
299     var window = this;
300     var state = window.input.current;
301     if (state.continuation)
302         state.continuation(event);
303     else
304         co_call(input_handle_sequence.call(window, event));
308 function input_handle_keyup (event) {
309     if (event.keyCode == 0 ||
310         event.keyCode == vk_name_to_keycode.shift ||
311         event.keyCode == vk_name_to_keycode.control ||
312         event.keyCode == vk_name_to_keycode.alt ||
313         event.keyCode == vk_name_to_keycode.caps_lock)
314         return event_kill(event);
315     var window = this;
316     var state = window.input.current;
317     if (state.fallthrough[event.keyCode])
318         delete state.fallthrough[event.keyCode];
319     else
320         event_kill(event);
324 function input_handle_appcommand (event) {
325     var window = this;
326     var state = window.input.current;
327     if (state.continuation)
328         state.continuation(event);
329     else
330         co_call(input_handle_sequence.call(window, event));
334 // handler for command_event special events
335 function input_handle_command (event) {
336     var window = this;
337     var state = window.input.current;
338     if (typeof event == 'string')
339         event = new command_event(event);
340     if (state.continuation)
341         state.continuation(event);
342     else
343         co_call(input_handle_sequence.call(window, event));
347 // handler for special abort event
348 function input_sequence_abort (message) {
349     var window = this;
350     window.input.help_displayed = false;
351     input_help_timer_clear(window);
352     window.minibuffer.clear();
353     if (message)
354         window.minibuffer.show(message);
355     delete window.input.current.continuation;
359 function input_initialize_window (window) {
360     window.input = new input_stack();
361     //window.addEventListener("keydown", input_handle_keydown, true);
362     window.addEventListener("keypress", input_handle_keypress, true);
363     //window.addEventListener("keyup", input_handle_keyup, true);
364     //TO-DO: mousedown, mouseup, click, dblclick
365     window.addEventListener("AppCommand", input_handle_appcommand, true);
368 add_hook("window_initialize_hook", input_initialize_window);
371 interactive("sequence-abort",
372     "Abort an ongoing key sequence.",
373     function (I) { I.minibuffer.message("abort sequence"); });
375 provide("input");