whitespace
[conkeror.git] / modules / minibuffer.js
blob5bfa88d178221c36fc1b2b41b14924a2d4a23b98
1 /**
2  * (C) Copyright 2007-2008 Jeremy Maitin-Shepard
3  *
4  * Use, modification, and distribution are subject to the terms specified in the
5  * COPYING file.
6 **/
8 /* This should only be used for minibuffer states where it makes
9  * sense.  In particular, it should not be used if additional cleanup
10  * must be done. */
11 function minibuffer_abort (window) {
12     var m = window.minibuffer;
13     var s = m.current_state;
14     if (s == null)
15         throw "Invalid minibuffer state";
16     m.pop_state();
18 interactive("minibuffer-abort", null, function (I) { minibuffer_abort(I.window); });
20 define_builtin_commands("minibuffer-",
21     function (I, command) {
22         try {
23             var m = I.minibuffer;
24             if (m._input_mode_enabled) {
25                 m._restore_normal_state();
26                 var e = m.input_element;
27                 var c = e.controllers.getControllerForCommand(command);
28                 try {
29                     m.ignore_input_events = true;
30                     if (c && c.isCommandEnabled(command))
31                         c.doCommand(command);
32                 } finally {
33                     m.ignore_input_events = false;
34                 }
35                 var s = m.current_state;
36                 if (s.ran_minibuffer_command)
37                     s.ran_minibuffer_command(m, command);
38             }
39         } catch (e) {
40             /* Ignore exceptions. */
41         }
42     },
43     function (I) { //XXX: need return??
44         I.minibuffer.current_state.mark_active = !I.minibuffer.current_state.mark_active;
45     },
46     function (I) I.minibuffer.current_state.mark_active,
47     false);
49 function minibuffer_state (keymap, use_input_mode) {
50     this.keymap = keymap;
51     this.use_input_mode = use_input_mode;
53 minibuffer_state.prototype.load = function () {};
54 minibuffer_state.prototype.unload = function () {};
55 minibuffer_state.prototype.destroy = function () {};
57 function minibuffer_message_state (keymap, message, destroy_function) {
58     minibuffer_state.call(this, keymap, false);
59     this._message = message;
60     if (destroy_function)
61         this.destroy = destroy_function;
63 minibuffer_message_state.prototype = {
64     __proto__: minibuffer_state.prototype,
65     load : function (window) {
66         this.window = window;
67     },
68     unload : function (window) {
69         this.window = null;
70     },
71     get message () { return this._message; },
72     set message (x) {
73         if (this.window) {
74             this.window.minibuffer._restore_normal_state();
75             this.window.minibuffer._show(this._message);
76         }
77     }
80 function minibuffer_input_state (keymap, prompt, input, selection_start, selection_end) {
81     this.prompt = prompt;
82     if (input)
83         this.input = input;
84     else
85         this.input = "";
86     if (selection_start)
87         this.selection_start = selection_start;
88     else
89         this.selection_start = 0;
90     if (selection_end)
91         this.selection_end = selection_end;
92     else
93         this.selection_end = this.selection_start;
95     minibuffer_state.call(this, keymap, true);
97 minibuffer_input_state.prototype.__proto__ = minibuffer_state.prototype;
101  * The parameter `args' is an object specifying the arguments for
102  * basic_minibuffer_state.  The following properties of args must/may
103  * be set:
105  * prompt:            [required]
107  * initial_value:     [optional] specifies the initial text
109  * select:            [optional] specifies to select the initial text if set to non-null
110  */
111 define_keywords("$prompt", "$initial_value", "$select");
112 function basic_minibuffer_state () {
113     keywords(arguments);
114     var initial_value = arguments.$initial_value || "";
115     var sel_start, sel_end;
116     if (arguments.$select) {
117         sel_start = 0;
118         sel_end = initial_value.length;
119     } else {
120         sel_start = sel_end = initial_value.length;
121     }
122     minibuffer_input_state.call(this, minibuffer_base_keymap,
123                                 arguments.$prompt, initial_value,
124                                 sel_start, sel_end);
126 basic_minibuffer_state.prototype.__proto__ = minibuffer_input_state.prototype; // inherit from minibuffer_state
129 define_variable("minibuffer_input_mode_show_message_timeout", 1000,
130     "Time duration (in milliseconds) to flash minibuffer messages while in "+
131     "minibuffer input mode.");
134 function minibuffer (window) {
135     this.element = window.document.getElementById("minibuffer");
136     this.output_element = window.document.getElementById("minibuffer-message");
137     this.input_prompt_element = window.document.getElementById("minibuffer-prompt");
138     this.input_element = window.document.getElementById("minibuffer-input");
139     var m = this;
140     this.input_element.inputField.addEventListener("blur",
141         function () {
142             if (m.active && m._input_mode_enabled && !m._showing_message) {
143                 window.setTimeout(function () {
144                         m.input_element.inputField.focus();
145                     }, 0);
146             }
147         }, false);
148     this.input_element.addEventListener("input",
149         function (e) {
150             if (m.ignore_input_events || !m._input_mode_enabled)
151                 return;
152             var s = m.current_state;
153             if (s) {
154                 if (s.handle_input)
155                     s.handle_input(m);
156             }
157         }, true);
159     // Ensure that the input area will have focus if a message is
160     // currently being flashed so that the default handler for key
161     // events will properly add text to the input area.
162     window.addEventListener("keydown",
163         function (e) {
164             if (m._input_mode_enabled && m._showing_message)
165                 m._restore_normal_state();
166         }, true);
167     this.window = window;
168     this.last_message = "";
169     this.states = [];
172 minibuffer.prototype = {
173     constructor : minibuffer.constructor,
174     get _selection_start () { return this.input_element.selectionStart; },
175     get _selection_end () { return this.input_element.selectionEnd; },
176     get _input_text () { return this.input_element.value; },
177     set _input_text (text) { this.input_element.value = text; },
178     get prompt () { return this.input_prompt_element.value; },
179     set prompt (s) { this.input_prompt_element.value = s; },
181     set_input_state : function (x) {
182         this._input_text = x[0];
183         this._set_selection(x[1], x[2]);
184     },
186     _set_selection : function (start, end) {
187         if (start == null)
188             start = this._input_text.length;
189         if (end == null)
190             end = this._input_text.length;
191         this.input_element.setSelectionRange(start,end);
192     },
194     /* Saved focus state */
195     saved_focused_frame : null,
196     saved_focused_element : null,
198     default_message : "",
200     current_message : null,
202     /* This method will display the specified string in the
203      * minibuffer, without recording it in any log/Messages buffer. */
204     show : function (str, force) {
205         if (!this.active || force) {
206             this.current_message = str;
207             this._show(str);
208         }
209     },
211     _show : function (str, force) {
212         if (this.last_message != str) {
213             this.output_element.value = str;
214             this.last_message = str;
215         }
216     },
218     message : function (str) {
219         /* TODO: add the message to a *Messages* buffer, and/or
220          * possibly dump them to the console. */
221         this.show(str, true /* force */);
223         if (str.length > 0 && this.active)
224             this._flash_temporary_message();
225     },
227     clear : function () {
228         this.current_message = null;
229         if (!this.active)
230             this._show(this.default_message);
231     },
233     set_default_message : function (str) {
234         this.default_message = str;
235         if (this.current_message == null)
236             this._show(str);
237     },
239     get current_state () {
240         if (this.states.length == 0)
241             return null;
242         return this.states[this.states.length - 1];
243     },
245     push_state : function (state) {
246         this._save_state();
247         this.states.push(state);
248         this._restore_state();
249     },
251     pop_state : function () {
252         this.current_state.destroy();
253         this.states.pop();
254         this._restore_state();
255     },
257     pop_all : function () {
258         while (this.states.length > 0) {
259             this.current_state.destroy();
260             this.states.pop();
261         }
262     },
264     remove_state : function (state) {
265         var i = this.states.indexOf(state);
266         if (i == -1)
267             return;
268         var was_current = (i == (this.states.length - 1));
269         state.destroy();
270         this.states.splice(i, 1);
271         if (was_current)
272             this._restore_state();
273     },
275     _input_mode_enabled : false,
277     active : false,
279     /* If _input_mode_enabled is true, this is set to indicate that
280      * the message area is being temporarily shown instead of the
281      * input box. */
282     _showing_message : false,
284     _message_timer_ID : null,
286     /* This must only be called if _input_mode_enabled is true */
287     _restore_normal_state : function () {
288         if (this._showing_message) {
289             this.window.clearTimeout(this._message_timer_ID);
290             this._message_timer_ID = null;
291             this._showing_message = false;
293             if (this._input_mode_enabled)
294                 this._switch_to_input_mode();
295             else
296                 this._show(this.current_state._message);
297         }
298     },
300     /* This must only be called if _input_mode_enabled is true */
301     _flash_temporary_message : function () {
302         if (this._showing_message)
303             this.window.clearTimeout(this._message_timer_ID);
304         else {
305             this._showing_message = true;
306             if (this._input_mode_enabled)
307                 this._switch_to_message_mode();
308         }
309         var obj = this;
310         this._message_timer_ID = this.window.setTimeout(function () {
311             obj._restore_normal_state();
312         }, minibuffer_input_mode_show_message_timeout);
313     },
315     _switch_to_input_mode : function () {
316         this.element.setAttribute("minibuffermode", "input");
317         this.input_element.inputField.focus();
318     },
320     _switch_to_message_mode : function () {
321         this.element.setAttribute("minibuffermode", "message");
322     },
324     _restore_state : function () {
325         var s = this.current_state;
326         var want_input_mode = false;
327         if (s) {
328             if (!this.active) {
329                 this.saved_focused_frame = this.window.document.commandDispatcher.focusedWindow;
330                 this.saved_focused_element = this.window.document.commandDispatcher.focusedElement;
331             }
332             if (s.use_input_mode) {
333                 want_input_mode = true;
334                 this._input_text = s.input;
335                 this.prompt = s.prompt;
336                 this._set_selection(s.selection_start, s.selection_end);
337             } else {
338                 this._show(s._message);
339             }
340             s.load(this.window);
341             this.window.keyboard.set_override_keymap(s.keymap);
342             this.active = true;
343         } else {
344             if (this.active) {
345                 this.active = false;
346                 this.window.keyboard.set_override_keymap(null);
347                 if (this.saved_focused_element)
348                     set_focus_no_scroll(this.window, this.saved_focused_element);
349                 else if (this.saved_focused_frame)
350                     set_focus_no_scroll(this.window, this.saved_focused_frame);
351                 this.saved_focused_element = null;
352                 this.saved_focused_frame = null;
353                 this._show(this.current_message || this.default_message);
354             }
355         }
356         var in_input_mode = this._input_mode_enabled && !this._showing_message;
357         if (this._showing_message) {
358             this.window.clearTimeout(this._message_timer_ID);
359             this._message_timer_ID = null;
360             this._showing_message = false;
361         }
362         if (want_input_mode && !in_input_mode)
363             this._switch_to_input_mode();
364         else if (!want_input_mode && in_input_mode)
365             this._switch_to_message_mode();
366         this._input_mode_enabled = want_input_mode;
367     },
369     _save_state : function () {
370         var s = this.current_state;
371         if (s) {
372             if (s.use_input_mode) {
373                 s.input = this._input_text;
374                 s.prompt = this.prompt;
375                 s.selection_start = this._selection_start;
376                 s.selection_end = this._selection_end;
377             }
378             s.unload(this.window);
379         }
380     },
382     insert_before : function (element) {
383         this.element.parentNode.insertBefore(element, this.element);
384     }
388 function minibuffer_initialize_window (window) {
389     window.minibuffer = new minibuffer(window);
391 add_hook("window_initialize_early_hook", minibuffer_initialize_window);
394 function minibuffer_window_close_handler (window) {
395     window.minibuffer.pop_all();
397 add_hook("window_close_hook", minibuffer_window_close_handler);
400 /* Note: This is concise, but doesn't seem to be useful in practice,
401  * because nothing can be done with the state alone. */
402 minibuffer.prototype.check_state = function (type) {
403     var s = this.current_state;
404     if (!(s instanceof type))
405         throw new Error("Invalid minibuffer state.");
406     return s;
409 minibuffer.prototype.show_wait_message = function (initial_message, destroy_function) {
410     var s = new minibuffer_message_state(minibuffer_message_keymap, initial_message, destroy_function);
411     this.push_state(s);
412     return s;
415 minibuffer.prototype.wait_for = function minibuffer__wait_for (message, coroutine) {
416     var cc = yield CONTINUATION;
417     var done = false;
418     var s = this.show_wait_message(message, function () { if (!done) cc.throw(abort()); });
419     var result;
420     try {
421         result = yield coroutine;
422     } finally {
423         done = true;
424         this.remove_state(s);
425     }
426     yield co_return(result);