kludge issue 513
[conkeror.git] / modules / minibuffer.js
blobd20e7640fc2cc2b32eb6b69c0fe9f4a8671a1394
1 /**
2 * (C) Copyright 2007-2008 Jeremy Maitin-Shepard
3 * (C) Copyright 2009-2010 John J. Foerch
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.
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.
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);
42 load: function () {
43 minibuffer_state.prototype.load.call(this);
44 this.minibuffer._show(this.message);
46 cleanup_function: null,
47 destroy: function () {
48 if (this.cleanup_function)
49 this.cleanup_function();
50 minibuffer_state.prototype.destroy.call(this);
55 /**
56 * minibuffer_input_state: base class for minibuffer states which use the
57 * input element.
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);
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);
96 destroy: function () {
97 this.minibuffer.window.input.end_recursion();
98 minibuffer_state.prototype.destroy.call(this);
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
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;
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);
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);
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)
167 call_after_timeout(dispatch_handle_input, 0);
169 return newval;
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 toString: function () "#<minibuffer>",
187 get _selection_start () { return this.input_element.selectionStart; },
188 get _selection_end () { return this.input_element.selectionEnd; },
189 get _input_text () { return this.input_element.value; },
190 set _input_text (text) { this.input_element.value = text; },
191 get prompt () { return this.input_prompt_element.value; },
192 set prompt (s) { this.input_prompt_element.value = s; },
194 set_input_state: function (x) {
195 this._input_text = x[0];
196 this._set_selection(x[1], x[2]);
199 _set_selection: function (start, end) {
200 if (start == null)
201 start = this._input_text.length;
202 if (end == null)
203 end = this._input_text.length;
204 this.input_element.setSelectionRange(start,end);
207 /* Saved focus state */
208 saved_focused_frame: null,
209 saved_focused_element: null,
211 default_message: "",
213 current_message: null,
215 /* This method will display the specified string in the
216 * minibuffer, without recording it in any log/Messages buffer. */
217 show: function (str, force) {
218 if (!this.active || force) {
219 this.current_message = str;
220 this._show(str);
224 _show: function (str) {
225 if (this.last_message != str) {
226 this.output_element.value = str;
227 this.last_message = str;
231 message: function (str) {
232 if (str == "")
233 this.clear();
234 else {
235 this.show(str, true /* force */);
236 if (this.active)
237 this._flash_temporary_message();
241 clear: function () {
242 this.current_message = null;
243 if (!this.active)
244 this._show(this.default_message);
247 set_default_message: function (str) {
248 this.default_message = str;
249 if (this.current_message == null)
250 this._show(str);
253 get current_state () {
254 if (! this.states[0])
255 return null;
256 return this.states[this.states.length - 1];
259 push_state: function (state) {
260 this._save_state();
261 this.states.push(state);
262 this._restore_state();
265 pop_state: function () {
266 this.current_state.destroy();
267 this.states.pop();
268 this._restore_state();
271 pop_all: function () {
272 var state;
273 while ((state = this.current_state)) {
274 state.destroy();
275 this.states.pop();
279 //XXX: breaking stack discipline can cause incorrect
280 // input recursion termination
281 remove_state: function (state) {
282 var i = this.states.indexOf(state);
283 if (i == -1)
284 return;
285 var was_current = (i == (this.states.length - 1));
286 state.destroy();
287 this.states.splice(i, 1);
288 if (was_current)
289 this._restore_state();
292 _input_mode_enabled: false,
294 active: false,
296 /* If _input_mode_enabled is true, this is set to indicate that
297 * the message area is being temporarily shown instead of the
298 * input box. */
299 _showing_message: false,
301 _message_timer_ID: null,
303 /* This must only be called if _input_mode_enabled is true */
304 //XXX: if it must only be called if _input_mode_enabled is true, then
305 // why does it have an else condition for handling
306 // minibuffer_message_state states?
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);
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();
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);
337 _switch_to_input_mode: function () {
338 this.element.setAttribute("minibuffermode", "input");
339 this.input_element.inputField.focus();
342 _switch_to_message_mode: function () {
343 this.element.setAttribute("minibuffermode", "message");
346 _restore_state: function () {
347 var s = this.current_state;
348 if (s) {
349 this.window.buffers.save_focus();
350 s.load();
351 this.active = true;
352 } else {
353 if (this.active) {
354 this.active = false;
355 this.window.buffers.restore_focus();
356 this._show(this.current_message || this.default_message);
359 if (this._showing_message) {
360 this.window.clearTimeout(this._message_timer_ID);
361 this._message_timer_ID = null;
362 this._showing_message = false;
364 var want_input_mode = s instanceof minibuffer_input_state;
365 var in_input_mode = this._input_mode_enabled && !this._showing_message;
366 if (want_input_mode && !in_input_mode)
367 this._switch_to_input_mode();
368 else if (!want_input_mode && in_input_mode)
369 this._switch_to_message_mode();
370 this._input_mode_enabled = want_input_mode;
373 _save_state: function () {
374 var s = this.current_state;
375 if (s)
376 s.unload();
379 insert_before: function (element) {
380 this.element.parentNode.insertBefore(element, this.element);
385 function minibuffer_initialize_window (window) {
386 window.minibuffer = new minibuffer(window);
388 add_hook("window_initialize_early_hook", minibuffer_initialize_window);
391 function minibuffer_window_close_handler (window) {
392 window.minibuffer.pop_all();
394 add_hook("window_close_hook", minibuffer_window_close_handler);
397 /* Note: This is concise, but doesn't seem to be useful in practice,
398 * because nothing can be done with the state alone. */
399 minibuffer.prototype.check_state = function (type) {
400 var s = this.current_state;
401 if (!(s instanceof type))
402 throw new Error("Invalid minibuffer state.");
403 return s;
406 minibuffer.prototype.show_wait_message = function (initial_message, cleanup_function) {
407 var s = new minibuffer_message_state(this, minibuffer_message_keymap, initial_message, cleanup_function);
408 this.push_state(s);
409 return s;
412 minibuffer.prototype.wait_for = function (message, coroutine) {
413 let promise = spawn(coroutine);
414 var s = this.show_wait_message(message, promise.cancel);
415 let cleanup = s.minibuffer.remove_state.bind(s);
416 promise.then(cleanup, cleanup);
417 return promise;
421 // This should only be used for minibuffer states where it makes
422 // sense. In particular, it should not be used if additional cleanup
423 // must be done.
424 function minibuffer_abort (window) {
425 var m = window.minibuffer;
426 var s = m.current_state;
427 if (s == null)
428 throw "Invalid minibuffer state";
429 m.pop_state();
430 input_sequence_abort.call(window);
432 interactive("minibuffer-abort", null, function (I) { minibuffer_abort(I.window); });
436 * Minibuffer-annotation-mode
439 var minibuffer_annotation_mode = {
440 stylesheet: "chrome://conkeror-gui/content/minibuffer-annotation.css",
441 users: [],
442 enabled: false,
443 register: function (user) {
444 this.users.push(user);
445 this._switch_if_needed();
447 unregister: function (user) {
448 var i = this.users.indexOf(user);
449 if (i > -1)
450 this.users.splice(i, 1);
451 this._switch_if_needed();
453 _switch_if_needed: function (user) {
454 if (this.enabled && this.users.length == 0)
455 this._disable();
456 if (!this.enabled && this.users.length != 0)
457 this._enable();
459 _enable: function () {
460 register_agent_stylesheet(this.stylesheet);
461 this.enabled = true;
463 _disable: function () {
464 unregister_agent_stylesheet(this.stylesheet);
465 this.enabled = false;
469 provide("minibuffer");