Refreshed debian/patches/sensible-editor.diff
[conkeror.git] / modules / content-buffer.js
blob7c37c5923076ed6fd8e3951d515f47ab88e28946
1 /**
2  * (C) Copyright 2004-2007 Shawn Betts
3  * (C) Copyright 2007-2009 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("buffer.js");
11 require("load-spec.js");
13 //FIXME: circular dependency for browser_prevent_automatic_form_focus_mode,
14 //       and content_buffer_update_input_mode_for_focus.
15 require_later("content-buffer-input.js");
17 define_variable("homepage", "chrome://conkeror-help/content/help.html",
18                 "The url loaded by default for new content buffers.");
20 define_buffer_local_hook("content_buffer_finished_loading_hook");
21 define_buffer_local_hook("content_buffer_started_loading_hook");
22 define_buffer_local_hook("content_buffer_progress_change_hook");
23 define_buffer_local_hook("content_buffer_location_change_hook");
24 define_buffer_local_hook("content_buffer_status_change_hook");
25 define_buffer_local_hook("content_buffer_focus_change_hook");
26 define_buffer_local_hook("content_buffer_overlink_change_hook");
27 define_buffer_local_hook("content_buffer_dom_link_added_hook");
28 define_buffer_local_hook("content_buffer_popup_blocked_hook");
30 define_current_buffer_hook("current_content_buffer_finished_loading_hook", "content_buffer_finished_loading_hook");
31 define_current_buffer_hook("current_content_buffer_progress_change_hook", "content_buffer_progress_change_hook");
32 define_current_buffer_hook("current_content_buffer_location_change_hook", "content_buffer_location_change_hook");
33 define_current_buffer_hook("current_content_buffer_status_change_hook", "content_buffer_status_change_hook");
34 define_current_buffer_hook("current_content_buffer_focus_change_hook", "content_buffer_focus_change_hook");
35 define_current_buffer_hook("current_content_buffer_overlink_change_hook", "content_buffer_overlink_change_hook");
37 /* If browser is null, create a new browser */
38 define_keywords("$load");
39 function content_buffer (window, element) {
40     keywords(arguments);
41     this.constructor_begin();
42     try {
43         conkeror.buffer.call(this, window, element, forward_keywords(arguments));
45         this.browser.addProgressListener(this);
46         var buffer = this;
47         this.browser.addEventListener("DOMTitleChanged", function (event) {
48             buffer_title_change_hook.run(buffer);
49         }, true /* capture */, false /* ignore untrusted events */);
51         this.browser.addEventListener("scroll", function (event) {
52             buffer_scroll_hook.run(buffer);
53         }, true /* capture */, false /* ignore untrusted events */);
55         this.browser.addEventListener("focus", function (event) {
56             content_buffer_focus_change_hook.run(buffer, event);
57         }, true /* capture */, false /* ignore untrusted events */);
59         this.browser.addEventListener("mouseover", function (event) {
60             var node = event.target;
61             while (node && !(node instanceof Ci.nsIDOMHTMLAnchorElement))
62                 node = node.parentNode;
63             if (node) {
64                 content_buffer_overlink_change_hook.run(buffer, node.href);
65                 buffer.current_overlink = event.target;
66             }
67         }, true, false);
69         this.browser.addEventListener("mouseout", function (event) {
70             // mouseout events can occur without any mouseover taking place
71             // prior to it, so we have to make one extra 'undefined' check to
72             // avoid ugly warnings.
73             if (typeof buffer.current_overlink !== 'undefined' && buffer.current_overlink == event.target) {
74                 buffer.current_overlink = null;
75                 content_buffer_overlink_change_hook.run(buffer, "");
76             }
77         }, true, false);
79         this.browser.addEventListener("mousedown", function (event) {
80             buffer.last_user_input_received = Date.now();
81         }, true, false);
83         this.browser.addEventListener("keypress", function (event) {
84             buffer.last_user_input_received = Date.now();
85         }, true, false);
87         this.browser.addEventListener("DOMLinkAdded", function (event) {
88             content_buffer_dom_link_added_hook.run(buffer, event);
89         }, true, false);
91         buffer.last_user_input_received = null;
93         /* FIXME: Add handler for PopupWindow event
94            WTF: What does that comment mean? Is PopupWindow an event? /Deniz
95         */
97         this.browser.addEventListener("DOMPopupBlocked", function (event) {
98             dumpln("Blocked popup: " + event.popupWindowURI.spec);
99             content_buffer_popup_blocked_hook.run(buffer, event);
100         }, true, false);
102         // XXX: This is needed to ensure we have a keymap at the
103         // instant the buffer is created, but before the page has been
104         // loaded in it.  It breaks heirarchy, as content_buffer
105         // should not have any business knowing about a specific
106         // input-mode system, so we'll see if we can figure out
107         // something more elegant.
108         content_buffer_update_input_mode_for_focus(this);
110         this.ignore_initial_blank = true;
112         var lspec = arguments.$load;
113         if (lspec) {
114             if (lspec.url == "about:blank")
115                 this.ignore_initial_blank = false;
116             else {
117                 /* Ensure that an existing load of about:blank is stopped */
118                 this.web_navigation.stop(Ci.nsIWebNavigation.STOP_ALL);
120                 this.load(lspec);
121             }
122         }
123     } finally {
124         this.constructor_end();
125     }
127 content_buffer.prototype = {
128     constructor : content_buffer,
130     get scrollX () { return this.top_frame.scrollX; },
131     get scrollY () { return this.top_frame.scrollY; },
132     get scrollMaxX () { return this.top_frame.scrollMaxX; },
133     get scrollMaxY () { return this.top_frame.scrollMaxY; },
135     /* Used to display the correct URI when the buffer opens initially
136      * even before loading has progressed far enough for currentURI to
137      * contain the correct URI. */
138     _display_URI : null,
140     get display_URI_string () {
141         if (this._display_URI)
142             return this._display_URI;
143         if (this.current_URI)
144             return this.current_URI.spec;
145         return "";
146     },
148     get title () { return this.browser.contentTitle; },
149     get description () { return this.display_URI_string; },
151     load : function (load_spec) {
152         apply_load_spec(this, load_spec);
153     },
155     _request_count: 0,
157     loading : false,
159     /* nsIWebProgressListener */
160     QueryInterface: generate_QI(Ci.nsIWebProgressListener, Ci.nsISupportsWeakReference),
162     // This method is called to indicate state changes.
163     onStateChange: function (aWebProgress, aRequest, aStateFlags, aStatus) {
165         const WPL = Components.interfaces.nsIWebProgressListener;
167         var flagstr = "";
168         if (aStateFlags & WPL.STATE_START)
169             flagstr += ",start";
170         if (aStateFlags & WPL.STATE_STOP)
171             flagstr += ",stop";
172         if (aStateFlags & WPL.STATE_IS_REQUEST)
173             flagstr += ",request";
174         if (aStateFlags & WPL.STATE_IS_DOCUMENT)
175             flagstr += ",document";
176         if (aStateFlags & WPL.STATE_IS_NETWORK)
177             flagstr += ",network";
178         if (aStateFlags & WPL.STATE_IS_WINDOW)
179             flagstr += ",window";
180         dumpln("onStateChange: " + flagstr + ", status: " + aStatus);
182         if (!aRequest)
183             return;
185         if (aStateFlags & Ci.nsIWebProgressListener.STATE_START) {
186             this._request_count++;
187         } else if (aStateFlags & Ci.nsIWebProgressListener.STATE_STOP) {
188             const NS_ERROR_UNKNOWN_HOST = 2152398878;
189             if (--this._request_count > 0 && aStatus == NS_ERROR_UNKNOWN_HOST) {
190                 // to prevent bug 235825: wait for the request handled
191                 // by the automatic keyword resolver
192                 return;
193             }
194             // since we (try to) only handle STATE_STOP of the last request,
195             // the count of open requests should now be 0
196             this._request_count = 0;
197         }
199         if (aStateFlags & Ci.nsIWebProgressListener.STATE_START &&
200             aStateFlags & Ci.nsIWebProgressListener.STATE_IS_NETWORK) {
201             // It's okay to clear what the user typed when we start
202             // loading a document. If the user types, this counter gets
203             // set to zero, if the document load ends without an
204             // onLocationChange, this counter gets decremented
205             // (so we keep it while switching tabs after failed loads)
206             //dumpln("*** started loading");
207             this.loading = true;
208             content_buffer_started_loading_hook.run(this);
209             this.last_user_input_received = null;
210         } else if (aStateFlags & Ci.nsIWebProgressListener.STATE_STOP &&
211                    aStateFlags & Ci.nsIWebProgressListener.STATE_IS_NETWORK) {
212             if (this.loading == true) {
213                 //dumpln("*** finished loading");
214                 this.loading = false;
215                 content_buffer_finished_loading_hook.run(this);
216             }
217         }
219         if (aStateFlags & (Ci.nsIWebProgressListener.STATE_STOP |
220                            Ci.nsIWebProgressListener.STATE_START)) {
221             if (!this.loading)
222                 this.set_default_message("Done");
223         }
224     },
226     /* This method is called to indicate progress changes for the currently
227        loading page. */
228     onProgressChange: function (webProgress, request, curSelf, maxSelf,
229                                 curTotal, maxTotal) {
230         content_buffer_progress_change_hook.run(this, request, curSelf, maxSelf, curTotal, maxTotal);
231     },
233     /* This method is called to indicate a change to the current location.
234        The url can be gotten as location.spec. */
235     onLocationChange : function (webProgress, request, location) {
236         /* Attempt to ignore onLocationChange calls due to the initial
237          * loading of about:blank by all xul:browser elements. */
238         if (location.spec == "about:blank" && this.ignore_initial_blank)
239             return;
241         this.ignore_initial_blank = false;
243         //dumpln("spec: " + location.spec  +" ;;; " + this.display_URI_string);
244         /* Use the real location URI now */
245         this._display_URI = null;
246         content_buffer_location_change_hook.run(this, request, location);
247         this.last_user_input_received = null;
248         buffer_description_change_hook.run(this);
249     },
251     // This method is called to indicate a status changes for the currently
252     // loading page.  The message is already formatted for display.
253     // Status messages could be displayed in the minibuffer output area.
254     onStatusChange: function (webProgress, request, status, msg) {
255         this.set_default_message(msg);
256         content_buffer_status_change_hook.run(this, request, status, msg);
257     },
259     // This method is called when the security state of the browser changes.
260     onSecurityChange: function (webProgress, request, state) {
261         /* FIXME: currently this isn't used */
263         /*
264         const WPL = Components.interfaces.nsIWebProgressListener;
266         if (state & WPL.STATE_IS_INSECURE) {
267             // update visual indicator
268         } else {
269             var level = "unknown";
270             if (state & WPL.STATE_IS_SECURE) {
271                 if (state & WPL.STATE_SECURE_HIGH)
272                     level = "high";
273                 else if (state & WPL.STATE_SECURE_MED)
274                     level = "medium";
275                 else if (state & WPL.STATE_SECURE_LOW)
276                     level = "low";
277             } else if (state & WPL_STATE_IS_BROKEN) {
278                 level = "mixed";
279             }
280             // provide a visual indicator of the security state here.
281         }
282         */
283     },
285     /* Inherit from buffer */
287     __proto__ : buffer.prototype
291 add_hook("current_content_buffer_finished_loading_hook",
292          function (buffer) {
293                  buffer.window.minibuffer.show("Done");
294          });
296 add_hook("current_content_buffer_status_change_hook",
297          function (buffer, request, status, msg) {
298              buffer.set_default_message(msg);
299          });
303 define_variable("read_url_handler_list", [],
304     "A list of handler functions which transform a typed url into a valid " +
305     "url or webjump.  If the typed input is not valid then each function " +
306     "on this list is tried in turn.  The handler function is called with " +
307     "a single string argument and it should return either a string or " +
308     "null.  The result of the first function on the list that returns a " +
309     "string is used in place of the input.");
312  * read_url_make_default_webjump_handler returns a function that
313  * transforms any input into the given webjump.  It should be the last
314  * handler on read_url_handler_list (because any input is
315  * accepted).
316  */
317 function read_url_make_default_webjump_handler (default_webjump) {
318     return function (input) {
319         return default_webjump + " " + input;
320     };
324  * read_url_make_blank_url_handler returns a function that replaces a
325  * blank (empty) input with the given url (or webjump).  The url may
326  * perform some function, eg. "javascript:location.reload()".
327  */
328 function read_url_make_blank_url_handler (url) {
329     return function (input) {
330         if (input.length == 0)
331             return url;
332         return null;
333     };
336 minibuffer.prototype.try_read_url_handlers = function (input) {
337     var result;
338     for (var i = 0; i < read_url_handler_list.length; ++i) {
339         if ((result = read_url_handler_list[i](input)))
340             return result;
341     }
342     return input;
345 define_variable("url_completion_use_webjumps", true,
346     "Specifies whether URL completion should complete webjumps.");
348 define_variable("url_completion_use_bookmarks", true,
349     "Specifies whether URL completion should complete bookmarks.");
351 define_variable("url_completion_use_history", false,
352     "Specifies whether URL completion should complete using browser "+
353     "history.");
355 define_variable("minibuffer_read_url_select_initial", true,
356     "Specifies whether a URL  presented in the minibuffer for editing "+
357     "should be selected.  This affects find-alternate-url.");
360 minibuffer_auto_complete_preferences["url"] = true;
361 minibuffer.prototype.read_url = function () {
362     keywords(arguments, $prompt = "URL:", $history = "url", $initial_value = "",
363              $use_webjumps = url_completion_use_webjumps,
364              $use_history = url_completion_use_history,
365              $use_bookmarks = url_completion_use_bookmarks);
366     var completer = url_completer($use_webjumps = arguments.$use_webjumps,
367         $use_bookmarks = arguments.$use_bookmarks,
368         $use_history = arguments.$use_history);
369     var result = yield this.read(
370         $prompt = arguments.$prompt,
371         $history = arguments.$history,
372         $completer = completer,
373         $initial_value = arguments.$initial_value,
374         $auto_complete = "url",
375         $select = minibuffer_read_url_select_initial,
376         $match_required = false);
377     if (!possibly_valid_url(result) && !getWebJump(result))
378         result = this.try_read_url_handlers(result);
379     if (result == "") // well-formedness check. (could be better!)
380         throw ("invalid url or webjump (\""+ result +"\")");
381     yield co_return(result);
384 I.content_charset = interactive_method(
385     $sync = function (ctx) {
386         var buffer = ctx.buffer;
387         if (!(buffer instanceof content_buffer))
388             throw new Error("Current buffer is of invalid type");
389         // -- Charset of content area of focusedWindow
390         var focusedWindow = buffer.focused_frame;
391         if (focusedWindow)
392             return focusedWindow.document.characterSet;
393         else
394             return null;
395     });
398 I.content_selection = interactive_method(
399     $sync = function (ctx) {
400         // -- Selection of content area of focusedWindow
401         var focusedWindow = this.buffers.current.focused_frame;
402         return focusedWindow.getSelection ();
403     });
405 function overlink_update_status (buffer, text) {
406     if (text.length > 0)
407         buffer.window.minibuffer.show("Link: " + text);
408     else
409         buffer.window.minibuffer.show("");
412 define_global_mode("overlink_mode", function () {
413     add_hook("current_content_buffer_overlink_change_hook", overlink_update_status);
414 }, function () {
415     remove_hook("current_content_buffer_overlink_change_hook", overlink_update_status);
418 overlink_mode(true);
421 function go_back (b, prefix) {
422     if (prefix < 0)
423         go_forward(b, -prefix);
425     check_buffer(b, content_buffer);
427     if (b.web_navigation.canGoBack) {
428         var hist = b.web_navigation.sessionHistory;
429         var idx = hist.index - prefix;
430         if (idx < 0)
431             idx = 0;
432         b.web_navigation.gotoIndex(idx);
433     } else
434         throw interactive_error("Can't go back");
437 interactive("back",
438     "Go back in the session history for the current buffer.",
439     function (I) {go_back(I.buffer, I.p);});
442 function go_forward (b, prefix) {
443     if (prefix < 0)
444         go_back(b, -prefix);
446     check_buffer(b, content_buffer);
448     if (b.web_navigation.canGoForward) {
449         var hist = b.web_navigation.sessionHistory;
450         var idx = hist.index + prefix;
451         if (idx >= hist.count) idx = hist.count-1;
452         b.web_navigation.gotoIndex(idx);
453     } else
454         throw interactive_error("Can't go forward");
457 interactive("forward",
458             "Go forward in the session history for the current buffer.",
459             function (I) {go_forward(I.buffer, I.p);});
462 function stop_loading (b) {
463     check_buffer(b, content_buffer);
464     b.web_navigation.stop(Ci.nsIWebNavigation.STOP_NETWORK);
467 interactive("stop-loading",
468             "Stop loading the current document.",
469             function (I) {stop_loading(I.buffer);});
472 function reload (b, bypass_cache, element, forced_charset) {
473     check_buffer(b, content_buffer);
474     if (element) {
475         if (element instanceof Ci.nsIDOMHTMLImageElement) {
476             try {
477                 var cache = Cc['@mozilla.org/image/cache;1']
478                     .getService(Ci.imgICache);
479                 cache.removeEntry(make_uri(element.src));
480             } catch (e) {}
481         }
482         element.parentNode.replaceChild(element.cloneNode(true), element);
483     } else {
484         var flags = bypass_cache == null ?
485             Ci.nsIWebNavigation.LOAD_FLAGS_NONE :
486             Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_CACHE;
488         if (! forced_charset && forced_charset_list)
489             forced_charset = predicate_alist_match(forced_charset_list,
490                                                    b.current_URI.spec);
492         if (forced_charset) {
493             try {
494                 var atomservice = Cc['@mozilla.org/atom-service;1']
495                     .getService(Ci.nsIAtomService);
496                 b.web_navigation.documentCharsetInfo.forcedCharset =
497                     atomservice.getAtom(forced_charset);
498             } catch (e) {}
499         }
500         b.web_navigation.reload(flags);
501     }
504 interactive("reload",
505     "Reload the current document.\n" +
506     "If a prefix argument is specified, the cache is bypassed.  If a "+
507     "DOM node is supplied via browser object, that node will be "+
508     "reloaded.",
509     function (I) {
510         check_buffer(I.buffer, content_buffer);
511         var element = yield read_browser_object(I);
512         reload(I.buffer, I.P, element);
513     });
516  * browserDOMWindow: intercept window opening
517  */
518 function initialize_browser_dom_window (window) {
519     window.QueryInterface(Ci.nsIDOMChromeWindow).browserDOMWindow =
520         new browser_dom_window(window);
523 define_variable("browser_default_open_target", OPEN_NEW_BUFFER,
524     "Specifies how new window requests by content pages (e.g. by "+
525     "window.open from JavaScript or by using the target attribute of "+
526     "anchor and form elements) will be handled.  This will generally "+
527     "be `OPEN_NEW_BUFFER', `OPEN_NEW_BUFFER_BACKGROUND', or "+
528     "`OPEN_NEW_WINDOW'.");
531 function browser_dom_window (window) {
532     this.window = window;
533     this.next_target = null;
535 browser_dom_window.prototype = {
536     QueryInterface: generate_QI(Ci.nsIBrowserDOMWindow),
538     openURI : function (aURI, aOpener, aWhere, aContext) {
540         // Reference: http://www.xulplanet.com/references/xpcomref/ifaces/nsIBrowserDOMWindow.html
541         var target = this.next_target;
542         if (target == null || target == FOLLOW_DEFAULT)
543             target = browser_default_open_target;
544         this.next_target = null;
546         /* Determine the opener buffer */
547         var opener = get_buffer_from_frame(this.window, aOpener);
549         switch (browser_default_open_target) {
550         case OPEN_CURRENT_BUFFER:
551             return aOpener.top;
552         case FOLLOW_CURRENT_FRAME:
553             return aOpener;
554         case OPEN_NEW_BUFFER:
555             var buffer = new content_buffer(this.window, null /* element */, $opener = opener);
556             this.window.buffers.current = buffer;
557             return buffer.top_frame;
558         case OPEN_NEW_BUFFER_BACKGROUND:
559             var buffer = new content_buffer(this.window, null /* element */, $opener = opener);
560             return buffer.top_frame;
561         case OPEN_NEW_WINDOW:
562         default: /* shouldn't be needed */
564             /* We don't call make_window here, because that will result
565              * in the URL being loaded as the top-level document,
566              * instead of within a browser buffer.  Instead, we can
567              * rely on Mozilla using browser.chromeURL. */
568             window_set_extra_arguments(
569                 {initial_buffer_creator: buffer_creator(content_buffer, $opener = opener)}
570             );
571             return null;
572         }
573     }
576 add_hook("window_initialize_early_hook", initialize_browser_dom_window);
578 define_keywords("$enable", "$disable", "$doc", "$keymaps");
579 function define_page_mode(name, display_name) {
580     keywords(arguments);
581     var enable = arguments.$enable;
582     var disable = arguments.$disable;
583     var doc = arguments.$doc;
584     var keymaps = arguments.$keymaps;
585     function page_mode_update_keymap (buffer) {
586         if (keymaps[buffer.input_mode])
587             buffer.keymap = keymaps[buffer.input_mode];
588     }
589     define_buffer_mode(name, display_name,
590                        $class = "page_mode",
591                        $enable = function (buffer) {
592                            buffer.page = {
593                                local: { __proto__: buffer.local }
594                            };
595                            if (enable)
596                                enable(buffer);
597                            if (keymaps) {
598                                add_hook.call(buffer, "input_mode_change_hook",
599                                              page_mode_update_keymap);
600                                page_mode_update_keymap(buffer);
601                            }
602                        },
603                        $disable = function (buffer) {
604                            if (disable)
605                                disable(buffer);
606                            buffer.page = null;
607                            buffer.default_browser_object_classes = {};
608                            if (keymaps) {
609                                remove_hook.call(buffer, "input_mode_change_hook",
610                                                 page_mode_update_keymap);
611                                buffer_update_keymap_for_input_mode(buffer);
612                            }
613                        },
614                        $doc = doc);
616 ignore_function_for_get_caller_source_code_reference("define_page_mode");
619 define_variable("auto_mode_list", [],
620     "A list of mappings from URI regular expressions to page modes.");
622 function page_mode_auto_update (buffer) {
623     var uri = buffer.current_URI.spec;
624     var mode = predicate_alist_match(auto_mode_list, uri);
625     if (mode)
626         mode(buffer, true);
627     else if (buffer.page_mode)
628         conkeror[buffer.page_mode](buffer, false);
631 add_hook("content_buffer_location_change_hook", page_mode_auto_update);