whitespace
[conkeror.git] / modules / buffer.js
blob8e2562ebac4109d4272acb76f53a7f2f98a10e18
1 /**
2  * (C) Copyright 2004-2007 Shawn Betts
3  * (C) Copyright 2007-2010 John J. Foerch
4  * (C) Copyright 2007-2008 Jeremy Maitin-Shepard
5  *
6  * Use, modification, and distribution are subject to the terms specified in the
7  * COPYING file.
8 **/
10 in_module(null);
12 var define_buffer_local_hook = local_hook_definer("window");
14 function define_current_buffer_hook (hook_name, existing_hook) {
15     define_buffer_local_hook(hook_name);
16     add_hook(existing_hook, function (buffer) {
17             if (!buffer.window.buffers || buffer != buffer.window.buffers.current)
18                 return;
19             var hook = conkeror[hook_name];
20             hook.run.apply(hook, Array.prototype.slice.call(arguments));
21         });
24 define_buffer_local_hook("buffer_title_change_hook");
25 define_buffer_local_hook("buffer_description_change_hook");
26 define_buffer_local_hook("buffer_icon_change_hook");
27 define_buffer_local_hook("select_buffer_hook");
28 define_buffer_local_hook("create_buffer_early_hook");
29 define_buffer_local_hook("create_buffer_late_hook");
30 define_buffer_local_hook("create_buffer_hook");
31 define_buffer_local_hook("kill_buffer_hook");
32 define_buffer_local_hook("buffer_scroll_hook");
33 define_buffer_local_hook("buffer_dom_content_loaded_hook");
34 define_buffer_local_hook("buffer_loaded_hook");
35 define_buffer_local_hook("set_input_mode_hook");
37 define_current_buffer_hook("current_buffer_title_change_hook", "buffer_title_change_hook");
38 define_current_buffer_hook("current_buffer_description_change_hook", "buffer_description_change_hook");
39 define_current_buffer_hook("current_buffer_icon_change_hook", "buffer_icon_change_hook");
40 define_current_buffer_hook("current_buffer_scroll_hook", "buffer_scroll_hook");
41 define_current_buffer_hook("current_buffer_dom_content_loaded_hook", "buffer_dom_content_loaded_hook");
44 define_keywords("$opener");
45 function buffer_creator (type) {
46     var args = forward_keywords(arguments);
47     return function (window) {
48         return new type(window, args);
49     };
52 define_variable("allow_browser_window_close", true,
53     "If this is set to true, if a content buffer page calls " +
54     "window.close() from JavaScript and is not prevented by the " +
55     "normal Mozilla mechanism that restricts pages from closing " +
56     "a window that was not opened by a script, the buffer will be " +
57     "killed, deleting the window as well if it is the only buffer.");
59 function buffer_modality (buffer) {
60     buffer.keymaps.push(default_global_keymap);
63 function buffer (window) {
64     this.constructor_begin();
65     keywords(arguments);
66     this.opener = arguments.$opener;
67     this.window = window;
68     var element = create_XUL(window, "vbox");
69     element.setAttribute("flex", "1");
70     var browser = create_XUL(window, "browser");
71     browser.setAttribute("type", "content");
72     browser.setAttribute("flex", "1");
73     browser.setAttribute("autocompletepopup", "popup_autocomplete");
74     element.appendChild(browser);
75     this.window.buffers.container.appendChild(element);
76     this.window.buffers.buffer_list.push(this);
77     this.element = element;
78     this.browser = element.firstChild;
79     this.element.conkeror_buffer_object = this;
81     this.local = { __proto__: conkeror };
82     this.page = null;
83     this.enabled_modes = [];
84     this.default_browser_object_classes = {};
86     var buffer = this;
88     this.browser.addEventListener("scroll", function (event) {
89             buffer_scroll_hook.run(buffer);
90         }, true /* capture */);
92     this.browser.addEventListener("DOMContentLoaded", function (event) {
93             buffer_dom_content_loaded_hook.run(buffer);
94         }, true /* capture */);
96     this.window.setTimeout(function () { create_buffer_late_hook.run(buffer); }, 0);
98     this.browser.addEventListener("load", function (event) {
99             buffer_loaded_hook.run(buffer);
100         }, true /* capture */);
102     this.browser.addEventListener("DOMWindowClose", function (event) {
103             /* This call to preventDefault is very important; without
104              * it, somehow Mozilla does something bad and as a result
105              * the window loses focus, causing keyboard commands to
106              * stop working. */
107             event.preventDefault();
109             if (allow_browser_window_close)
110                 kill_buffer(buffer, true);
111         }, true);
113     this.browser.addEventListener("focus", function (event) {
114         if (buffer.focusblocker &&
115             event.target instanceof Ci.nsIDOMHTMLElement &&
116             buffer.focusblocker(buffer, event))
117         {
118             event.target.blur();
119         } else
120             buffer.set_input_mode();
121     }, true);
123     this.browser.addEventListener("blur", function (event) {
124         buffer.set_input_mode();
125     }, true);
127     this.modalities = [buffer_modality];
129     // When create_buffer_early_hook runs, basic buffer properties
130     // will be available, but not the properties subclasses.
131     create_buffer_early_hook.run(this);
133     this.constructor_end();
135 buffer.prototype = {
136     constructor: buffer,
138     /* Saved focus state */
139     saved_focused_frame: null,
140     saved_focused_element: null,
142     // get title ()   [must be defined by subclasses]
143     // get name ()    [must be defined by subclasses]
144     dead: false, /* This is set when the buffer is killed */
146     keymaps: null,
147     mark_active: false,
149     // The property focusblocker is available for an external module to
150     // put a function on which takes a buffer as its argument and returns
151     // true to block a focus event, or false to let normal processing
152     // occur.  Having this one property explicitly handled by the buffer
153     // class allows for otherwise modular focus-blockers.
154     focusblocker: null,
156     // icon is a string url of an icon to use for this buffer.  Setting it
157     // causes buffer_icon_change_hook to be run.
158     _icon: null,
159     get icon () this._icon,
160     set icon (x) {
161         if (this._icon != x) {
162             this._icon = x;
163             buffer_icon_change_hook.run(this);
164         }
165     },
167     default_message: "",
169     set_default_message: function (str) {
170         this.default_message = str;
171         if (this == this.window.buffers.current)
172             this.window.minibuffer.set_default_message(str);
173     },
175     constructors_running: 0,
177     constructor_begin: function () {
178         this.constructors_running++;
179     },
181     constructor_end: function () {
182         if (--this.constructors_running == 0) {
183             create_buffer_hook.run(this);
184             this.set_input_mode();
185             delete this.opener;
186         }
187     },
189     destroy: function () {
190         this.dead = true;
191         this.browser = null;
192         this.element = null;
193         this.saved_focused_frame = null;
194         this.saved_focused_element = null;
195         // prevent modalities from accessing dead browser
196         this.modalities = [];
197     },
199     set_input_mode: function () {
200         if (this != this.window.buffers.current)
201             return;
202         this.keymaps = [];
203         this.modalities.map(function (m) m(this), this);
204         set_input_mode_hook.run(this);
205     },
207     override_keymaps: function (keymaps) {
208         if (keymaps) {
209             this.keymaps = keymaps;
210             this.set_input_mode = function () {
211                 set_input_mode_hook.run(this);
212             };
213         } else
214             delete this.set_input_mode;
215         this.set_input_mode();
216     },
218     /* Browser accessors */
219     get top_frame () { return this.browser.contentWindow; },
220     get document () { return this.browser.contentDocument; },
221     get web_navigation () { return this.browser.webNavigation; },
222     get doc_shell () { return this.browser.docShell; },
223     get markup_document_viewer () { return this.browser.markupDocumentViewer; },
224     get current_uri () { return this.browser.currentURI; },
226     is_child_element: function (element) {
227         return (element && this.is_child_frame(element.ownerDocument.defaultView));
228     },
230     is_child_frame: function (frame) {
231         return (frame && frame.top == this.top_frame);
232     },
234     // This method is like focused_frame, except that if no content
235     // frame actually has focus, this returns null.
236     get focused_frame_or_null () {
237         var frame = this.window.document.commandDispatcher.focusedWindow;
238         if (this.is_child_frame(frame))
239             return frame;
240         return null;
241     },
243     get focused_frame () {
244         var frame = this.window.document.commandDispatcher.focusedWindow;
245         if (this.is_child_frame(frame))
246             return frame;
247         return this.top_frame;
248     },
250     get focused_element () {
251         var element = this.window.document.commandDispatcher.focusedElement;
252         if (this.is_child_element(element))
253             return element;
254         return null;
255     },
257     get focused_selection_controller () {
258         return this.focused_frame
259             .QueryInterface(Ci.nsIInterfaceRequestor)
260             .getInterface(Ci.nsIWebNavigation)
261             .QueryInterface(Ci.nsIInterfaceRequestor)
262             .getInterface(Ci.nsISelectionDisplay)
263             .QueryInterface(Ci.nsISelectionController);
264     },
266     do_command: function (command) {
267         function attempt_command (element, command) {
268             var controller;
269             if (element.controllers
270                 && (controller = element.controllers.getControllerForCommand(command)) != null
271                 && controller.isCommandEnabled(command))
272             {
273                 controller.doCommand(command);
274                 return true;
275             }
276             return false;
277         }
279         var element = this.focused_element;
280         if (element && attempt_command(element, command))
281             return;
282         var win = this.focused_frame;
283         while (true) {
284             if (attempt_command(win, command))
285                 return;
286             if (!win.parent || win == win.parent)
287                 break;
288             win = win.parent;
289         }
290     }
293 function with_current_buffer (buffer, callback) {
294     return callback(new interactive_context(buffer));
297 function check_buffer (obj, type) {
298     if (!(obj instanceof type))
299         throw interactive_error("Buffer has invalid type.");
300     if (obj.dead)
301         throw interactive_error("Buffer has already been killed.");
302     return obj;
305 function caret_enabled (buffer) {
306     return buffer.browser.getAttribute('showcaret');
309 function clear_selection (buffer) {
310     let sel_ctrl = buffer.focused_selection_controller;
311     if (sel_ctrl) {
312         let sel = sel_ctrl.getSelection(sel_ctrl.SELECTION_NORMAL);
313         if (caret_enabled(buffer)) {
314             if (sel.anchorNode)
315                 sel.collapseToStart();
316         } else {
317             sel.removeAllRanges();
318         }
319     }
323 function buffer_container (window, create_initial_buffer) {
324     this.window = window;
325     this.container = window.document.getElementById("buffer-container");
326     this.buffer_list = [];
327     window.buffers = this;
328     create_initial_buffer(window);
330 buffer_container.prototype = {
331     constructor: buffer_container,
333     get current () {
334         return this.container.selectedPanel.conkeror_buffer_object;
335     },
337     set current (buffer) {
338         var old_value = this.current;
339         if (old_value == buffer)
340             return;
342         this.buffer_list.splice(this.buffer_list.indexOf(buffer), 1);
343         this.buffer_list.unshift(buffer);
345         this._switch_away_from(this.current);
346         this._switch_to(buffer);
348         // Run hooks
349         select_buffer_hook.run(buffer);
350     },
352     _switch_away_from: function (old_value) {
353         // Save focus state
354         old_value.saved_focused_frame = old_value.focused_frame;
355         old_value.saved_focused_element = old_value.focused_element;
357         old_value.browser.setAttribute("type", "content");
358     },
360     _switch_to: function (buffer) {
361         // Select new buffer in the XUL deck
362         this.container.selectedPanel = buffer.element;
364         buffer.browser.setAttribute("type", "content-primary");
366         /**
367          * This next focus call seems to be needed to avoid focus
368          * somehow getting lost (and the keypress handler therefore
369          * not getting called at all) when killing buffers.
370          */
371         this.window.focus();
373         // Restore focus state
374         buffer.browser.focus();
375         if (buffer.saved_focused_element)
376             set_focus_no_scroll(this.window, buffer.saved_focused_element);
377         else if (buffer.saved_focused_frame)
378             set_focus_no_scroll(this.window, buffer.saved_focused_frame);
380         buffer.saved_focused_element = null;
381         buffer.saved_focused_frame = null;
383         buffer.set_input_mode();
385         this.window.minibuffer.set_default_message(buffer.default_message);
386     },
388     get count () {
389         return this.container.childNodes.length;
390     },
392     get_buffer: function (index) {
393         if (index >= 0 && index < this.count)
394             return this.container.childNodes.item(index).conkeror_buffer_object;
395         return null;
396     },
398     get selected_index () {
399         var nodes = this.container.childNodes;
400         var count = nodes.length;
401         for (var i = 0; i < count; ++i)
402             if (nodes.item(i) == this.container.selectedPanel)
403                 return i;
404         return null;
405     },
407     index_of: function (b) {
408         var nodes = this.container.childNodes;
409         var count = nodes.length;
410         for (var i = 0; i < count; ++i)
411             if (nodes.item(i) == b.element)
412                 return i;
413         return null;
414     },
416     get unique_name_list () {
417         var existing_names = new string_hashset();
418         var bufs = [];
419         this.for_each(function(b) {
420                 var base_name = b.name;
421                 var name = base_name;
422                 var index = 1;
423                 while (existing_names.contains(name)) {
424                     ++index;
425                     name = base_name + "<" + index + ">";
426                 }
427                 existing_names.add(name);
428                 bufs.push([name, b]);
429             });
430         return bufs;
431     },
433     kill_buffer: function (b) {
434         if (b.dead)
435             return true;
436         var count = this.count;
437         if (count <= 1)
438             return false;
439         var new_buffer = this.buffer_list[0];
440         var changed = false;
441         if (b == new_buffer) {
442             new_buffer = this.buffer_list[1];
443             changed = true;
444         }
445         this._switch_away_from(this.current);
446         // The removeChild call below may trigger events in progress
447         // listeners.  This call to `destroy' gives buffer subclasses a
448         // chance to remove such listeners, so that they cannot try to
449         // perform UI actions based upon a xul:browser that no longer
450         // exists.
451         var element = b.element;
452         b.destroy();
453         this.container.removeChild(element);
454         this.buffer_list.splice(this.buffer_list.indexOf(b), 1);
455         this._switch_to(new_buffer);
456         if (changed) {
457             select_buffer_hook.run(new_buffer);
458             this.buffer_list.splice(this.buffer_list.indexOf(new_buffer), 1);
459             this.buffer_list.unshift(new_buffer);
460         }
461         kill_buffer_hook.run(b);
462         return true;
463     },
465     bury_buffer: function (b) {
466         var new_buffer = this.buffer_list[0];
467         if (b == new_buffer)
468             new_buffer = this.buffer_list[1];
469         if (! new_buffer)
470             throw interactive_error("No other buffer");
471         this.buffer_list.splice(this.buffer_list.indexOf(b), 1);
472         this.buffer_list.push(b);
473         this.current = new_buffer;
474         return true;
475     },
477     for_each: function (f) {
478         var count = this.count;
479         for (var i = 0; i < count; ++i)
480             f(this.get_buffer(i));
481     }
484 function buffer_initialize_window_early (window) {
485     /**
486      * Use content_buffer by default to handle an unusual case where
487      * browser.chromeURI is used perhaps.  In general this default
488      * should not be needed.
489      */
490     var create_initial_buffer =
491         window.args.initial_buffer_creator || buffer_creator(content_buffer);
492     new buffer_container(window, create_initial_buffer);
495 add_hook("window_initialize_early_hook", buffer_initialize_window_early);
499  * initialize_first_buffer_type is a workaround for a XULRunner bug that
500  * first appeared in version 2.0, manifested as missing scrollbars in the
501  * first buffer of any window.  It only affects content-primary browsers,
502  * and the workaround is to initialize the browser as type "content" then
503  * change it to content-primary after a delay.
504  */
505 function initialize_first_buffer_type (window) {
506     window.buffers.current.browser.setAttribute("type", "content-primary");
509 add_hook("window_initialize_late_hook", initialize_first_buffer_type);
512 define_buffer_local_hook("buffer_kill_before_hook", RUN_HOOK_UNTIL_FAILURE);
513 function buffer_before_window_close (window) {
514     var bs = window.buffers;
515     var count = bs.count;
516     for (let i = 0; i < count; ++i) {
517         if (!buffer_kill_before_hook.run(bs.get_buffer(i)))
518             return false;
519     }
520     return true;
522 add_hook("window_before_close_hook", buffer_before_window_close);
524 function buffer_window_close_handler (window) {
525     var bs = window.buffers;
526     var count = bs.count;
527     for (let i = 0; i < count; ++i) {
528         let b = bs.get_buffer(i);
529         b.destroy();
530     }
532 add_hook("window_close_hook", buffer_window_close_handler);
534 /* open/follow targets */
535 const OPEN_CURRENT_BUFFER = 0; // only valid for open if the current
536                                // buffer is a content_buffer.
537 const OPEN_NEW_BUFFER = 1;
538 const OPEN_NEW_BUFFER_BACKGROUND = 2;
539 const OPEN_NEW_WINDOW = 3;
541 const FOLLOW_DEFAULT = 4; // for open, implies OPEN_CURRENT_BUFFER
542 const FOLLOW_CURRENT_FRAME = 5; // for open, implies OPEN_CURRENT_BUFFER
544 var TARGET_PROMPTS = [" in current buffer",
545                       " in new buffer",
546                       " in new buffer (background)",
547                       " in new window",
548                       "",
549                       " in current frame"];
551 var TARGET_NAMES = ["current buffer",
552                     "new buffer",
553                     "new buffer (background)",
554                     "new window",
555                     "default",
556                     "current frame"];
559 function create_buffer (window, creator, target) {
560     switch (target) {
561     case OPEN_NEW_BUFFER:
562         window.buffers.current = creator(window, null);
563         break;
564     case OPEN_NEW_BUFFER_BACKGROUND:
565         creator(window, null);
566         break;
567     case OPEN_NEW_WINDOW:
568         make_window(creator);
569         break;
570     default:
571         throw new Error("invalid target");
572     }
575 let (queued_buffer_creators = null) {
576     function create_buffer_in_current_window (creator, target, focus_existing) {
577         function process_queued_buffer_creators (window) {
578             for (var i = 0; i < queued_buffer_creators.length; ++i) {
579                 var x = queued_buffer_creators[i];
580                 create_buffer(window, x[0], x[1]);
581             }
582             queued_buffer_creators = null;
583         }
585         if (target == OPEN_NEW_WINDOW)
586             throw new Error("invalid target");
587         var window = get_recent_conkeror_window();
588         if (window) {
589             if (focus_existing)
590                 window.focus();
591             create_buffer(window, creator, target);
592         } else if (queued_buffer_creators != null) {
593             queued_buffer_creators.push([creator,target]);
594         } else {
595             queued_buffer_creators = [];
596             window = make_window(creator);
597             add_hook.call(window, "window_initialize_late_hook", process_queued_buffer_creators);
598         }
599     }
604  * Read Buffer
605  */
606 define_variable("read_buffer_show_icons", false,
607     "Boolean which says whether read_buffer should show buffer "+
608     "icons in the completions list.\nNote, setting this variable "+
609     "alone does not cause favicons or other kinds of icons to be "+
610     "fetched.  For that, load the `favicon' (or similar other) "+
611     "library.");
613 minibuffer_auto_complete_preferences["buffer"] = true;
614 define_keywords("$default");
615 minibuffer.prototype.read_buffer = function () {
616     var window = this.window;
617     var buffer = this.window.buffers.current;
618     keywords(arguments, $prompt = "Buffer:",
619              $default = buffer,
620              $history = "buffer");
621     var completer = all_word_completer(
622         $completions = function (visitor) window.buffers.for_each(visitor),
623         $get_string = function (x) x.description,
624         $get_description = function (x) x.title,
625         $get_icon = (read_buffer_show_icons ?
626                      function (x) x.icon : null));
627     var result = yield this.read(
628         $keymap = read_buffer_keymap,
629         $prompt = arguments.$prompt,
630         $history = arguments.$history,
631         $completer = completer,
632         $enable_icons = read_buffer_show_icons,
633         $match_required = true,
634         $auto_complete = "buffer",
635         $auto_complete_initial = true,
636         $auto_complete_delay = 0,
637         $default_completion = arguments.$default);
638     yield co_return(result);
642 function buffer_next (window, count) {
643     var index = window.buffers.selected_index;
644     var total = window.buffers.count;
645     if (total == 1)
646         throw new interactive_error("No other buffer");
647     index = (index + count) % total;
648     if (index < 0)
649         index += total;
650     window.buffers.current = window.buffers.get_buffer(index);
652 interactive("buffer-next",
653     "Switch to the next buffer.",
654     function (I) { buffer_next(I.window, I.p); });
655 interactive("buffer-previous",
656     "Switch to the previous buffer.",
657     function (I) { buffer_next(I.window, -I.p); });
659 function switch_to_buffer (window, buffer) {
660     if (buffer && !buffer.dead)
661         window.buffers.current = buffer;
663 interactive("switch-to-buffer",
664     "Switch to a buffer specified in the minibuffer.",
665     function (I) {
666         switch_to_buffer(
667             I.window,
668             (yield I.minibuffer.read_buffer(
669                 $prompt = "Switch to buffer:",
670                 $default = (I.window.buffers.count > 1 ?
671                             I.window.buffers.buffer_list[1] :
672                             I.buffer))));
673     });
675 define_variable("can_kill_last_buffer", true,
676     "If this is set to true, kill-buffer can kill the last "+
677     "remaining buffer, and close the window.");
679 function kill_other_buffers (buffer) {
680     if (!buffer)
681         return;
682     var bs = buffer.window.buffers;
683     var b;
684     while ((b = bs.get_buffer(0)) != buffer)
685         bs.kill_buffer(b);
686     var count = bs.count;
687     while (--count)
688         bs.kill_buffer(bs.get_buffer(1));
690 interactive("kill-other-buffers",
691     "Kill all buffers except current one.\n",
692     function (I) { kill_other_buffers(I.buffer); });
695 function kill_buffer (buffer, force) {
696     if (!buffer)
697         return;
698     var buffers = buffer.window.buffers;
699     if (buffers.count == 1 && buffer == buffers.current) {
700         if (can_kill_last_buffer || force) {
701             delete_window(buffer.window);
702             return;
703         } else
704             throw interactive_error("Can't kill last buffer.");
705     }
706     buffers.kill_buffer(buffer);
708 interactive("kill-buffer",
709     "Kill a buffer specified in the minibuffer.\n" +
710     "If `can_kill_last_buffer' is set to true, an attempt to kill the "+
711     "last remaining buffer in a window will cause the window to be closed.",
712     function (I) {
713         kill_buffer((yield I.minibuffer.read_buffer($prompt = "Kill buffer:")));
714     });
716 interactive("kill-current-buffer",
717     "Kill the current buffer.\n" +
718     "If `can_kill_last_buffer' is set to true, an attempt to kill the "+
719     "last remaining buffer in a window will cause the window to be closed.",
720     function (I) { kill_buffer(I.buffer); });
722 interactive("read-buffer-kill-buffer",
723     "Kill the current selected buffer in the completions list "+
724     "in a read buffer minibuffer interaction.",
725     function (I) {
726         var s = I.window.minibuffer.current_state;
727         var i = s.selected_completion_index;
728         var c = s.completions;
729         if (i == -1)
730             return;
731         kill_buffer(c.get_value(i));
732         s.completer.refresh();
733         s.handle_input(I.window.minibuffer);
734     });
736 interactive("bury-buffer",
737     "Bury the current buffer.\n Put the current buffer at the end of " +
738     "the buffer list, so that it is the least likely buffer to be " +
739     "selected by `switch-to-buffer'.",
740     function (I) { I.window.buffers.bury_buffer(I.buffer); });
742 function change_directory (buffer, dir) {
743     if (buffer.page != null)
744         delete buffer.page.local.cwd;
745     buffer.local.cwd = make_file(dir);
747 interactive("change-directory",
748     "Change the current directory of the selected buffer.",
749     function (I) {
750         change_directory(
751             I.buffer,
752             (yield I.minibuffer.read_existing_directory_path(
753                 $prompt = "Change to directory:",
754                 $initial_value = make_file(I.local.cwd).path)));
755     });
757 interactive("shell-command", null,
758     function (I) {
759         var cwd = I.local.cwd;
760         var cmd = (yield I.minibuffer.read_shell_command($cwd = cwd));
761         yield shell_command(cmd, $cwd = cwd);
762     });
766  * selection_is_embed_p is used to test whether the unfocus command can
767  * unfocus an element, even though there is a selection.  This happens
768  * when the focused element is an html:embed.
769  */
770 function selection_is_embed_p (sel, focused_element) {
771     if (sel.rangeCount == 1) {
772         try {
773             var r = sel.getRangeAt(0);
774             var a = r.startContainer.childNodes[r.startOffset];
775             if ((a instanceof Ci.nsIDOMHTMLEmbedElement ||
776                  a instanceof Ci.nsIDOMHTMLObjectElement) &&
777                 a == focused_element)
778             {
779                 return true;
780             }
781         } catch (e) {}
782     }
783     return false;
787  * unfocus is a high-level command for unfocusing hyperlinks, inputs,
788  * frames, iframes, plugins, and also clearing the selection.
789  */
790 define_buffer_local_hook("unfocus_hook");
791 function unfocus (window, buffer) {
792     // 1. if there is a selection, clear it.
793     var selc = buffer.focused_selection_controller;
794     if (selc) {
795         var sel = selc.getSelection(selc.SELECTION_NORMAL);
796         var active = ! sel.isCollapsed;
797         var embed_p = selection_is_embed_p(sel, buffer.focused_element);
798         clear_selection(buffer);
799         if (active && !embed_p) {
800             window.minibuffer.message("cleared selection");
801             return;
802         }
803     }
804     // 2. if there is a focused element, unfocus it.
805     if (buffer.focused_element) {
806         buffer.focused_element.blur();
807         // if an element in a detached fragment has focus, blur() will
808         // not work, and we need to take more drastic measures.  the
809         // action taken was found through experiment, so it is possibly
810         // not the most concise way to unfocus such an element.
811         if (buffer.focused_element) {
812             buffer.element.focus();
813             buffer.top_frame.focus();
814         }
815         window.minibuffer.message("unfocused element");
816         return;
817     }
818     // 3. if an iframe has focus, we must blur it.
819     if (buffer.focused_frame_or_null &&
820         buffer.focused_frame_or_null.frameElement)
821     {
822         buffer.focused_frame_or_null.frameElement.blur();
823     }
824     // 4. return focus to top-frame from subframes and plugins.
825     buffer.top_frame.focus();
826     buffer.top_frame.focus(); // needed to get focus back from plugins
827     window.minibuffer.message("refocused top frame");
828     // give page-modes an opportunity to set focus specially
829     unfocus_hook.run(buffer);
831 interactive("unfocus",
832     "Unfocus is a high-level command for unfocusing hyperlinks, inputs, "+
833     "frames, iframes, plugins, and also for clearing the selection.\n"+
834     "The action that it takes is based on precedence.  If there is a "+
835     "focused hyperlink or input, it will unfocus that.  Otherwise, if "+
836     "there is a selection, it will clear the selection.  Otherwise, it "+
837     "will return focus to the top frame from a focused frame, iframe, "+
838     "or plugin.  In the case of plugins, since they steal keyboard "+
839     "control away from Conkeror, the normal way to unfocus them is "+
840     "to use command-line remoting externally: conkeror -batch -f "+
841     "unfocus.  Page-modes also have an opportunity to alter the default"+
842     "focus via the hook, `focus_hook'.",
843     function (I) {
844         unfocus(I.window, I.buffer);
845     });
848 function for_each_buffer (f) {
849     for_each_window(function (w) { w.buffers.for_each(f); });
854  * Buffer Modes
855  */
857 define_buffer_local_hook("buffer_mode_change_hook");
858 define_current_buffer_hook("current_buffer_mode_change_hook", "buffer_mode_change_hook");
860 define_keywords("$display_name", "$doc");
861 function buffer_mode (name, enable, disable) {
862     keywords(arguments);
863     this.hyphen_name = name.replace("_","-","g");
864     this.name = this.hyphen_name.replace("-","_","g"); // normalize
865     this._enable = enable;
866     this._disable = disable;
867     this.display_name = arguments.$display_name;
868     this.doc = arguments.$doc;
869     this.enable_hook = name + "_enable_hook";
870     this.disable_hook = name + "_disable_hook";
872 buffer_mode.prototype = {
873     constructor: buffer_mode,
874     name: null,
875     display_name: null,
876     doc: null,
877     enable_hook: null,
878     disable_hook: null,
879     _enable: null,
880     _disable: null,
881     enable: function (buffer) {
882         try {
883             this._enable(buffer);
884         } finally {
885             buffer.enabled_modes.push(this.name);
886             if (conkeror[this.enable_hook])
887                 conkeror[this.enable_hook].run(buffer);
888             buffer_mode_change_hook.run(buffer);
889         }
890     },
891     disable: function (buffer) {
892         try {
893             this._disable(buffer);
894         } finally {
895             var i = buffer.enabled_modes.indexOf(this.name);
896             if (i > -1)
897                 buffer.enabled_modes.splice(i, 1);
898             if (conkeror[this.disable_hook])
899                 conkeror[this.disable_hook].run(buffer);
900             buffer_mode_change_hook.run(buffer);
901         }
902     }
904 define_keywords("$constructor");
905 function define_buffer_mode (name, enable, disable) {
906     keywords(arguments, $constructor = buffer_mode);
907     var constructor = arguments.$constructor;
908     var m = new constructor(name, enable, disable, forward_keywords(arguments));
909     name = m.name; // normalized
910     conkeror[name] = m;
911     define_buffer_local_hook(m.enable_hook);
912     define_buffer_local_hook(m.disable_hook);
913     interactive(m.hyphen_name,
914         arguments.$doc,
915         function (I) {
916             var enabledp = (I.buffer.enabled_modes.indexOf(name) > -1);
917             if (enabledp)
918                 m.disable(I.buffer);
919             else
920                 m.enable(I.buffer);
921             I.minibuffer.message(m.hyphen_name + (enabledp ? " disabled" : " enabled"));
922         });
923 ignore_function_for_get_caller_source_code_reference("define_buffer_mode");
927  * Mode Display in Minibuffer
928  */
930 function minibuffer_mode_indicator (window) {
931     this.window = window;
932     var element = create_XUL(window, "label");
933     element.setAttribute("id", "minibuffer-mode-indicator");
934     element.setAttribute("class", "mode-text-widget");
935     window.document.getElementById("minibuffer").appendChild(element);
936     this.element = element;
937     this.hook_function = method_caller(this, this.update);
938     add_hook.call(window, "select_buffer_hook", this.hook_function);
939     add_hook.call(window, "current_buffer_mode_change_hook", this.hook_function);
940     this.update();
942 minibuffer_mode_indicator.prototype = {
943     constructor: minibuffer_mode_indicator,
944     window: null,
945     element: null,
946     hook_function: null,
947     update: function () {
948         var buffer = this.window.buffers.current;
949         var str = buffer.enabled_modes.map(
950             function (x) {
951                 return (conkeror[x].display_name || null);
952             }).filter(function (x) x != null).join(" ");
953         this.element.value = str;
954     },
955     uninstall: function () {
956         remove_hook.call(this.window, "select_buffer_hook", this.hook_function);
957         remove_hook.call(this.window, "current_buffer_mode_change_hook", this.hook_function);
958         this.element.parentNode.removeChild(this.element);
959     }
961 define_global_window_mode("minibuffer_mode_indicator", "window_initialize_hook");
962 minibuffer_mode_indicator_mode(true);
966  * minibuffer-keymaps-display
967  */
968 function minibuffer_keymaps_display_update (buffer) {
969     var element = buffer.window.document
970         .getElementById("keymaps-display");
971     if (element) {
972         var str = buffer.keymaps.reduce(
973             function (acc, kmap) {
974                 if (kmap.display_name)
975                     acc.push(kmap.display_name);
976                 return acc;
977             }, []).join("/");
978         if (element.value != str)
979             element.value = str;
980     }
983 function minibuffer_keymaps_display_initialize (window) {
984     var element = create_XUL(window, "label");
985     element.setAttribute("id", "keymaps-display");
986     element.setAttribute("class", "mode-text-widget");
987     element.setAttribute("value", "");
988     var mb = window.document.getElementById("minibuffer");
989     mb.appendChild(element);
992 define_global_mode("minibuffer_keymaps_display_mode",
993     function enable () {
994         add_hook("window_initialize_hook", minibuffer_keymaps_display_initialize);
995         add_hook("set_input_mode_hook", minibuffer_keymaps_display_update);
996         for_each_window(minibuffer_keymaps_display_initialize);
997     },
998     function disable () {
999         remove_hook("window_initialize_hook", minibuffer_keymaps_display_initialize);
1000         remove_hook("set_input_mode_hook", minibuffer_keymaps_display_update);
1001         for_each_window(function (w) {
1002             var element = w.document
1003                 .getElementById("keymaps-display");
1004             if (element)
1005                 element.parentNode.removeChild(element);
1006         });
1007     });
1009 minibuffer_keymaps_display_mode(true);
1013  * minibuffer-keymaps-highlight
1014  */
1015 function minibuffer_keymaps_highlight_update (buffer) {
1016     var mb = buffer.window.document.getElementById("minibuffer");
1017     if (buffer.keymaps.some(function (k) k.notify))
1018         dom_add_class(mb, "highlight");
1019     else
1020         dom_remove_class(mb, "highlight");
1023 define_global_mode("minibuffer_keymaps_highlight_mode",
1024     function enable () {
1025         add_hook("set_input_mode_hook", minibuffer_keymaps_highlight_update);
1026     },
1027     function disable () {
1028         remove_hook("set_input_mode_hook", minibuffer_keymaps_highlight_update);
1029         for_each_window(function (w) {
1030             var mb = w.document.getElementById("minibuffer");
1031             if (mb)
1032                 dom_remove_class("highlight");
1033         });
1034     });
1036 minibuffer_keymaps_highlight_mode(true);
1039 provide("buffer");