minibuffer.pop_all: js efficiency change
[conkeror.git] / modules / minibuffer.js
blobc4527257804eb75714b69f43b62f2706ad61926a
1 /**
2  * (C) Copyright 2007-2008 Jeremy Maitin-Shepard
3  * (C) Copyright 2009 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 // This should only be used for minibuffer states where it makes
12 // sense.  In particular, it should not be used if additional cleanup
13 // must be done.
14 function minibuffer_abort (window) {
15     var m = window.minibuffer;
16     var s = m.current_state;
17     if (s == null)
18         throw "Invalid minibuffer state";
19     m.pop_state();
20     input_sequence_abort.call(window);
22 interactive("minibuffer-abort", null, function (I) { minibuffer_abort(I.window); });
24 define_builtin_commands("minibuffer-",
25     function (I, command) {
26         try {
27             var m = I.minibuffer;
28             if (m._input_mode_enabled) {
29                 m._restore_normal_state();
30                 var e = m.input_element;
31                 var c = e.controllers.getControllerForCommand(command);
32                 try {
33                     m.ignore_input_events = true;
34                     if (c && c.isCommandEnabled(command))
35                         c.doCommand(command);
36                 } finally {
37                     m.ignore_input_events = false;
38                 }
39                 var s = m.current_state;
40                 if (s.ran_minibuffer_command)
41                     s.ran_minibuffer_command(m, command);
42             }
43         } catch (e) {
44             /* Ignore exceptions. */
45         }
46     },
47     function (I) { //XXX: need return??
48         I.minibuffer.current_state.mark_active = !I.minibuffer.current_state.mark_active;
49     },
50     function (I) I.minibuffer.current_state.mark_active,
51     false);
54 /**
55  * minibuffer_state: abstact base class for minibuffer states.
56  */
57 function minibuffer_state (keymap, use_input_mode) {
58     this.keymap = keymap;
59     this.use_input_mode = use_input_mode;
61 minibuffer_state.prototype = {
62     load: function () {},
63     unload: function () {},
64     destroy: function () {}
68 function minibuffer_message_state (keymap, message, destroy_function) {
69     minibuffer_state.call(this, keymap, false);
70     this._message = message;
71     if (destroy_function)
72         this.destroy = destroy_function;
74 minibuffer_message_state.prototype = {
75     __proto__: minibuffer_state.prototype,
76     load: function (window) {
77         minibuffer_state.prototype.load.call(this, window);
78         this.window = window;
79     },
80     unload: function (window) {
81         this.window = null;
82         minibuffer_state.prototype.unload.call(this, window);
83     },
84     get message () { return this._message; },
85     set message (x) {
86         if (this.window) {
87             this.window.minibuffer._restore_normal_state();
88             this.window.minibuffer._show(this._message);
89         }
90     }
94 function minibuffer_input_state (window, keymap, prompt, input, selection_start, selection_end) {
95     minibuffer_state.call(this, keymap, true);
96     this.prompt = prompt;
97     if (input)
98         this.input = input;
99     else
100         this.input = "";
101     if (selection_start)
102         this.selection_start = selection_start;
103     else
104         this.selection_start = 0;
105     if (selection_end)
106         this.selection_end = selection_end;
107     else
108         this.selection_end = this.selection_start;
109     window.input.begin_recursion();
111 minibuffer_input_state.prototype = {
112     __proto__: minibuffer_state.prototype,
113     mark_active : false,
114     destroy: function (window) {
115         window.input.end_recursion();
116         minibuffer_state.prototype.destroy.call(this, window);
117     }
122  * The parameter `args' is an object specifying the arguments for
123  * basic_minibuffer_state.  The following properties of args must/may
124  * be set:
126  * prompt:            [required]
128  * initial_value:     [optional] specifies the initial text
130  * select:            [optional] specifies to select the initial text if set to non-null
131  */
132 define_keywords("$keymap", "$prompt", "$initial_value", "$select");
133 function basic_minibuffer_state (window) {
134     keywords(arguments, $keymap = minibuffer_base_keymap);
135     var initial_value = arguments.$initial_value || "";
136     var sel_start, sel_end;
137     if (arguments.$select) {
138         sel_start = 0;
139         sel_end = initial_value.length;
140     } else {
141         sel_start = sel_end = initial_value.length;
142     }
143     minibuffer_input_state.call(this, window, arguments.$keymap,
144                                 arguments.$prompt, initial_value,
145                                 sel_start, sel_end);
147 basic_minibuffer_state.prototype = {
148     __proto__: minibuffer_input_state.prototype
152 define_variable("minibuffer_input_mode_show_message_timeout", 1000,
153     "Time duration (in milliseconds) to flash minibuffer messages while in "+
154     "minibuffer input mode.");
157 function minibuffer (window) {
158     this.element = window.document.getElementById("minibuffer");
159     this.output_element = window.document.getElementById("minibuffer-message");
160     this.input_prompt_element = window.document.getElementById("minibuffer-prompt");
161     this.input_element = window.document.getElementById("minibuffer-input");
162     var m = this;
163     this.input_element.inputField.addEventListener("blur",
164         function () {
165             if (m.active && m._input_mode_enabled && !m._showing_message) {
166                 window.setTimeout(function () {
167                         m.input_element.inputField.focus();
168                     }, 0);
169             }
170         }, false);
171     this.input_element.addEventListener("input",
172         function (e) {
173             if (m.ignore_input_events || !m._input_mode_enabled)
174                 return;
175             var s = m.current_state;
176             if (s) {
177                 if (s.handle_input)
178                     s.handle_input(m);
179             }
180         }, true);
182     // Ensure that the input area will have focus if a message is
183     // currently being flashed so that the default handler for key
184     // events will properly add text to the input area.
185     window.addEventListener("keydown",
186         function (e) {
187             if (m._input_mode_enabled && m._showing_message)
188                 m._restore_normal_state();
189         }, true);
190     this.window = window;
191     this.last_message = "";
192     this.states = [];
195 minibuffer.prototype = {
196     constructor: minibuffer.constructor,
197     get _selection_start () { return this.input_element.selectionStart; },
198     get _selection_end () { return this.input_element.selectionEnd; },
199     get _input_text () { return this.input_element.value; },
200     set _input_text (text) { this.input_element.value = text; },
201     get prompt () { return this.input_prompt_element.value; },
202     set prompt (s) { this.input_prompt_element.value = s; },
204     set_input_state: function (x) {
205         this._input_text = x[0];
206         this._set_selection(x[1], x[2]);
207     },
209     _set_selection: function (start, end) {
210         if (start == null)
211             start = this._input_text.length;
212         if (end == null)
213             end = this._input_text.length;
214         this.input_element.setSelectionRange(start,end);
215     },
217     /* Saved focus state */
218     saved_focused_frame: null,
219     saved_focused_element: null,
221     default_message: "",
223     current_message: null,
225     /* This method will display the specified string in the
226      * minibuffer, without recording it in any log/Messages buffer. */
227     show: function (str, force) {
228         if (!this.active || force) {
229             this.current_message = str;
230             this._show(str);
231         }
232     },
234     _show: function (str) {
235         if (this.last_message != str) {
236             this.output_element.value = str;
237             this.last_message = str;
238         }
239     },
241     message: function (str) {
242         /* TODO: add the message to a *Messages* buffer, and/or
243          * possibly dump them to the console. */
244         if (str == "")
245             this.clear();
246         else {
247             this.show(str, true /* force */);
248             if (this.active)
249                 this._flash_temporary_message();
250         }
251     },
253     clear: function () {
254         this.current_message = null;
255         if (!this.active)
256             this._show(this.default_message);
257     },
259     set_default_message: function (str) {
260         this.default_message = str;
261         if (this.current_message == null)
262             this._show(str);
263     },
265     get current_state () {
266         if (! this.states[0])
267             return null;
268         return this.states[this.states.length - 1];
269     },
271     push_state: function (state) {
272         this._save_state();
273         this.states.push(state);
274         this._restore_state();
275     },
277     pop_state: function () {
278         this.current_state.destroy(this.window);
279         this.states.pop();
280         this._restore_state();
281     },
283     pop_all: function () {
284         var state;
285         while ((state = this.current_state)) {
286             state.destroy(this.window);
287             this.states.pop();
288         }
289     },
291     remove_state: function (state) {
292         var i = this.states.indexOf(state);
293         if (i == -1)
294             return;
295         var was_current = (i == (this.states.length - 1));
296         state.destroy(this.window);
297         this.states.splice(i, 1);
298         if (was_current)
299             this._restore_state();
300     },
302     _input_mode_enabled: false,
304     active: false,
306     /* If _input_mode_enabled is true, this is set to indicate that
307      * the message area is being temporarily shown instead of the
308      * input box. */
309     _showing_message: false,
311     _message_timer_ID: null,
313     /* This must only be called if _input_mode_enabled is true */
314     //XXX: if it must only be called if _input_mode_enabled is true,
315     //     then why does it have an else condition?
316     _restore_normal_state: function () {
317         if (this._showing_message) {
318             this.window.clearTimeout(this._message_timer_ID);
319             this._message_timer_ID = null;
320             this._showing_message = false;
322             if (this._input_mode_enabled)
323                 this._switch_to_input_mode();
324             else
325                 // assumes that anything other than an input state is a
326                 // minibuffer_message_state.
327                 this._show(this.current_state._message);
328         }
329     },
331     /* This must only be called if _input_mode_enabled is true */
332     _flash_temporary_message: function () {
333         if (this._showing_message)
334             this.window.clearTimeout(this._message_timer_ID);
335         else {
336             this._showing_message = true;
337             if (this._input_mode_enabled)
338                 this._switch_to_message_mode();
339         }
340         var obj = this;
341         this._message_timer_ID = this.window.setTimeout(function () {
342             obj._restore_normal_state();
343         }, minibuffer_input_mode_show_message_timeout);
344     },
346     _switch_to_input_mode: function () {
347         this.element.setAttribute("minibuffermode", "input");
348         this.input_element.inputField.focus();
349     },
351     _switch_to_message_mode: function () {
352         this.element.setAttribute("minibuffermode", "message");
353     },
355     _restore_state: function () {
356         var s = this.current_state;
357         var want_input_mode = false;
358         if (s) {
359             if (!this.active) {
360                 this.saved_focused_frame = this.window.document.commandDispatcher.focusedWindow;
361                 this.saved_focused_element = this.window.document.commandDispatcher.focusedElement;
362             }
363             if (s.use_input_mode) {
364                 want_input_mode = true;
365                 this._input_text = s.input;
366                 this.prompt = s.prompt;
367                 this._set_selection(s.selection_start, s.selection_end);
368             } else {
369                 this._show(s._message);
370             }
371             s.load(this.window);
372             this.window.input.current.override_keymap = s.keymap;
373             this.active = true;
374         } else {
375             if (this.active) {
376                 this.active = false;
377                 this.window.input.current.override_keymap = null;
378                 this.window.buffers.current.browser.focus();
379                 if (this.saved_focused_element)
380                     set_focus_no_scroll(this.window, this.saved_focused_element);
381                 else if (this.saved_focused_frame)
382                     set_focus_no_scroll(this.window, this.saved_focused_frame);
383                 this.saved_focused_element = null;
384                 this.saved_focused_frame = null;
385                 this._show(this.current_message || this.default_message);
386             }
387         }
388         var in_input_mode = this._input_mode_enabled && !this._showing_message;
389         if (this._showing_message) {
390             this.window.clearTimeout(this._message_timer_ID);
391             this._message_timer_ID = null;
392             this._showing_message = false;
393         }
394         if (want_input_mode && !in_input_mode)
395             this._switch_to_input_mode();
396         else if (!want_input_mode && in_input_mode)
397             this._switch_to_message_mode();
398         this._input_mode_enabled = want_input_mode;
399     },
401     _save_state: function () {
402         var s = this.current_state;
403         if (s) {
404             if (s.use_input_mode) {
405                 s.input = this._input_text;
406                 s.prompt = this.prompt;
407                 s.selection_start = this._selection_start;
408                 s.selection_end = this._selection_end;
409             }
410             s.unload(this.window);
411         }
412     },
414     insert_before: function (element) {
415         this.element.parentNode.insertBefore(element, this.element);
416     }
420 function minibuffer_initialize_window (window) {
421     window.minibuffer = new minibuffer(window);
423 add_hook("window_initialize_early_hook", minibuffer_initialize_window);
426 function minibuffer_window_close_handler (window) {
427     window.minibuffer.pop_all();
429 add_hook("window_close_hook", minibuffer_window_close_handler);
432 /* Note: This is concise, but doesn't seem to be useful in practice,
433  * because nothing can be done with the state alone. */
434 minibuffer.prototype.check_state = function (type) {
435     var s = this.current_state;
436     if (!(s instanceof type))
437         throw new Error("Invalid minibuffer state.");
438     return s;
441 minibuffer.prototype.show_wait_message = function (initial_message, destroy_function) {
442     var s = new minibuffer_message_state(minibuffer_message_keymap, initial_message, destroy_function);
443     this.push_state(s);
444     return s;
447 minibuffer.prototype.wait_for = function (message, coroutine) {
448     var cc = yield CONTINUATION;
449     var done = false;
450     var s = this.show_wait_message(message, function () { if (!done) cc.throw(abort()); });
451     var result;
452     try {
453         result = yield coroutine;
454     } finally {
455         done = true;
456         this.remove_state(s);
457     }
458     yield co_return(result);
461 provide("minibuffer");