Search code examples
javascriptfirefox-addonfirefox-addon-restartless

In a Firefox restartless add-on, how do I run code when a new window opens (listen for window open)?


I am starting to build a restartless Firefox add-on and I am having trouble setting up the bootstrap.js. Everyone seems to agree that the core of a bootstrap.js is pretty much boilerplate code, along these lines:

const Cc = Components.classes;
const Ci = Components.interfaces;

function startup() {
  let wm = Cc["@mozilla.org/appshell/window-mediator;1"].getService(Ci.nsIWindowMediator);
  let windows = wm.getEnumerator("navigator:browser");
  while (windows.hasMoreElements()) {
    let domWindow = windows.getNext().QueryInterface(Ci.nsIDOMWindow); 
    // then can control what happens with domWindow.document
  } 
}
function shutdown() {}
function install() {}
function uninstall() {}

This code works and I can control things in the existing windows. For example, domWindow.alert("text") successfully creates a standard alert saying "text" on every window that is currently open.

However, I can't find any code that will allow me to do things in new windows; i.e. those created after the script runs. What is the correct way to handle the creation of new windows and gain control over them, to the point where I could get another "text" alert from one when it is created?

Edit: Using the nsWindowMediator class and the code sample from MDN, I now have this:

var windowListener = {
onOpenWindow: function (aWindow) {
  try {
    let domWindow = aWindow.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowInternal || Ci.nsIDOMWindow);
    domWindow.addEventListener("load", function () {
      domWindow.removeEventListener("load", arguments.callee, false);
      //window has now loaded now do stuff to it
      domWindow.alert("text");
    }, false);
  } catch (err) {
    Services.prompt.alert(null, "Error", err);
  }
},
onCloseWindow: function (aWindow) {},
onWindowTitleChange: function (aWindow, aTitle) {}
};

function startup(aData, aReason) {
  // Load into any existing windows
  try {
    let wm = Cc["@mozilla.org/appshell/window-mediator;1"].getService(Ci.nsIWindowMediator);
    let windows = wm.getEnumerator("navigator:browser");
    while (windows.hasMoreElements()) {
      let domWindow = windows.getNext().QueryInterface(Ci.nsIDOMWindow);
      loadIntoWindow(domWindow);
    }
  } catch (err) {
    Services.prompt.alert(null, "Error", err);
  }

  Services.wm.addListener(windowListener);
}

However, there is still no output from the onOpenWindow call - the "text" alert does not appear, nor does the error alert in the catch block. I can confirm that onOpenWindow is actually being entered; if I put a Services.prompt.alert() at the beginning of onOpenWindow, I get the alert when I create a new window. Unfortunately, I get an infinite loop of alerts and I have no idea why.


Solution

  • However, I can't find any code that will allow me to do things in new windows; i.e. those created after the script runs. What is the correct way to handle the creation of new windows and gain control over them, to the point where I could get another "text" alert from one when it is created?

    The correct way to act on each window when it opens is to use addListener() from nsIWindowMediator. The example code below does this. The nsIWindowMediator is included in Services.jsm and is accessed through Services.wm.addListener(WindowListener). In order to use a window listener, you have to pass it an nsIWindowMediatorListener (ref2) object. An nsIWindowMediatorListener contains three keys: onOpenWindow, onCloseWindow, and onWindowTitleChange. Each should be defined as a function which will be called when the appropriate event occurs.

    The MDN document How to convert an overlay extension to restartless in "Step 9: bootstrap.js" contains an example of a basic bootstrap.js which will run the code in the function loadIntoWindow(window) for each currently open browser window and any browser window which opens in the future. I have used code modified from this in a couple of different add-ons. The example is substantially similar to the code you are already using. The example is (slightly modified):

    const Ci = Components.interfaces;
    
    Components.utils.import("resource://gre/modules/Services.jsm");
    
    function startup(data,reason) {
        // Load this add-ons module(s):
        Components.utils.import("chrome://myAddon/content/myModule.jsm");
        // Do whatever initial startup stuff is needed for this add-on.
        //   Code is in module just loaded.
        myModule.startup();  
    
        // Make changes to the Firefox UI to hook in this add-on
        forEachOpenWindow(loadIntoWindow);
        // Listen for any windows that open in the future
        Services.wm.addListener(WindowListener);
    }
    
    function shutdown(data,reason) {
        if (reason == APP_SHUTDOWN)
            return;
    
        // Unload the UI from each window
        forEachOpenWindow(unloadFromWindow);
        // Stop listening for new windows to open.
        Services.wm.removeListener(WindowListener);
    
        // Do whatever shutdown stuff you need to do on add-on disable
        myModule.shutdown();  
    
        // Unload the module(s) loaded specific to this extension.
        // Use the same URL for your module(s) as when loaded:
        Components.utils.unload("chrome://myAddon/content/myModule.jsm"); 
    
        // HACK WARNING: The Addon Manager does not properly clear all add-on related caches
        //               on update. In order to fully update images and locales, their
        //               caches need clearing here.
        Services.obs.notifyObservers(null, "chrome-flush-caches", null);
    }
    
    function install(data,reason) { }
    
    function uninstall(data,reason) { }
    
    function loadIntoWindow(window) {
        /* call/move your UI construction function here */
    }
    
    function unloadFromWindow(window) {
        /* call/move your UI tear down function here */
    }
    
    function forEachOpenWindow(todo) {
        // Apply a function to all open browser windows
        var windows = Services.wm.getEnumerator("navigator:browser");
        while (windows.hasMoreElements())
            todo(windows.getNext().QueryInterface(Ci.nsIDOMWindow));
    }
    
    var WindowListener = {
        onOpenWindow: function(xulWindow) {
            var window = xulWindow.QueryInterface(Ci.nsIInterfaceRequestor)
                                  .getInterface(Ci.nsIDOMWindow);
            function onWindowLoad() {
                window.removeEventListener("load",onWindowLoad);
                // Only add UI changes if this is a browser window
                if (window.document.documentElement.getAttribute("windowtype") 
                                                                    == "navigator:browser")
                    loadIntoWindow(window);
            }
            window.addEventListener("load",onWindowLoad);
        },
        onCloseWindow: function(xulWindow) { },
        onWindowTitleChange: function(xulWindow, newTitle) { }
    };
    

    While there is quite a bit more that your might want to do in your bootstrap.js code, the above is organized reasonably well and keeps all of the code to load into the Firefox UI within loadIntoWindow(window) and unloading the UI within unloadFromWindow(window). However, it should be noted that some UI elements you should only be adding/removing once (e.g. australis widgets, like buttons) and other elements (e.g. direct changes to the Firefox DOM) have to be added once in each window.

    Unfortunately, I get an infinite loop of alerts and I have no idea why.

    One of the significant differences between this example and what you are currently using is the test for the type of window that has opened. This is done so that we are only acting on newly opened windows which are browser windows instead of all newly opened windows:

    if (window.document.documentElement.getAttribute("windowtype") == "navigator:browser")
        loadIntoWindow(window);
    

    The problem you describe of getting an infinite loop of alert() popups is caused by not checking to make sure that you are only acting on browser windows. The alert() popup is a window. Thus, you are calling alert() for every alert() window you open which, of course, just opens another alert() window on which you call alert(). This is your infinite loop.

    Additional references:
    1. Working with windows in chrome code