jsx module system
[conkeror.git] / modules / minibuffer.js
blob807e423883e38567f441cf5fe6f032cb0ef699a7
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 /**
10  * minibuffer_state: abstact base class for minibuffer states.
11  */
12 function minibuffer_state (minibuffer, keymap) {
13     this.minibuffer = minibuffer;
14     this.keymaps = [default_base_keymap, keymap];
16 minibuffer_state.prototype = {
17     constructor: minibuffer_state,
18     load: function () {},
19     unload: function () {},
20     destroy: function () {}
24 /**
25  * minibuffer_message_state: base class for minibuffer states which do not
26  * use the input element, but still may use a keymap.
27  */
28 function minibuffer_message_state (minibuffer, keymap, message, cleanup_function) {
29     minibuffer_state.call(this, minibuffer, keymap);
30     this._message = message;
31     this.cleanup_function = cleanup_function;
33 minibuffer_message_state.prototype = {
34     constructor: minibuffer_message_state,
35     __proto__: minibuffer_state.prototype,
36     _message: null,
37     get message () { return this._message; },
38     set message (x) {
39         this.minibuffer._restore_normal_state();
40         this.minibuffer._show(this._message);
41     },
42     load: function () {
43         minibuffer_state.prototype.load.call(this);
44         this.minibuffer._show(this.message);
45     },
46     cleanup_function: null,
47     destroy: function () {
48         if (this.cleanup_function)
49             this.cleanup_function();
50         minibuffer_state.prototype.destroy.call(this);
51     }
55 /**
56  * minibuffer_input_state: base class for minibuffer states which use the
57  * input element.
58  */
59 function minibuffer_input_state (minibuffer, keymap, prompt, input, selection_start, selection_end) {
60     minibuffer_state.call(this, minibuffer, keymap);
61     this.prompt = prompt;
62     if (input)
63         this.input = input;
64     else
65         this.input = "";
66     if (selection_start)
67         this.selection_start = selection_start;
68     else
69         this.selection_start = 0;
70     if (selection_end)
71         this.selection_end = selection_end;
72     else
73         this.selection_end = this.selection_start;
74     this.minibuffer.window.input.begin_recursion();
76 minibuffer_input_state.prototype = {
77     constructor: minibuffer_input_state,
78     __proto__: minibuffer_state.prototype,
79     mark_active: false,
80     load: function () {
81         minibuffer_state.prototype.load.call(this);
82         this.minibuffer.ignore_input_events = true;
83         this.minibuffer._input_text = this.input;
84         this.minibuffer.ignore_input_events = false;
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     function dispatch_handle_input () {
155         if (m.ignore_input_events || !m._input_mode_enabled)
156             return;
157         var s = m.current_state;
158         if (s && s.handle_input)
159             s.handle_input(m);
160     }
161     this.input_element.addEventListener("input", dispatch_handle_input, true);
162     this.input_element.watch("value",
163         function (prop, oldval, newval) {
164             if (newval != oldval &&
165                 !m.ignore_input_events)
166             {
167                 call_after_timeout(dispatch_handle_input, 0);
168             }
169             return newval;
170         });
171     // Ensure that the input area will have focus if a message is
172     // currently being flashed so that the default handler for key
173     // events will properly add text to the input area.
174     window.addEventListener("keydown",
175         function (e) {
176             if (m._input_mode_enabled && m._showing_message)
177                 m._restore_normal_state();
178         }, true);
179     this.window = window;
180     this.last_message = "";
181     this.states = [];
183 minibuffer.prototype = {
184     constructor: minibuffer,
185     get _selection_start () { return this.input_element.selectionStart; },
186     get _selection_end () { return this.input_element.selectionEnd; },
187     get _input_text () { return this.input_element.value; },
188     set _input_text (text) { this.input_element.value = text; },
189     get prompt () { return this.input_prompt_element.value; },
190     set prompt (s) { this.input_prompt_element.value = s; },
192     set_input_state: function (x) {
193         this._input_text = x[0];
194         this._set_selection(x[1], x[2]);
195     },
197     _set_selection: function (start, end) {
198         if (start == null)
199             start = this._input_text.length;
200         if (end == null)
201             end = this._input_text.length;
202         this.input_element.setSelectionRange(start,end);
203     },
205     /* Saved focus state */
206     saved_focused_frame: null,
207     saved_focused_element: null,
209     default_message: "",
211     current_message: null,
213     /* This method will display the specified string in the
214      * minibuffer, without recording it in any log/Messages buffer. */
215     show: function (str, force) {
216         if (!this.active || force) {
217             this.current_message = str;
218             this._show(str);
219         }
220     },
222     _show: function (str) {
223         if (this.last_message != str) {
224             this.output_element.value = str;
225             this.last_message = str;
226         }
227     },
229     message: function (str) {
230         if (str == "")
231             this.clear();
232         else {
233             this.show(str, true /* force */);
234             if (this.active)
235                 this._flash_temporary_message();
236         }
237     },
239     clear: function () {
240         this.current_message = null;
241         if (!this.active)
242             this._show(this.default_message);
243     },
245     set_default_message: function (str) {
246         this.default_message = str;
247         if (this.current_message == null)
248             this._show(str);
249     },
251     get current_state () {
252         if (! this.states[0])
253             return null;
254         return this.states[this.states.length - 1];
255     },
257     push_state: function (state) {
258         this._save_state();
259         this.states.push(state);
260         this._restore_state();
261     },
263     pop_state: function () {
264         this.current_state.destroy();
265         this.states.pop();
266         this._restore_state();
267     },
269     pop_all: function () {
270         var state;
271         while ((state = this.current_state)) {
272             state.destroy();
273             this.states.pop();
274         }
275     },
277     //XXX: breaking stack discipline can cause incorrect
278     //     input recursion termination
279     remove_state: function (state) {
280         var i = this.states.indexOf(state);
281         if (i == -1)
282             return;
283         var was_current = (i == (this.states.length - 1));
284         state.destroy();
285         this.states.splice(i, 1);
286         if (was_current)
287             this._restore_state();
288     },
290     _input_mode_enabled: false,
292     active: false,
294     /* If _input_mode_enabled is true, this is set to indicate that
295      * the message area is being temporarily shown instead of the
296      * input box. */
297     _showing_message: false,
299     _message_timer_ID: null,
301     /* This must only be called if _input_mode_enabled is true */
302     //XXX: if it must only be called if _input_mode_enabled is true, then
303     //     why does it have an else condition for handling
304     //     minibuffer_message_state states?
305     _restore_normal_state: function () {
306         if (this._showing_message) {
307             this.window.clearTimeout(this._message_timer_ID);
308             this._message_timer_ID = null;
309             this._showing_message = false;
311             if (this._input_mode_enabled)
312                 this._switch_to_input_mode();
313             else
314                 // assumes that anything other than an input state is a
315                 // minibuffer_message_state.
316                 this._show(this.current_state._message);
317         }
318     },
320     /* This must only be called if _input_mode_enabled is true */
321     _flash_temporary_message: function () {
322         if (this._showing_message)
323             this.window.clearTimeout(this._message_timer_ID);
324         else {
325             this._showing_message = true;
326             if (this._input_mode_enabled)
327                 this._switch_to_message_mode();
328         }
329         var obj = this;
330         this._message_timer_ID = this.window.setTimeout(function () {
331             obj._restore_normal_state();
332         }, minibuffer_input_mode_show_message_timeout);
333     },
335     _switch_to_input_mode: function () {
336         this.element.setAttribute("minibuffermode", "input");
337         this.input_element.inputField.focus();
338     },
340     _switch_to_message_mode: function () {
341         this.element.setAttribute("minibuffermode", "message");
342     },
344     _restore_state: function () {
345         var s = this.current_state;
346         if (s) {
347             if (!this.active) {
348                 this.saved_focused_frame = this.window.document.commandDispatcher.focusedWindow;
349                 this.saved_focused_element = this.window.document.commandDispatcher.focusedElement;
350             }
351             s.load();
352             this.active = true;
353         } else {
354             if (this.active) {
355                 this.active = false;
356                 this.window.buffers.current.browser.focus();
357                 if (this.saved_focused_element && this.saved_focused_element.focus)
358                     set_focus_no_scroll(this.window, this.saved_focused_element);
359                 else if (this.saved_focused_frame)
360                     set_focus_no_scroll(this.window, this.saved_focused_frame);
361                 this.saved_focused_element = null;
362                 this.saved_focused_frame = null;
363                 this._show(this.current_message || this.default_message);
364             }
365         }
366         if (this._showing_message) {
367             this.window.clearTimeout(this._message_timer_ID);
368             this._message_timer_ID = null;
369             this._showing_message = false;
370         }
371         var want_input_mode = s instanceof minibuffer_input_state;
372         var in_input_mode = this._input_mode_enabled && !this._showing_message;
373         if (want_input_mode && !in_input_mode)
374             this._switch_to_input_mode();
375         else if (!want_input_mode && in_input_mode)
376             this._switch_to_message_mode();
377         this._input_mode_enabled = want_input_mode;
378     },
380     _save_state: function () {
381         var s = this.current_state;
382         if (s)
383             s.unload();
384     },
386     insert_before: function (element) {
387         this.element.parentNode.insertBefore(element, this.element);
388     }
392 function minibuffer_initialize_window (window) {
393     window.minibuffer = new minibuffer(window);
395 add_hook("window_initialize_early_hook", minibuffer_initialize_window);
398 function minibuffer_window_close_handler (window) {
399     window.minibuffer.pop_all();
401 add_hook("window_close_hook", minibuffer_window_close_handler);
404 /* Note: This is concise, but doesn't seem to be useful in practice,
405  * because nothing can be done with the state alone. */
406 minibuffer.prototype.check_state = function (type) {
407     var s = this.current_state;
408     if (!(s instanceof type))
409         throw new Error("Invalid minibuffer state.");
410     return s;
413 minibuffer.prototype.show_wait_message = function (initial_message, cleanup_function) {
414     var s = new minibuffer_message_state(this, minibuffer_message_keymap, initial_message, cleanup_function);
415     this.push_state(s);
416     return s;
419 minibuffer.prototype.wait_for = function (message, coroutine) {
420     var cc = yield CONTINUATION;
421     var done = false;
422     var s = this.show_wait_message(message, function () { if (!done) cc.throw(abort()); });
423     var result;
424     try {
425         result = yield coroutine;
426     } finally {
427         done = true;
428         this.remove_state(s);
429     }
430     yield co_return(result);
434 // This should only be used for minibuffer states where it makes
435 // sense.  In particular, it should not be used if additional cleanup
436 // must be done.
437 function minibuffer_abort (window) {
438     var m = window.minibuffer;
439     var s = m.current_state;
440     if (s == null)
441         throw "Invalid minibuffer state";
442     m.pop_state();
443     input_sequence_abort.call(window);
445 interactive("minibuffer-abort", null, function (I) { minibuffer_abort(I.window); });
448 provide("minibuffer");