buffer.override_keymaps: dealt with privately inside buffer
[conkeror.git] / modules / minibuffer.js
blobf21c133d0eb95bea91948284803d15b105001b62
1 /**
2  * (C) Copyright 2007-2008 Jeremy Maitin-Shepard
3  * (C) Copyright 2009-2010 John J. Foerch
4  *
5  * Use, modification, and distribution are subject to the terms specified in the
6  * COPYING file.
7 **/
9 in_module(null);
11 /**
12  * minibuffer_state: abstact base class for minibuffer states.
13  */
14 function minibuffer_state (minibuffer, keymap) {
15     this.minibuffer = minibuffer;
16     this.keymap = keymap;
18 minibuffer_state.prototype = {
19     constructor: minibuffer_state,
20     load: function () {},
21     unload: function () {},
22     destroy: function () {}
26 /**
27  * minibuffer_message_state: base class for minibuffer states which do not
28  * use the input element, but still may use a keymap.
29  */
30 function minibuffer_message_state (minibuffer, keymap, message, cleanup_function) {
31     minibuffer_state.call(this, minibuffer, keymap);
32     this._message = message;
33     this.cleanup_function = cleanup_function;
35 minibuffer_message_state.prototype = {
36     constructor: minibuffer_message_state,
37     __proto__: minibuffer_state.prototype,
38     _message: null,
39     get message () { return this._message; },
40     set message (x) {
41         this.minibuffer._restore_normal_state();
42         this.minibuffer._show(this._message);
43     },
44     load: function () {
45         minibuffer_state.prototype.load.call(this);
46         this.minibuffer._show(this.message);
47     },
48     cleanup_function: null,
49     destroy: function () {
50         if (this.cleanup_function)
51             this.cleanup_function();
52         minibuffer_state.prototype.destroy.call(this);
53     }
57 /**
58  * minibuffer_input_state: base class for minibuffer states which use the
59  * input element.
60  */
61 function minibuffer_input_state (minibuffer, keymap, prompt, input, selection_start, selection_end) {
62     minibuffer_state.call(this, minibuffer, keymap);
63     this.prompt = prompt;
64     if (input)
65         this.input = input;
66     else
67         this.input = "";
68     if (selection_start)
69         this.selection_start = selection_start;
70     else
71         this.selection_start = 0;
72     if (selection_end)
73         this.selection_end = selection_end;
74     else
75         this.selection_end = this.selection_start;
76     this.minibuffer.window.input.begin_recursion();
78 minibuffer_input_state.prototype = {
79     constructor: minibuffer_input_state,
80     __proto__: minibuffer_state.prototype,
81     mark_active: false,
82     load: function () {
83         minibuffer_state.prototype.load.call(this);
84         this.minibuffer._input_text = this.input;
85         this.minibuffer.prompt = this.prompt;
86         this.minibuffer._set_selection(this.selection_start,
87                                        this.selection_end);
88     },
89     unload: function () {
90         this.input = this.minibuffer._input_text;
91         this.prompt = this.minibuffer.prompt;
92         this.selection_start = this.minibuffer._selection_start;
93         this.selection_end = this.minibuffer._selection_end;
94         minibuffer_state.prototype.unload.call(this);
95     },
96     destroy: function () {
97         this.minibuffer.window.input.end_recursion();
98         minibuffer_state.prototype.destroy.call(this);
99     }
104  * The parameter `args' is an object specifying the arguments for
105  * basic_minibuffer_state.  The following properties of args must/may
106  * be set:
108  * prompt:            [required]
110  * initial_value:     [optional] specifies the initial text
112  * select:            [optional] specifies to select the initial text if set to non-null
113  */
114 define_keywords("$keymap", "$prompt", "$initial_value", "$select");
115 function basic_minibuffer_state (minibuffer) {
116     keywords(arguments, $keymap = minibuffer_base_keymap);
117     var initial_value = arguments.$initial_value || "";
118     var sel_start, sel_end;
119     if (arguments.$select) {
120         sel_start = 0;
121         sel_end = initial_value.length;
122     } else {
123         sel_start = sel_end = initial_value.length;
124     }
125     minibuffer_input_state.call(this, minibuffer, arguments.$keymap,
126                                 arguments.$prompt, initial_value,
127                                 sel_start, sel_end);
129 basic_minibuffer_state.prototype = {
130     constructor: basic_minibuffer_state,
131     __proto__: minibuffer_input_state.prototype
135 define_variable("minibuffer_input_mode_show_message_timeout", 1000,
136     "Time duration (in milliseconds) to flash minibuffer messages while in "+
137     "minibuffer input mode.");
140 function minibuffer (window) {
141     this.element = window.document.getElementById("minibuffer");
142     this.output_element = window.document.getElementById("minibuffer-message");
143     this.input_prompt_element = window.document.getElementById("minibuffer-prompt");
144     this.input_element = window.document.getElementById("minibuffer-input");
145     var m = this;
146     this.input_element.inputField.addEventListener("blur",
147         function () {
148             if (m.active && m._input_mode_enabled && !m._showing_message) {
149                 window.setTimeout(function () {
150                         m.input_element.inputField.focus();
151                     }, 0);
152             }
153         }, false);
154     this.input_element.addEventListener("input",
155         function (e) {
156             if (m.ignore_input_events || !m._input_mode_enabled)
157                 return;
158             var s = m.current_state;
159             if (s) {
160                 if (s.handle_input)
161                     s.handle_input(m);
162             }
163         }, true);
165     // Ensure that the input area will have focus if a message is
166     // currently being flashed so that the default handler for key
167     // events will properly add text to the input area.
168     window.addEventListener("keydown",
169         function (e) {
170             if (m._input_mode_enabled && m._showing_message)
171                 m._restore_normal_state();
172         }, true);
173     this.window = window;
174     this.last_message = "";
175     this.states = [];
177 minibuffer.prototype = {
178     constructor: minibuffer,
179     get _selection_start () { return this.input_element.selectionStart; },
180     get _selection_end () { return this.input_element.selectionEnd; },
181     get _input_text () { return this.input_element.value; },
182     set _input_text (text) { this.input_element.value = text; },
183     get prompt () { return this.input_prompt_element.value; },
184     set prompt (s) { this.input_prompt_element.value = s; },
186     set_input_state: function (x) {
187         this._input_text = x[0];
188         this._set_selection(x[1], x[2]);
189     },
191     _set_selection: function (start, end) {
192         if (start == null)
193             start = this._input_text.length;
194         if (end == null)
195             end = this._input_text.length;
196         this.input_element.setSelectionRange(start,end);
197     },
199     /* Saved focus state */
200     saved_focused_frame: null,
201     saved_focused_element: null,
203     default_message: "",
205     current_message: null,
207     /* This method will display the specified string in the
208      * minibuffer, without recording it in any log/Messages buffer. */
209     show: function (str, force) {
210         if (!this.active || force) {
211             this.current_message = str;
212             this._show(str);
213         }
214     },
216     _show: function (str) {
217         if (this.last_message != str) {
218             this.output_element.value = str;
219             this.last_message = str;
220         }
221     },
223     message: function (str) {
224         /* TODO: add the message to a *Messages* buffer, and/or
225          * possibly dump them to the console. */
226         if (str == "")
227             this.clear();
228         else {
229             this.show(str, true /* force */);
230             if (this.active)
231                 this._flash_temporary_message();
232         }
233     },
235     clear: function () {
236         this.current_message = null;
237         if (!this.active)
238             this._show(this.default_message);
239     },
241     set_default_message: function (str) {
242         this.default_message = str;
243         if (this.current_message == null)
244             this._show(str);
245     },
247     get current_state () {
248         if (! this.states[0])
249             return null;
250         return this.states[this.states.length - 1];
251     },
253     push_state: function (state) {
254         this._save_state();
255         this.states.push(state);
256         this._restore_state();
257     },
259     pop_state: function () {
260         this.current_state.destroy();
261         this.states.pop();
262         this._restore_state();
263     },
265     pop_all: function () {
266         var state;
267         while ((state = this.current_state)) {
268             state.destroy();
269             this.states.pop();
270         }
271     },
273     //XXX: breaking stack discipline can cause incorrect
274     //     input recursion termination
275     remove_state: function (state) {
276         var i = this.states.indexOf(state);
277         if (i == -1)
278             return;
279         var was_current = (i == (this.states.length - 1));
280         state.destroy();
281         this.states.splice(i, 1);
282         if (was_current)
283             this._restore_state();
284     },
286     _input_mode_enabled: false,
288     active: false,
290     /* If _input_mode_enabled is true, this is set to indicate that
291      * the message area is being temporarily shown instead of the
292      * input box. */
293     _showing_message: false,
295     _message_timer_ID: null,
297     /* This must only be called if _input_mode_enabled is true */
298     //XXX: if it must only be called if _input_mode_enabled is true, then
299     //     why does it have an else condition for handling
300     //     minibuffer_message_state states?
301     _restore_normal_state: function () {
302         if (this._showing_message) {
303             this.window.clearTimeout(this._message_timer_ID);
304             this._message_timer_ID = null;
305             this._showing_message = false;
307             if (this._input_mode_enabled)
308                 this._switch_to_input_mode();
309             else
310                 // assumes that anything other than an input state is a
311                 // minibuffer_message_state.
312                 this._show(this.current_state._message);
313         }
314     },
316     /* This must only be called if _input_mode_enabled is true */
317     _flash_temporary_message: function () {
318         if (this._showing_message)
319             this.window.clearTimeout(this._message_timer_ID);
320         else {
321             this._showing_message = true;
322             if (this._input_mode_enabled)
323                 this._switch_to_message_mode();
324         }
325         var obj = this;
326         this._message_timer_ID = this.window.setTimeout(function () {
327             obj._restore_normal_state();
328         }, minibuffer_input_mode_show_message_timeout);
329     },
331     _switch_to_input_mode: function () {
332         this.element.setAttribute("minibuffermode", "input");
333         this.input_element.inputField.focus();
334     },
336     _switch_to_message_mode: function () {
337         this.element.setAttribute("minibuffermode", "message");
338     },
340     _restore_state: function () {
341         var s = this.current_state;
342         if (s) {
343             if (!this.active) {
344                 this.saved_focused_frame = this.window.document.commandDispatcher.focusedWindow;
345                 this.saved_focused_element = this.window.document.commandDispatcher.focusedElement;
346             }
347             s.load();
348             this.active = true;
349         } else {
350             if (this.active) {
351                 this.active = false;
352                 this.window.buffers.current.browser.focus();
353                 if (this.saved_focused_element && this.saved_focused_element.focus)
354                     set_focus_no_scroll(this.window, this.saved_focused_element);
355                 else if (this.saved_focused_frame)
356                     set_focus_no_scroll(this.window, this.saved_focused_frame);
357                 this.saved_focused_element = null;
358                 this.saved_focused_frame = null;
359                 this._show(this.current_message || this.default_message);
360             }
361         }
362         if (this._showing_message) {
363             this.window.clearTimeout(this._message_timer_ID);
364             this._message_timer_ID = null;
365             this._showing_message = false;
366         }
367         var want_input_mode = s instanceof minibuffer_input_state;
368         var in_input_mode = this._input_mode_enabled && !this._showing_message;
369         if (want_input_mode && !in_input_mode)
370             this._switch_to_input_mode();
371         else if (!want_input_mode && in_input_mode)
372             this._switch_to_message_mode();
373         this._input_mode_enabled = want_input_mode;
374     },
376     _save_state: function () {
377         var s = this.current_state;
378         if (s)
379             s.unload();
380     },
382     insert_before: function (element) {
383         this.element.parentNode.insertBefore(element, this.element);
384     }
388 function minibuffer_initialize_window (window) {
389     window.minibuffer = new minibuffer(window);
391 add_hook("window_initialize_early_hook", minibuffer_initialize_window);
394 function minibuffer_window_close_handler (window) {
395     window.minibuffer.pop_all();
397 add_hook("window_close_hook", minibuffer_window_close_handler);
400 /* Note: This is concise, but doesn't seem to be useful in practice,
401  * because nothing can be done with the state alone. */
402 minibuffer.prototype.check_state = function (type) {
403     var s = this.current_state;
404     if (!(s instanceof type))
405         throw new Error("Invalid minibuffer state.");
406     return s;
409 minibuffer.prototype.show_wait_message = function (initial_message, cleanup_function) {
410     var s = new minibuffer_message_state(this, minibuffer_message_keymap, initial_message, cleanup_function);
411     this.push_state(s);
412     return s;
415 minibuffer.prototype.wait_for = function (message, coroutine) {
416     var cc = yield CONTINUATION;
417     var done = false;
418     var s = this.show_wait_message(message, function () { if (!done) cc.throw(abort()); });
419     var result;
420     try {
421         result = yield coroutine;
422     } finally {
423         done = true;
424         this.remove_state(s);
425     }
426     yield co_return(result);
430 // This should only be used for minibuffer states where it makes
431 // sense.  In particular, it should not be used if additional cleanup
432 // must be done.
433 function minibuffer_abort (window) {
434     var m = window.minibuffer;
435     var s = m.current_state;
436     if (s == null)
437         throw "Invalid minibuffer state";
438     m.pop_state();
439     input_sequence_abort.call(window);
441 interactive("minibuffer-abort", null, function (I) { minibuffer_abort(I.window); });
443 define_builtin_commands("minibuffer-",
444     function (I, command) {
445         try {
446             var m = I.minibuffer;
447             if (m._input_mode_enabled) {
448                 m._restore_normal_state();
449                 var e = m.input_element;
450                 var c = e.controllers.getControllerForCommand(command);
451                 try {
452                     m.ignore_input_events = true;
453                     if (c && c.isCommandEnabled(command))
454                         c.doCommand(command);
455                 } finally {
456                     m.ignore_input_events = false;
457                 }
458                 var s = m.current_state;
459                 if (s.ran_minibuffer_command)
460                     s.ran_minibuffer_command(m, command);
461             }
462         } catch (e) {
463             /* Ignore exceptions. */
464         }
465     },
466     function (I) { //XXX: need return??
467         I.minibuffer.current_state.mark_active = !I.minibuffer.current_state.mark_active;
468     },
469     function (I) I.minibuffer.current_state.mark_active,
470     false);
473 provide("minibuffer");