download-manager.js: fix bug resulting in un-killable special buffers
[conkeror.git] / modules / content-buffer.js
blob569c35cfcc718926a7c1f83799a60824b1d58d5e
1 /**
2  * (C) Copyright 2004-2007 Shawn Betts
3  * (C) Copyright 2007-2008 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 require("load-spec.js");
12 require_later("content-buffer-input.js");
14 define_buffer_local_hook("content_buffer_finished_loading_hook");
15 define_buffer_local_hook("content_buffer_started_loading_hook");
16 define_buffer_local_hook("content_buffer_progress_change_hook");
17 define_buffer_local_hook("content_buffer_location_change_hook");
18 define_buffer_local_hook("content_buffer_status_change_hook");
19 define_buffer_local_hook("content_buffer_focus_change_hook");
20 define_buffer_local_hook("content_buffer_overlink_change_hook");
21 define_buffer_local_hook("content_buffer_dom_link_added_hook");
23 define_current_buffer_hook("current_content_buffer_finished_loading_hook", "content_buffer_finished_loading_hook");
24 define_current_buffer_hook("current_content_buffer_progress_change_hook", "content_buffer_progress_change_hook");
25 define_current_buffer_hook("current_content_buffer_location_change_hook", "content_buffer_location_change_hook");
26 define_current_buffer_hook("current_content_buffer_status_change_hook", "content_buffer_status_change_hook");
27 define_current_buffer_hook("current_content_buffer_focus_change_hook", "content_buffer_focus_change_hook");
28 define_current_buffer_hook("current_content_buffer_overlink_change_hook", "content_buffer_overlink_change_hook");
30 /* If browser is null, create a new browser */
31 define_keywords("$load");
32 function content_buffer(window, element)
34     keywords(arguments);
35     this.constructor_begin();
37     conkeror.buffer.call(this, window, element, forward_keywords(arguments));
39     this.browser.addProgressListener(this);
40     var buffer = this;
41     this.browser.addEventListener("DOMTitleChanged", function (event) {
42             buffer_title_change_hook.run(buffer);
43         }, true /* capture */, false /* ignore untrusted events */);
45     this.browser.addEventListener("scroll", function (event) {
46             buffer_scroll_hook.run(buffer);
47         }, true /* capture */, false /* ignore untrusted events */);
49     this.browser.addEventListener("focus", function (event) {
50             content_buffer_focus_change_hook.run(buffer, event);
51         }, true /* capture */, false /* ignore untrusted events */);
53     this.browser.addEventListener("mouseover", function (event) {
54             if (event.target instanceof Ci.nsIDOMHTMLAnchorElement) {
55                 content_buffer_overlink_change_hook.run(buffer, event.target.href);
56                 buffer.current_overlink = event.target;
57             }
58         }, true, false);
60     this.browser.addEventListener("mouseout", function (event) {
61             if (buffer.current_overlink == event.target) {
62                 buffer.current_overlink = null;
63                 content_buffer_overlink_change_hook.run(buffer, "");
64             }
65         }, true, false);
67     this.browser.addEventListener("mousedown", function (event) {
68             buffer.last_user_input_received = Date.now();
69         }, true, false);
71     this.browser.addEventListener("keypress", function (event) {
72             buffer.last_user_input_received = Date.now();
73         }, true, false);
75     this.browser.addEventListener("DOMLinkAdded", function (event) {
76             content_buffer_dom_link_added_hook.run(buffer, event);
77         }, true, false);
79     buffer.last_user_input_received = null;
81     /* FIXME: Add a handler for blocked popups, and also PopupWindow event */
82     /*
83     this.browser.addEventListener("DOMPopupBlocked", function (event) {
84             dumpln("PopupWindow: " + event);
85         }, true, false);
86     */
88     normal_input_mode(this);
90     this.ignore_initial_blank = true;
92     var lspec = arguments.$load;
93     if (lspec) {
94         if (lspec.url == "about:blank")
95             this.ignore_initial_blank = false;
96         else {
97             /* Ensure that an existing load of about:blank is stopped */
98             this.web_navigation.stop(Ci.nsIWebNavigation.STOP_ALL);
100             this.load(lspec);
101         }
102     }
104     this.constructor_end();
106 content_buffer.prototype = {
107     constructor : content_buffer,
109     get scrollX () { return this.top_frame.scrollX; },
110     get scrollY () { return this.top_frame.scrollY; },
111     get scrollMaxX () { return this.top_frame.scrollMaxX; },
112     get scrollMaxY () { return this.top_frame.scrollMaxY; },
115     /* Used to display the correct URI when the buffer opens initially
116      * even before loading has progressed far enough for currentURI to
117      * contain the correct URI. */
118     _display_URI : null,
120     get display_URI_string () {
121         if (this._display_URI)
122             return this._display_URI;
123         if (this.current_URI)
124             return this.current_URI.spec;
125         return "";
126     },
128     get title() { return this.browser.contentTitle; },
129     get description () { return this.display_URI_string; },
131     load : function (load_spec) {
132         apply_load_spec(this, load_spec);
133     },
135     _request_count: 0,
137     loading : false,
139     /* nsIWebProgressListener */
140     QueryInterface: generate_QI(Ci.nsIWebProgressListener, Ci.nsISupportsWeakReference),
142     // This method is called to indicate state changes.
143     onStateChange: function(aWebProgress, aRequest, aStateFlags, aStatus) {
145         const WPL = Components.interfaces.nsIWebProgressListener;
147         var flagstr = "";
148         if (aStateFlags & WPL.STATE_START)
149             flagstr += ",start";
150         if (aStateFlags & WPL.STATE_STOP)
151             flagstr += ",stop";
152         if (aStateFlags & WPL.STATE_IS_REQUEST)
153             flagstr += ",request";
154         if (aStateFlags & WPL.STATE_IS_DOCUMENT)
155             flagstr += ",document";
156         if (aStateFlags & WPL.STATE_IS_NETWORK)
157             flagstr += ",network";
158         if (aStateFlags & WPL.STATE_IS_WINDOW)
159             flagstr += ",window";
160         dumpln("onStateChange: " + flagstr + ", status: " + aStatus);
162         if (!aRequest)
163             return;
166         if (aStateFlags & Ci.nsIWebProgressListener.STATE_START) {
167             this._request_count++;
168         }
169         else if (aStateFlags & Ci.nsIWebProgressListener.STATE_STOP) {
170             const NS_ERROR_UNKNOWN_HOST = 2152398878;
171             if (--this._request_count > 0 && aStatus == NS_ERROR_UNKNOWN_HOST) {
172                 // to prevent bug 235825: wait for the request handled
173                 // by the automatic keyword resolver
174                 return;
175             }
176             // since we (try to) only handle STATE_STOP of the last request,
177             // the count of open requests should now be 0
178             this._request_count = 0;
179         }
181         if (aStateFlags & Ci.nsIWebProgressListener.STATE_START &&
182             aStateFlags & Ci.nsIWebProgressListener.STATE_IS_NETWORK) {
183             // It's okay to clear what the user typed when we start
184             // loading a document. If the user types, this counter gets
185             // set to zero, if the document load ends without an
186             // onLocationChange, this counter gets decremented
187             // (so we keep it while switching tabs after failed loads)
188             //dumpln("*** started loading");
189             this.loading = true;
190             content_buffer_started_loading_hook.run(this);
191             this.last_user_input_received = null;
192         }
193         else if (aStateFlags & Ci.nsIWebProgressListener.STATE_STOP &&
194                  aStateFlags & Ci.nsIWebProgressListener.STATE_IS_NETWORK) {
195             if (this.loading == true)  {
196                 //dumpln("*** finished loading");
197                 content_buffer_finished_loading_hook.run(this);
198                 this.loading = false;
199             }
200         }
202         if (aStateFlags & (Ci.nsIWebProgressListener.STATE_STOP |
203                            Ci.nsIWebProgressListener.STATE_START)) {
204             if (!this.loading)
205                 this.set_default_message("Done");
206         }
207     },
209     /* This method is called to indicate progress changes for the currently
210        loading page. */
211     onProgressChange: function(webProgress, request, curSelf, maxSelf,
212                                curTotal, maxTotal) {
213         content_buffer_progress_change_hook.run(this, request, curSelf, maxSelf, curTotal, maxTotal);
214     },
216     /* This method is called to indicate a change to the current location.
217        The url can be gotten as location.spec. */
218     onLocationChange : function(webProgress, request, location) {
219         /* Attempt to ignore onLocationChange calls due to the initial
220          * loading of about:blank by all xul:browser elements. */
221         if (location.spec == "about:blank" && this.ignore_initial_blank)
222             return;
224         this.ignore_initial_blank = false;
226         //dumpln("spec: " + location.spec  +" ;;; " + this.display_URI_string);
227         /* Use the real location URI now */
228         this._display_URI = null;
229         content_buffer_location_change_hook.run(this, request, location);
230         this.last_user_input_received = null;
231         buffer_description_change_hook.run(this);
232     },
234     // This method is called to indicate a status changes for the currently
235     // loading page.  The message is already formatted for display.
236     // Status messages could be displayed in the minibuffer output area.
237     onStatusChange: function(webProgress, request, status, msg) {
238         this.set_default_message(msg);
239         content_buffer_status_change_hook.run(this, request, status, msg);
240     },
242     // This method is called when the security state of the browser changes.
243     onSecurityChange: function(webProgress, request, state) {
244         /* FIXME: currently this isn't used */
246         /*
247         const WPL = Components.interfaces.nsIWebProgressListener;
249         if (state & WPL.STATE_IS_INSECURE) {
250             // update visual indicator
251         } else {
252             var level = "unknown";
253             if (state & WPL.STATE_IS_SECURE) {
254                 if (state & WPL.STATE_SECURE_HIGH)
255                     level = "high";
256                 else if (state & WPL.STATE_SECURE_MED)
257                     level = "medium";
258                 else if (state & WPL.STATE_SECURE_LOW)
259                     level = "low";
260             } else if (state & WPL_STATE_IS_BROKEN) {
261                 level = "mixed";
262             }
263             // provide a visual indicator of the security state here.
264         }
265         */
266     },
268     /* Inherit from buffer */
270     __proto__ : buffer.prototype
274 add_hook("current_content_buffer_finished_loading_hook",
275          function (buffer) {
276                  buffer.window.minibuffer.show("Done");
277          });
279 add_hook("current_content_buffer_status_change_hook",
280          function (buffer, request, status, msg) {
281              buffer.set_default_message(msg);
282          });
285 define_variable("url_completion_use_webjumps", true, "Specifies whether URL completion should complete webjumps.");
286 define_variable("url_completion_use_bookmarks", true, "Specifies whether URL completion should complete bookmarks.");
287 define_variable("url_completion_use_history", false,
288                      "Specifies whether URL completion should complete using browser history.");
290 minibuffer_auto_complete_preferences["url"] = true;
291 minibuffer.prototype.read_url = function () {
292     keywords(arguments, $prompt = "URL:", $history = "url", $initial_value = "",
293              $use_webjumps = url_completion_use_webjumps,
294              $use_history = url_completion_use_history,
295              $use_bookmarks = url_completion_use_bookmarks);
296     var completer = url_completer ($use_webjumps = arguments.$use_webjumps,
297         $use_bookmarks = arguments.$use_bookmarks,
298         $use_history = arguments.$use_history);
299     var result = yield this.read(
300         $prompt = arguments.$prompt,
301         $history = arguments.$history,
302         $completer = completer,
303         $initial_value = arguments.$initial_value,
304         $auto_complete = "url",
305         $select,
306         $match_required = false);
307     if (result == "") // well-formedness check. (could be better!)
308         throw ("invalid url or webjump (\""+ result +"\")");
309     yield co_return(result);
312 I.content_charset = interactive_method(
313     $sync = function (ctx) {
314         var buffer = ctx.buffer;
315         if (!(buffer instanceof content_buffer))
316             throw new Error("Current buffer is of invalid type");
317         // -- Charset of content area of focusedWindow
318         var focusedWindow = buffer.focused_frame;
319         if (focusedWindow)
320             return focusedWindow.document.characterSet;
321         else
322             return null;
323     });
326 I.content_selection = interactive_method(
327     $sync = function (ctx) {
328         // -- Selection of content area of focusedWindow
329         var focusedWindow = this.buffers.current.focused_frame;
330         return focusedWindow.getSelection ();
331     });
333 function overlink_update_status(buffer, text) {
334     if (text.length > 0)
335         buffer.window.minibuffer.show("Link: " + text);
336     else
337         buffer.window.minibuffer.show("");
340 define_global_mode("overlink_mode",
341                    function () {
342                        add_hook("current_content_buffer_overlink_change_hook", overlink_update_status);
343                    },
344                    function () {
345                        remove_hook("current_content_buffer_overlink_change_hook", overlink_update_status);
346                    });
348 overlink_mode(true);
350 function open_in_browser(buffer, target, lspec)
352     switch (target) {
353     case OPEN_CURRENT_BUFFER:
354     case FOLLOW_DEFAULT:
355     case FOLLOW_CURRENT_FRAME:
356     case FOLLOW_TOP_FRAME:
357         if (buffer instanceof content_buffer)  {
358             apply_load_spec(buffer, lspec);
359             break;
360         }
361         target = OPEN_NEW_BUFFER;
362         // If the current buffer is not a content_buffer, use a new buffer.
363     default:
364         create_buffer(buffer.window,
365                       buffer_creator(content_buffer,
366                                      $load = lspec,
367                                      $configuration = buffer.configuration),
368                       target);
369         break;
370     }
373 interactive("find-alternate-url",
374             "Edit the current URL in the minibuffer",
375             function (I) {
376                 var target = I.browse_target("find-url");
377                 check_buffer(I.buffer, content_buffer);
378                 open_in_browser(I.buffer, target,
379                                 (yield I.minibuffer.read_url($prompt = browse_target_prompt(target),
380                                                              $initial_value = I.buffer.display_URI_string)));
381             });
383 interactive("find-url",
384             "Open a URL in the current buffer",
385             function (I) {
386                 var target = I.browse_target("find-url");
387                 open_in_browser(I.buffer, target,
388                                 (yield I.minibuffer.read_url($prompt = browse_target_prompt(target))));
389             });
390 default_browse_targets["find-url"] = [OPEN_CURRENT_BUFFER, OPEN_NEW_BUFFER, OPEN_NEW_WINDOW];
393 interactive("find-url-new-buffer",
394             "Open a URL in a new buffer",
395             function (I) {
396                 var target = I.browse_target("find-url-new-buffer");
397                 open_in_browser(I.buffer, target,
398                                 (yield I.minibuffer.read_url($prompt = browse_target_prompt(target))));
399             });
400 default_browse_targets["find-url-new-buffer"] = [OPEN_NEW_BUFFER, OPEN_NEW_WINDOW];
402 interactive("find-url-new-window",
403             "Open a URL in a new window",
404             function (I) {
405                 var target = I.browse_target("find-url-new-window");
406                 open_in_browser(I.buffer, target,
407                                 (yield I.minibuffer.read_url($prompt = browse_target_prompt(target))));
408             });
409 default_browse_targets["find-url-new-window"] = [OPEN_NEW_WINDOW];
411 function go_up (b, target)
413     var url = Cc["@mozilla.org/network/standard-url;1"]
414         .createInstance (Ci.nsIURL);
415     url.spec = b.current_URI.spec;
416     var up;
417     if (url.param != "" || url.query != "")
418         up = url.filePath;
419     else if (url.fileName != "")
420         up = ".";
421     else
422         up = "..";
423     open_in_browser(b, target, b.current_URI.resolve (up));
425 interactive("go-up",
426             "Go to the parent directory of the current URL",
427             function (I) { go_up(check_buffer(I.buffer, content_buffer),  I.browse_target("go-up")); });
428 default_browse_targets["go-up"] = "find-url";
431 function go_back (b, prefix)
433     if (prefix < 0)
434         go_forward(b, -prefix);
436     check_buffer(b, content_buffer);
438     if (b.web_navigation.canGoBack)
439     {
440         var hist = b.web_navigation.sessionHistory;
441         var idx = hist.index - prefix;
442         if (idx < 0)
443             idx = 0;
444         b.web_navigation.gotoIndex(idx);
445     } else
446         throw interactive_error("Can't go back");
448 interactive(
449     "go-back",
450     "Go back in the session hisory for the current buffer.",
451     function (I) {go_back(I.buffer, I.p);});
453 function go_forward (b, prefix)
455     if (prefix < 0)
456         go_back(b, -prefix);
458     check_buffer(b, content_buffer);
460     if (b.web_navigation.canGoForward)
461     {
462         var hist = b.web_navigation.sessionHistory;
463         var idx = hist.index + prefix;
464         if (idx >= hist.count) idx = hist.count-1;
465         b.web_navigation.gotoIndex(idx);
466     } else
467         throw interactive_error("Can't go forward");
469 interactive("go-forward",
470             "Go back in the session hisory for the current buffer.",
471             function (I) {go_forward(I.buffer, I.p);});
473 function stop_loading (b)
475     check_buffer(b, content_buffer);
476     b.web_navigation.stop(Ci.nsIWebNavigation.STOP_NETWORK);
478 interactive("stop-loading",
479             "Stop loading the current document.",
480             function (I) {stop_loading(I.buffer);});
482 function reload (b, bypass_cache)
484     check_buffer(b, content_buffer);
485     var flags = bypass_cache != null ?
486         Ci.nsIWebNavigation.LOAD_FLAGS_NONE : Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_CACHE;
487     b.web_navigation.reload(flags);
489 interactive("reload",
490             "Reload the current document.\n" +
491             "If a prefix argument is specified, the cache is bypassed.",
492             function (I) {reload(I.buffer, I.P);});
495  * browserDOMWindow: intercept window opening
496  */
497 function initialize_browser_dom_window(window) {
498     window.QueryInterface(Ci.nsIDOMChromeWindow).browserDOMWindow =
499         new browser_dom_window(window);
502 define_variable("browser_default_open_target", OPEN_NEW_BUFFER, "Specifies how new window requests by content pages (e.g. by window.open from JavaScript or by using the target attribute of anchor and form elements) will be handled.  This will generally be `OPEN_NEW_BUFFER', `OPEN_NEW_BUFFER_BACKGROUND', or `OPEN_NEW_WINDOW'.");
504 function browser_dom_window(window) {
505     this.window = window;
506     this.next_target = null;
508 browser_dom_window.prototype = {
509     QueryInterface: generate_QI(Ci.nsIBrowserDOMWindow),
511     openURI : function(aURI, aOpener, aWhere, aContext) {
513         // Reference: http://www.xulplanet.com/references/xpcomref/ifaces/nsIBrowserDOMWindow.html
514         var target = this.next_target;
515         if (target == null || target == FOLLOW_DEFAULT)
516             target = browser_default_open_target;
517         this.next_target = null;
519         /* Determine the opener buffer */
520         var opener_buffer = get_buffer_from_frame(this.window, aOpener.top);
521         var config = opener_buffer ? opener_buffer.configuration : null;
523         switch (browser_default_open_target) {
524         case OPEN_CURRENT_BUFFER:
525         case FOLLOW_TOP_FRAME:
526             return aOpener.top;
527         case FOLLOW_CURRENT_FRAME:
528             return aOpener;
529         case OPEN_NEW_BUFFER:
530             var buffer = new content_buffer(this.window, null /* element */, $configuration = config);
531             this.window.buffers.current = buffer;
532             return buffer.top_frame;
533         case OPEN_NEW_BUFFER_BACKGROUND:
534             var buffer = new content_buffer(this.window, null /* element */, $configuration = config);
535             return buffer.top_frame;
536         case OPEN_NEW_WINDOW:
537         default: /* shouldn't be needed */
539             /* We don't call make_window here, because that will result
540              * in the URL being loaded as the top-level document,
541              * instead of within a browser buffer.  Instead, we can
542              * rely on Mozilla using browser.chromeURL. */
543             window_set_extra_arguments({initial_buffer_configuration: config});
544             return null;
545         }
546     }
549 add_hook("window_initialize_early_hook", initialize_browser_dom_window);
551 define_keywords("$enable", "$disable", "$doc");
552 function define_page_mode(name, display_name) {
553     keywords(arguments);
554     var enable = arguments.$enable;
555     var disable = arguments.$disable;
556     var doc = arguments.$doc;
557     define_buffer_mode(name, display_name,
558                        $class = "page_mode",
559                        $enable = enable,
560                        $disable = function (buffer) {
561                            if (disable)
562                                disable(buffer);
563                            buffer.local_variables = {};
564                        },
565                        $doc = doc);
567 ignore_function_for_get_caller_source_code_reference("define_page_mode");
569 define_variable("auto_mode_list", [], "A list of mappings from URI regular expressions to page modes.");
570 function page_mode_auto_update(buffer) {
571     var uri = buffer.current_URI.spec;
572     var mode = predicate_alist_match(auto_mode_list, uri);
573     if (mode)
574         mode(buffer, true);
575     else if (buffer.page_mode)
576         conkeror[buffer.page_mode](buffer, false);
579 add_hook("content_buffer_location_change_hook", page_mode_auto_update);