whitespace
[conkeror.git] / modules / minibuffer.js
blobdc6ce5511a4ef05038114988b99b08628443cdb9
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     destroy: function (window) {
112         window.input.end_recursion();
113         minibuffer_state.prototype.destroy.call(this, window);
114     }
119  * The parameter `args' is an object specifying the arguments for
120  * basic_minibuffer_state.  The following properties of args must/may
121  * be set:
123  * prompt:            [required]
125  * initial_value:     [optional] specifies the initial text
127  * select:            [optional] specifies to select the initial text if set to non-null
128  */
129 define_keywords("$keymap", "$prompt", "$initial_value", "$select");
130 function basic_minibuffer_state (window) {
131     keywords(arguments, $keymap = minibuffer_base_keymap);
132     var initial_value = arguments.$initial_value || "";
133     var sel_start, sel_end;
134     if (arguments.$select) {
135         sel_start = 0;
136         sel_end = initial_value.length;
137     } else {
138         sel_start = sel_end = initial_value.length;
139     }
140     minibuffer_input_state.call(this, window, arguments.$keymap,
141                                 arguments.$prompt, initial_value,
142                                 sel_start, sel_end);
144 basic_minibuffer_state.prototype.__proto__ = minibuffer_input_state.prototype;
147 define_variable("minibuffer_input_mode_show_message_timeout", 1000,
148     "Time duration (in milliseconds) to flash minibuffer messages while in "+
149     "minibuffer input mode.");
152 function minibuffer (window) {
153     this.element = window.document.getElementById("minibuffer");
154     this.output_element = window.document.getElementById("minibuffer-message");
155     this.input_prompt_element = window.document.getElementById("minibuffer-prompt");
156     this.input_element = window.document.getElementById("minibuffer-input");
157     var m = this;
158     this.input_element.inputField.addEventListener("blur",
159         function () {
160             if (m.active && m._input_mode_enabled && !m._showing_message) {
161                 window.setTimeout(function () {
162                         m.input_element.inputField.focus();
163                     }, 0);
164             }
165         }, false);
166     this.input_element.addEventListener("input",
167         function (e) {
168             if (m.ignore_input_events || !m._input_mode_enabled)
169                 return;
170             var s = m.current_state;
171             if (s) {
172                 if (s.handle_input)
173                     s.handle_input(m);
174             }
175         }, true);
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 = [];
190 minibuffer.prototype = {
191     constructor: minibuffer.constructor,
192     get _selection_start () { return this.input_element.selectionStart; },
193     get _selection_end () { return this.input_element.selectionEnd; },
194     get _input_text () { return this.input_element.value; },
195     set _input_text (text) { this.input_element.value = text; },
196     get prompt () { return this.input_prompt_element.value; },
197     set prompt (s) { this.input_prompt_element.value = s; },
199     set_input_state: function (x) {
200         this._input_text = x[0];
201         this._set_selection(x[1], x[2]);
202     },
204     _set_selection: function (start, end) {
205         if (start == null)
206             start = this._input_text.length;
207         if (end == null)
208             end = this._input_text.length;
209         this.input_element.setSelectionRange(start,end);
210     },
212     /* Saved focus state */
213     saved_focused_frame: null,
214     saved_focused_element: null,
216     default_message: "",
218     current_message: null,
220     /* This method will display the specified string in the
221      * minibuffer, without recording it in any log/Messages buffer. */
222     show: function (str, force) {
223         if (!this.active || force) {
224             this.current_message = str;
225             this._show(str);
226         }
227     },
229     _show: function (str, force) {
230         if (this.last_message != str) {
231             this.output_element.value = str;
232             this.last_message = str;
233         }
234     },
236     message: function (str) {
237         /* TODO: add the message to a *Messages* buffer, and/or
238          * possibly dump them to the console. */
239         this.show(str, true /* force */);
241         if (str.length > 0 && this.active)
242             this._flash_temporary_message();
243     },
245     clear: function () {
246         this.current_message = null;
247         if (!this.active)
248             this._show(this.default_message);
249     },
251     set_default_message: function (str) {
252         this.default_message = str;
253         if (this.current_message == null)
254             this._show(str);
255     },
257     get current_state () {
258         if (this.states.length == 0)
259             return null;
260         return this.states[this.states.length - 1];
261     },
263     push_state: function (state) {
264         this._save_state();
265         this.states.push(state);
266         this._restore_state();
267     },
269     pop_state: function () {
270         this.current_state.destroy(this.window);
271         this.states.pop();
272         this._restore_state();
273     },
275     pop_all: function () {
276         while (this.states.length > 0) {
277             this.current_state.destroy(this.window);
278             this.states.pop();
279         }
280     },
282     remove_state: function (state) {
283         var i = this.states.indexOf(state);
284         if (i == -1)
285             return;
286         var was_current = (i == (this.states.length - 1));
287         state.destroy(this.window);
288         this.states.splice(i, 1);
289         if (was_current)
290             this._restore_state();
291     },
293     _input_mode_enabled: false,
295     active: false,
297     /* If _input_mode_enabled is true, this is set to indicate that
298      * the message area is being temporarily shown instead of the
299      * input box. */
300     _showing_message: false,
302     _message_timer_ID: null,
304     /* This must only be called if _input_mode_enabled is true */
305     //XXX: if it must only be called if _input_mode_enabled is true,
306     //     then why does it have an else condition?
307     _restore_normal_state: function () {
308         if (this._showing_message) {
309             this.window.clearTimeout(this._message_timer_ID);
310             this._message_timer_ID = null;
311             this._showing_message = false;
313             if (this._input_mode_enabled)
314                 this._switch_to_input_mode();
315             else
316                 // assumes that anything other than an input state is a
317                 // minibuffer_message_state.
318                 this._show(this.current_state._message);
319         }
320     },
322     /* This must only be called if _input_mode_enabled is true */
323     _flash_temporary_message: function () {
324         if (this._showing_message)
325             this.window.clearTimeout(this._message_timer_ID);
326         else {
327             this._showing_message = true;
328             if (this._input_mode_enabled)
329                 this._switch_to_message_mode();
330         }
331         var obj = this;
332         this._message_timer_ID = this.window.setTimeout(function () {
333             obj._restore_normal_state();
334         }, minibuffer_input_mode_show_message_timeout);
335     },
337     _switch_to_input_mode: function () {
338         this.element.setAttribute("minibuffermode", "input");
339         this.input_element.inputField.focus();
340     },
342     _switch_to_message_mode: function () {
343         this.element.setAttribute("minibuffermode", "message");
344     },
346     _restore_state: function () {
347         var s = this.current_state;
348         var want_input_mode = false;
349         if (s) {
350             if (!this.active) {
351                 this.saved_focused_frame = this.window.document.commandDispatcher.focusedWindow;
352                 this.saved_focused_element = this.window.document.commandDispatcher.focusedElement;
353             }
354             if (s.use_input_mode) {
355                 want_input_mode = true;
356                 this._input_text = s.input;
357                 this.prompt = s.prompt;
358                 this._set_selection(s.selection_start, s.selection_end);
359             } else {
360                 this._show(s._message);
361             }
362             s.load(this.window);
363             this.window.input.current.override_keymap = s.keymap;
364             this.active = true;
365         } else {
366             if (this.active) {
367                 this.active = false;
368                 this.window.input.current.override_keymap = null;
369                 if (this.saved_focused_element)
370                     set_focus_no_scroll(this.window, this.saved_focused_element);
371                 else if (this.saved_focused_frame)
372                     set_focus_no_scroll(this.window, this.saved_focused_frame);
373                 this.saved_focused_element = null;
374                 this.saved_focused_frame = null;
375                 this._show(this.current_message || this.default_message);
376             }
377         }
378         var in_input_mode = this._input_mode_enabled && !this._showing_message;
379         if (this._showing_message) {
380             this.window.clearTimeout(this._message_timer_ID);
381             this._message_timer_ID = null;
382             this._showing_message = false;
383         }
384         if (want_input_mode && !in_input_mode)
385             this._switch_to_input_mode();
386         else if (!want_input_mode && in_input_mode)
387             this._switch_to_message_mode();
388         this._input_mode_enabled = want_input_mode;
389     },
391     _save_state: function () {
392         var s = this.current_state;
393         if (s) {
394             if (s.use_input_mode) {
395                 s.input = this._input_text;
396                 s.prompt = this.prompt;
397                 s.selection_start = this._selection_start;
398                 s.selection_end = this._selection_end;
399             }
400             s.unload(this.window);
401         }
402     },
404     insert_before: function (element) {
405         this.element.parentNode.insertBefore(element, this.element);
406     }
410 function minibuffer_initialize_window (window) {
411     window.minibuffer = new minibuffer(window);
413 add_hook("window_initialize_early_hook", minibuffer_initialize_window);
416 function minibuffer_window_close_handler (window) {
417     window.minibuffer.pop_all();
419 add_hook("window_close_hook", minibuffer_window_close_handler);
422 /* Note: This is concise, but doesn't seem to be useful in practice,
423  * because nothing can be done with the state alone. */
424 minibuffer.prototype.check_state = function (type) {
425     var s = this.current_state;
426     if (!(s instanceof type))
427         throw new Error("Invalid minibuffer state.");
428     return s;
431 minibuffer.prototype.show_wait_message = function (initial_message, destroy_function) {
432     var s = new minibuffer_message_state(minibuffer_message_keymap, initial_message, destroy_function);
433     this.push_state(s);
434     return s;
437 minibuffer.prototype.wait_for = function (message, coroutine) {
438     var cc = yield CONTINUATION;
439     var done = false;
440     var s = this.show_wait_message(message, function () { if (!done) cc.throw(abort()); });
441     var result;
442     try {
443         result = yield coroutine;
444     } finally {
445         done = true;
446         this.remove_state(s);
447     }
448     yield co_return(result);