Prepare new Debian package
[conkeror.git] / modules / minibuffer.js
blobdb05c4102117f65524041c1204fecf502ca8df5e
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 // This should only be used for minibuffer states where it makes
10 // sense.  In particular, it should not be used if additional cleanup
11 // must be done.
12 function minibuffer_abort (window) {
13     var m = window.minibuffer;
14     var s = m.current_state;
15     if (s == null)
16         throw "Invalid minibuffer state";
17     m.pop_state();
18     input_sequence_abort.call(window);
20 interactive("minibuffer-abort", null, function (I) { minibuffer_abort(I.window); });
22 define_builtin_commands("minibuffer-",
23     function (I, command) {
24         try {
25             var m = I.minibuffer;
26             if (m._input_mode_enabled) {
27                 m._restore_normal_state();
28                 var e = m.input_element;
29                 var c = e.controllers.getControllerForCommand(command);
30                 try {
31                     m.ignore_input_events = true;
32                     if (c && c.isCommandEnabled(command))
33                         c.doCommand(command);
34                 } finally {
35                     m.ignore_input_events = false;
36                 }
37                 var s = m.current_state;
38                 if (s.ran_minibuffer_command)
39                     s.ran_minibuffer_command(m, command);
40             }
41         } catch (e) {
42             /* Ignore exceptions. */
43         }
44     },
45     function (I) { //XXX: need return??
46         I.minibuffer.current_state.mark_active = !I.minibuffer.current_state.mark_active;
47     },
48     function (I) I.minibuffer.current_state.mark_active,
49     false);
52 /**
53  * minibuffer_state: abstact base class for minibuffer states.
54  */
55 function minibuffer_state (keymap, use_input_mode) {
56     this.keymap = keymap;
57     this.use_input_mode = use_input_mode;
59 minibuffer_state.prototype = {
60     load: function () {},
61     unload: function () {},
62     destroy: function () {}
66 function minibuffer_message_state (keymap, message, destroy_function) {
67     minibuffer_state.call(this, keymap, false);
68     this._message = message;
69     if (destroy_function)
70         this.destroy = destroy_function;
72 minibuffer_message_state.prototype = {
73     __proto__: minibuffer_state.prototype,
74     load: function (window) {
75         minibuffer_state.prototype.load.call(this, window);
76         this.window = window;
77     },
78     unload: function (window) {
79         this.window = null;
80         minibuffer_state.prototype.unload.call(this, window);
81     },
82     get message () { return this._message; },
83     set message (x) {
84         if (this.window) {
85             this.window.minibuffer._restore_normal_state();
86             this.window.minibuffer._show(this._message);
87         }
88     }
92 function minibuffer_input_state (window, keymap, prompt, input, selection_start, selection_end) {
93     minibuffer_state.call(this, keymap, true);
94     this.prompt = prompt;
95     if (input)
96         this.input = input;
97     else
98         this.input = "";
99     if (selection_start)
100         this.selection_start = selection_start;
101     else
102         this.selection_start = 0;
103     if (selection_end)
104         this.selection_end = selection_end;
105     else
106         this.selection_end = this.selection_start;
107     window.input.begin_recursion();
109 minibuffer_input_state.prototype = {
110     __proto__: minibuffer_state.prototype,
111     mark_active : false,
112     destroy: function (window) {
113         window.input.end_recursion();
114         minibuffer_state.prototype.destroy.call(this, window);
115     }
120  * The parameter `args' is an object specifying the arguments for
121  * basic_minibuffer_state.  The following properties of args must/may
122  * be set:
124  * prompt:            [required]
126  * initial_value:     [optional] specifies the initial text
128  * select:            [optional] specifies to select the initial text if set to non-null
129  */
130 define_keywords("$keymap", "$prompt", "$initial_value", "$select");
131 function basic_minibuffer_state (window) {
132     keywords(arguments, $keymap = minibuffer_base_keymap);
133     var initial_value = arguments.$initial_value || "";
134     var sel_start, sel_end;
135     if (arguments.$select) {
136         sel_start = 0;
137         sel_end = initial_value.length;
138     } else {
139         sel_start = sel_end = initial_value.length;
140     }
141     minibuffer_input_state.call(this, window, arguments.$keymap,
142                                 arguments.$prompt, initial_value,
143                                 sel_start, sel_end);
145 basic_minibuffer_state.prototype.__proto__ = minibuffer_input_state.prototype;
148 define_variable("minibuffer_input_mode_show_message_timeout", 1000,
149     "Time duration (in milliseconds) to flash minibuffer messages while in "+
150     "minibuffer input mode.");
153 function minibuffer (window) {
154     this.element = window.document.getElementById("minibuffer");
155     this.output_element = window.document.getElementById("minibuffer-message");
156     this.input_prompt_element = window.document.getElementById("minibuffer-prompt");
157     this.input_element = window.document.getElementById("minibuffer-input");
158     var m = this;
159     this.input_element.inputField.addEventListener("blur",
160         function () {
161             if (m.active && m._input_mode_enabled && !m._showing_message) {
162                 window.setTimeout(function () {
163                         m.input_element.inputField.focus();
164                     }, 0);
165             }
166         }, false);
167     this.input_element.addEventListener("input",
168         function (e) {
169             if (m.ignore_input_events || !m._input_mode_enabled)
170                 return;
171             var s = m.current_state;
172             if (s) {
173                 if (s.handle_input)
174                     s.handle_input(m);
175             }
176         }, true);
178     // Ensure that the input area will have focus if a message is
179     // currently being flashed so that the default handler for key
180     // events will properly add text to the input area.
181     window.addEventListener("keydown",
182         function (e) {
183             if (m._input_mode_enabled && m._showing_message)
184                 m._restore_normal_state();
185         }, true);
186     this.window = window;
187     this.last_message = "";
188     this.states = [];
191 minibuffer.prototype = {
192     constructor: minibuffer.constructor,
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, force) {
231         if (this.last_message != str) {
232             this.output_element.value = str;
233             this.last_message = str;
234         }
235     },
237     message: function (str) {
238         /* TODO: add the message to a *Messages* buffer, and/or
239          * possibly dump them to the console. */
240         this.show(str, true /* force */);
242         if (str.length > 0 && this.active)
243             this._flash_temporary_message();
244     },
246     clear: function () {
247         this.current_message = null;
248         if (!this.active)
249             this._show(this.default_message);
250     },
252     set_default_message: function (str) {
253         this.default_message = str;
254         if (this.current_message == null)
255             this._show(str);
256     },
258     get current_state () {
259         if (this.states.length == 0)
260             return null;
261         return this.states[this.states.length - 1];
262     },
264     push_state: function (state) {
265         this._save_state();
266         this.states.push(state);
267         this._restore_state();
268     },
270     pop_state: function () {
271         this.current_state.destroy(this.window);
272         this.states.pop();
273         this._restore_state();
274     },
276     pop_all: function () {
277         while (this.states.length > 0) {
278             this.current_state.destroy(this.window);
279             this.states.pop();
280         }
281     },
283     remove_state: function (state) {
284         var i = this.states.indexOf(state);
285         if (i == -1)
286             return;
287         var was_current = (i == (this.states.length - 1));
288         state.destroy(this.window);
289         this.states.splice(i, 1);
290         if (was_current)
291             this._restore_state();
292     },
294     _input_mode_enabled: false,
296     active: false,
298     /* If _input_mode_enabled is true, this is set to indicate that
299      * the message area is being temporarily shown instead of the
300      * input box. */
301     _showing_message: false,
303     _message_timer_ID: null,
305     /* This must only be called if _input_mode_enabled is true */
306     //XXX: if it must only be called if _input_mode_enabled is true,
307     //     then why does it have an else condition?
308     _restore_normal_state: function () {
309         if (this._showing_message) {
310             this.window.clearTimeout(this._message_timer_ID);
311             this._message_timer_ID = null;
312             this._showing_message = false;
314             if (this._input_mode_enabled)
315                 this._switch_to_input_mode();
316             else
317                 // assumes that anything other than an input state is a
318                 // minibuffer_message_state.
319                 this._show(this.current_state._message);
320         }
321     },
323     /* This must only be called if _input_mode_enabled is true */
324     _flash_temporary_message: function () {
325         if (this._showing_message)
326             this.window.clearTimeout(this._message_timer_ID);
327         else {
328             this._showing_message = true;
329             if (this._input_mode_enabled)
330                 this._switch_to_message_mode();
331         }
332         var obj = this;
333         this._message_timer_ID = this.window.setTimeout(function () {
334             obj._restore_normal_state();
335         }, minibuffer_input_mode_show_message_timeout);
336     },
338     _switch_to_input_mode: function () {
339         this.element.setAttribute("minibuffermode", "input");
340         this.input_element.inputField.focus();
341     },
343     _switch_to_message_mode: function () {
344         this.element.setAttribute("minibuffermode", "message");
345     },
347     _restore_state: function () {
348         var s = this.current_state;
349         var want_input_mode = false;
350         if (s) {
351             if (!this.active) {
352                 this.saved_focused_frame = this.window.document.commandDispatcher.focusedWindow;
353                 this.saved_focused_element = this.window.document.commandDispatcher.focusedElement;
354             }
355             if (s.use_input_mode) {
356                 want_input_mode = true;
357                 this._input_text = s.input;
358                 this.prompt = s.prompt;
359                 this._set_selection(s.selection_start, s.selection_end);
360             } else {
361                 this._show(s._message);
362             }
363             s.load(this.window);
364             this.window.input.current.override_keymap = s.keymap;
365             this.active = true;
366         } else {
367             if (this.active) {
368                 this.active = false;
369                 this.window.input.current.override_keymap = null;
370                 if (this.saved_focused_element)
371                     set_focus_no_scroll(this.window, this.saved_focused_element);
372                 else if (this.saved_focused_frame)
373                     set_focus_no_scroll(this.window, this.saved_focused_frame);
374                 this.saved_focused_element = null;
375                 this.saved_focused_frame = null;
376                 this._show(this.current_message || this.default_message);
377             }
378         }
379         var in_input_mode = this._input_mode_enabled && !this._showing_message;
380         if (this._showing_message) {
381             this.window.clearTimeout(this._message_timer_ID);
382             this._message_timer_ID = null;
383             this._showing_message = false;
384         }
385         if (want_input_mode && !in_input_mode)
386             this._switch_to_input_mode();
387         else if (!want_input_mode && in_input_mode)
388             this._switch_to_message_mode();
389         this._input_mode_enabled = want_input_mode;
390     },
392     _save_state: function () {
393         var s = this.current_state;
394         if (s) {
395             if (s.use_input_mode) {
396                 s.input = this._input_text;
397                 s.prompt = this.prompt;
398                 s.selection_start = this._selection_start;
399                 s.selection_end = this._selection_end;
400             }
401             s.unload(this.window);
402         }
403     },
405     insert_before: function (element) {
406         this.element.parentNode.insertBefore(element, this.element);
407     }
411 function minibuffer_initialize_window (window) {
412     window.minibuffer = new minibuffer(window);
414 add_hook("window_initialize_early_hook", minibuffer_initialize_window);
417 function minibuffer_window_close_handler (window) {
418     window.minibuffer.pop_all();
420 add_hook("window_close_hook", minibuffer_window_close_handler);
423 /* Note: This is concise, but doesn't seem to be useful in practice,
424  * because nothing can be done with the state alone. */
425 minibuffer.prototype.check_state = function (type) {
426     var s = this.current_state;
427     if (!(s instanceof type))
428         throw new Error("Invalid minibuffer state.");
429     return s;
432 minibuffer.prototype.show_wait_message = function (initial_message, destroy_function) {
433     var s = new minibuffer_message_state(minibuffer_message_keymap, initial_message, destroy_function);
434     this.push_state(s);
435     return s;
438 minibuffer.prototype.wait_for = function (message, coroutine) {
439     var cc = yield CONTINUATION;
440     var done = false;
441     var s = this.show_wait_message(message, function () { if (!done) cc.throw(abort()); });
442     var result;
443     try {
444         result = yield coroutine;
445     } finally {
446         done = true;
447         this.remove_state(s);
448     }
449     yield co_return(result);