spawn-process: Better error information in case of timeout
[conkeror.git] / modules / minibuffer.js
blob747855c29fae29cd3c71c363a32ed114a5df26d5
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;
32     this.loaded = false;
34 minibuffer_message_state.prototype = {
35     constructor: minibuffer_message_state,
36     __proto__: minibuffer_state.prototype,
37     _message: null,
38     get message () { return this._message; },
39     set message (x) {
40         if (this.loaded) {
41             this.minibuffer._restore_normal_state();
42             this.minibuffer._show(this._message);
43         }
44     },
45     load: function () {
46         minibuffer_state.prototype.load.call(this);
47         this.minibuffer._show(this.message);
48         this.loaded = true;
49     },
50     unload: function () {
51         this.loaded = false;
52         minibuffer_state.prototype.unload.call(this);
53     },
54     cleanup_function: null,
55     destroy: function () {
56         if (this.cleanup_function)
57             this.cleanup_function();
58         minibuffer_state.prototype.destroy.call(this);
59     }
63 /**
64  * minibuffer_input_state: base class for minibuffer states which use the
65  * input element.
66  */
67 function minibuffer_input_state (minibuffer, keymap, prompt, input, selection_start, selection_end) {
68     minibuffer_state.call(this, minibuffer, keymap);
69     this.prompt = prompt;
70     if (input)
71         this.input = input;
72     else
73         this.input = "";
74     if (selection_start)
75         this.selection_start = selection_start;
76     else
77         this.selection_start = 0;
78     if (selection_end)
79         this.selection_end = selection_end;
80     else
81         this.selection_end = this.selection_start;
83 minibuffer_input_state.prototype = {
84     constructor: minibuffer_input_state,
85     __proto__: minibuffer_state.prototype,
86     mark_active: false,
87     load: function () {
88         minibuffer_state.prototype.load.call(this);
89         this.minibuffer.ignore_input_events = true;
90         this.minibuffer._input_text = this.input;
91         this.minibuffer.ignore_input_events = false;
92         this.minibuffer.prompt = this.prompt;
93         this.minibuffer._set_selection(this.selection_start,
94                                        this.selection_end);
95     },
96     unload: function () {
97         this.input = this.minibuffer._input_text;
98         this.prompt = this.minibuffer.prompt;
99         this.selection_start = this.minibuffer._selection_start;
100         this.selection_end = this.minibuffer._selection_end;
101         minibuffer_state.prototype.unload.call(this);
102     },
103     destroy: function () {
104         minibuffer_state.prototype.destroy.call(this);
105     }
110  * The parameter `args' is an object specifying the arguments for
111  * basic_minibuffer_state.  The following properties of args must/may
112  * be set:
114  * prompt:            [required]
116  * initial_value:     [optional] specifies the initial text
118  * select:            [optional] specifies to select the initial text if set to non-null
119  */
120 define_keywords("$keymap", "$prompt", "$initial_value", "$select");
121 function basic_minibuffer_state (minibuffer) {
122     keywords(arguments, $keymap = minibuffer_base_keymap);
123     var initial_value = arguments.$initial_value || "";
124     var sel_start, sel_end;
125     if (arguments.$select) {
126         sel_start = 0;
127         sel_end = initial_value.length;
128     } else {
129         sel_start = sel_end = initial_value.length;
130     }
131     minibuffer_input_state.call(this, minibuffer, arguments.$keymap,
132                                 arguments.$prompt, initial_value,
133                                 sel_start, sel_end);
135 basic_minibuffer_state.prototype = {
136     constructor: basic_minibuffer_state,
137     __proto__: minibuffer_input_state.prototype
141 define_variable("minibuffer_input_mode_show_message_timeout", 1000,
142     "Time duration (in milliseconds) to flash minibuffer messages while in "+
143     "minibuffer input mode.");
146 function minibuffer (window) {
147     this.element = window.document.getElementById("minibuffer");
148     this.output_element = window.document.getElementById("minibuffer-message");
149     this.input_prompt_element = window.document.getElementById("minibuffer-prompt");
150     this.input_element = window.document.getElementById("minibuffer-input");
151     var m = this;
152     this.input_element.inputField.addEventListener("blur",
153         function () {
154             if (m.active && m._input_mode_enabled && !m._showing_message) {
155                 window.setTimeout(function () {
156                         m.input_element.inputField.focus();
157                     }, 0);
158             }
159         }, false);
160     function dispatch_handle_input () {
161         if (m.ignore_input_events || !m._input_mode_enabled)
162             return;
163         var s = m.current_state;
164         if (s && s.handle_input)
165             s.handle_input(m);
166     }
167     this.input_element.addEventListener("input", dispatch_handle_input, true);
168     this.input_element.watch("value",
169         function (prop, oldval, newval) {
170             if (newval != oldval &&
171                 !m.ignore_input_events)
172             {
173                 call_after_timeout(dispatch_handle_input, 0);
174             }
175             return newval;
176         });
177     // Ensure that the input area will have focus if a message is
178     // currently being flashed so that the default handler for key
179     // events will properly add text to the input area.
180     window.addEventListener("keydown",
181         function (e) {
182             if (m._input_mode_enabled && m._showing_message)
183                 m._restore_normal_state();
184         }, true);
185     this.window = window;
186     this.last_message = "";
187     this.states = [];
189 minibuffer.prototype = {
190     constructor: minibuffer,
191     toString: function () "#<minibuffer>",
193     get _selection_start () { return this.input_element.selectionStart; },
194     get _selection_end () { return this.input_element.selectionEnd; },
195     get _input_text () { return this.input_element.value; },
196     set _input_text (text) { this.input_element.value = text; },
197     get prompt () { return this.input_prompt_element.value; },
198     set prompt (s) { this.input_prompt_element.value = s; },
200     set_input_state: function (x) {
201         this._input_text = x[0];
202         this._set_selection(x[1], x[2]);
203     },
205     _set_selection: function (start, end) {
206         if (start == null)
207             start = this._input_text.length;
208         if (end == null)
209             end = this._input_text.length;
210         this.input_element.setSelectionRange(start,end);
211     },
213     /* Saved focus state */
214     saved_focused_frame: null,
215     saved_focused_element: null,
217     default_message: "",
219     current_message: null,
221     /* This method will display the specified string in the
222      * minibuffer, without recording it in any log/Messages buffer. */
223     show: function (str, force) {
224         if (!this.active || force) {
225             this.current_message = str;
226             this._show(str);
227         }
228     },
230     _show: function (str) {
231         if (this.last_message != str) {
232             this.output_element.value = str;
233             this.last_message = str;
234         }
235     },
237     message: function (str) {
238         if (str == "")
239             this.clear();
240         else {
241             this.show(str, true /* force */);
242             if (this.active)
243                 this._flash_temporary_message();
244         }
245     },
247     clear: function () {
248         this.current_message = null;
249         if (!this.active)
250             this._show(this.default_message);
251     },
253     set_default_message: function (str) {
254         this.default_message = str;
255         if (this.current_message == null)
256             this._show(str);
257     },
259     get current_state () {
260         if (! this.states[0])
261             return null;
262         return this.states[this.states.length - 1];
263     },
265     push_state: function (state) {
266         this._save_state();
267         this.states.push(state);
268         this._restore_state();
269     },
271     pop_state: function () {
272         this.current_state.destroy();
273         this.states.pop();
274         this._restore_state();
275     },
277     pop_all: function () {
278         var state;
279         while ((state = this.current_state)) {
280             state.destroy();
281             this.states.pop();
282         }
283     },
285     remove_state: function (state) {
286         var i = this.states.indexOf(state);
287         if (i == -1)
288             return;
289         var was_current = (i == (this.states.length - 1));
290         state.destroy();
291         this.states.splice(i, 1);
292         if (was_current)
293             this._restore_state();
294     },
296     _input_mode_enabled: false,
298     active: false,
300     /* If _input_mode_enabled is true, this is set to indicate that
301      * the message area is being temporarily shown instead of the
302      * input box. */
303     _showing_message: false,
305     _message_timer_ID: null,
307     /* This must only be called if _input_mode_enabled is true */
308     //XXX: if it must only be called if _input_mode_enabled is true, then
309     //     why does it have an else condition for handling
310     //     minibuffer_message_state states?
311     _restore_normal_state: function () {
312         if (this._showing_message) {
313             this.window.clearTimeout(this._message_timer_ID);
314             this._message_timer_ID = null;
315             this._showing_message = false;
317             if (this._input_mode_enabled)
318                 this._switch_to_input_mode();
319             else
320                 // assumes that anything other than an input state is a
321                 // minibuffer_message_state.
322                 this._show(this.current_state._message);
323         }
324     },
326     /* This must only be called if _input_mode_enabled is true */
327     _flash_temporary_message: function () {
328         if (this._showing_message)
329             this.window.clearTimeout(this._message_timer_ID);
330         else {
331             this._showing_message = true;
332             if (this._input_mode_enabled)
333                 this._switch_to_message_mode();
334         }
335         var obj = this;
336         this._message_timer_ID = this.window.setTimeout(function () {
337             obj._restore_normal_state();
338         }, minibuffer_input_mode_show_message_timeout);
339     },
341     _switch_to_input_mode: function () {
342         this.element.setAttribute("minibuffermode", "input");
343         this.input_element.inputField.focus();
344     },
346     _switch_to_message_mode: function () {
347         this.element.setAttribute("minibuffermode", "message");
348     },
350     _restore_state: function () {
351         var s = this.current_state;
352         if (s) {
353             this.window.buffers.save_focus();
354             s.load();
355             this.active = true;
356         } else {
357             if (this.active) {
358                 this.active = false;
359                 this.window.buffers.restore_focus();
360                 this._show(this.current_message || this.default_message);
361             }
362         }
363         if (this._showing_message) {
364             this.window.clearTimeout(this._message_timer_ID);
365             this._message_timer_ID = null;
366             this._showing_message = false;
367         }
368         var want_input_mode = s instanceof minibuffer_input_state;
369         var in_input_mode = this._input_mode_enabled && !this._showing_message;
370         if (want_input_mode && !in_input_mode)
371             this._switch_to_input_mode();
372         else if (!want_input_mode && in_input_mode)
373             this._switch_to_message_mode();
374         this._input_mode_enabled = want_input_mode;
375     },
377     _save_state: function () {
378         var s = this.current_state;
379         if (s)
380             s.unload();
381     },
383     insert_before: function (element) {
384         this.element.parentNode.insertBefore(element, this.element);
385     }
389 function minibuffer_initialize_window (window) {
390     window.minibuffer = new minibuffer(window);
392 add_hook("window_initialize_early_hook", minibuffer_initialize_window);
395 function minibuffer_window_close_handler (window) {
396     window.minibuffer.pop_all();
398 add_hook("window_close_hook", minibuffer_window_close_handler);
401 /* Note: This is concise, but doesn't seem to be useful in practice,
402  * because nothing can be done with the state alone. */
403 minibuffer.prototype.check_state = function (type) {
404     var s = this.current_state;
405     if (!(s instanceof type))
406         throw new Error("Invalid minibuffer state.");
407     return s;
410 minibuffer.prototype.show_wait_message = function (initial_message, cleanup_function) {
411     var s = new minibuffer_message_state(this, minibuffer_message_keymap, initial_message, cleanup_function);
412     this.push_state(s);
413     return s;
416 minibuffer.prototype.wait_for = function (message, coroutine) {
417     let promise = spawn(coroutine);
418     var s = this.show_wait_message(message, promise.cancel);
419     let cleanup = s.minibuffer.remove_state.bind(s);
420     promise.then(cleanup, cleanup);
421     return promise;
425 // This should only be used for minibuffer states where it makes
426 // sense.  In particular, it should not be used if additional cleanup
427 // must be done.
428 function minibuffer_abort (window) {
429     var m = window.minibuffer;
430     var s = m.current_state;
431     if (s == null)
432         throw "Invalid minibuffer state";
433     m.pop_state();
435 interactive("minibuffer-abort", null, function (I) { minibuffer_abort(I.window); });
439  * Minibuffer-annotation-mode
440  */
442 var minibuffer_annotation_mode = {
443     stylesheet: "chrome://conkeror-gui/content/minibuffer-annotation.css",
444     users: [],
445     enabled: false,
446     register: function (user) {
447         this.users.push(user);
448         this._switch_if_needed();
449     },
450     unregister: function (user) {
451         var i = this.users.indexOf(user);
452         if (i > -1)
453             this.users.splice(i, 1);
454         this._switch_if_needed();
455     },
456     _switch_if_needed: function (user) {
457         if (this.enabled && this.users.length == 0)
458             this._disable();
459         if (!this.enabled && this.users.length != 0)
460             this._enable();
461     },
462     _enable: function () {
463         register_agent_stylesheet(this.stylesheet);
464         this.enabled = true;
465     },
466     _disable: function () {
467         unregister_agent_stylesheet(this.stylesheet);
468         this.enabled = false;
469     }
472 provide("minibuffer");