Search code examples
javascriptfirefox-addonxul

How to create a bottom-docked panel with XUL in Firefox?


For my addon, I would like to create a bottom-docked "panel" that will display information. An example of something like this would be Firefox's own Web Console panel that can be toggled under Web Developer Tools.

I tried to dig through the code, but couldn't figure out how this was implemented. Could someone give me a a basic explanation of how to create this with XUL, or point me in the right direction?


Solution

  • The Web Console is not a sidebar. In Fireox there is only one sidebar, it can be on the left or right of the browser content. The sidebar is a constant part of the UI, even if you change tabs. It is generally used for content, History, Bookmarks, or other such information that does not change depending on the tab you are viewing.

    For investigating stuff like this, I would suggest the DOM Inspector add-on, if you do not already have it installed.

    The Web Console is contained in an <iframe> within the <tabbrowser> under the tab in which it is located.

    The XUL for the iframe is: <iframe xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" class="devtools-toolbox-bottom-iframe" height="436" tooltip="aHTMLTooltip" src="chrome://browser/content/devtools/framework/toolbox.xul"/>

    First a <splitter> then the <iframe> are inserted after the <hbox class="browserSidebarContainer"> for the tab in which it is contained.

    The web console is opened by a call to gDevToolsBrowser.selectToolCommand(gBrowser, "webconsole");

    gDevToolsBrowser is defined in chrome://browser/content/browser.js from the contents of resource:///modules/devtools/gDevTools.jsm

    They are actually created in the function SH_create within resource:///modules/devtools/framework/toolbox-hosts.js

    All of these chrome:// and resource:/// URLs should work from within Firefox. Firefox installs have a large number of files packed in three files cales omni.ja. The files are located in <install directory>/omni.ja, <install directory>/browser/omni.ja, and <install directory>/webart/omni.ja. The omni.ja files are just zip format files with the extension renamed. In order to have easy access to these files I routinely unpack them into directories (outside of the Firefox install directory tree). I find that this makes it easier to do searches and manipulate the files when I desire to figure out how something was done.

    If you were just looking for code that will create a box like is used for the web console, the complexity depends on the context in which you are operating. The following should work from almost anywhere:

    /**
     *   Creates an <iframe> based panel within the current tab,
     *   or opens a window, for use as an user interface box.
     *   If it is not a window, it is associated with the current
     *   browser tab.
     * @param location 
     *        Placement of the panel [right|left|top|bottom|window]
     *        The default location is "right".
     * @param size
     *        Width if on left or right. Height if top or bottom.
     *        Both width and height if location="window" unless
     *        features is a string. 
     *        Default is 400.
     * @param id
     *        The ID to assign to the iframe. Default is
     *        "makyen-interface-panel"
     *        The <splitter> will be assigned the
     *        ID = id + "-splitter"
     * @param chromeUrl
     *        This is the chrome://  URL to use for the contents
     *        of the iframe or the window.
     *        the default is:
     *        "chrome://browser/content/devtools/framework/toolbox.xul"
     * @param features
     *        The features string for the window. See:
     *        https://developer.mozilla.org/en-US/docs/Web/API/Window.open
     * returns [splitterEl, iframeEl]
     *        The elements for the <splitter> and <iframe>
     *
     * Copyright 2014 by Makyen.
     * Released under the MPL 2.0. http://mozilla.org/MPL/2.0/.
     **/
    function createInterfacePanelIframe(location,size,id,chromeUrl,features) {
        //defaults
        size = ( (typeof size !== "number") || size<1) ? 400 : size; 
        id = typeof id !== "string" ? "makyen-interface-panel" : id;
        chromeUrl = typeof chromeUrl !== "string"
            ? "chrome://browser/content/devtools/framework/toolbox.xul"
            : chromeUrl;
    
        //Create some common variables if they do not exist.
        //  This should work from any Firefox context.
        //  Depending on the context in which the function is being run,
        //  this could be simplified.
        if (typeof window === "undefined") {
            //If there is no window defined, get the most recent.
            var window=Components.classes["@mozilla.org/appshell/window-mediator;1"]
                               .getService(Components.interfaces.nsIWindowMediator)
                               .getMostRecentWindow("navigator:browser");
        }
        if (typeof document === "undefined") {
            //If there is no document defined, get it
            var document = window.content.document;
        }
        if (typeof gBrowser === "undefined") {
            //If there is no gBrowser defined, get it
            var gBrowser = window.gBrowser;
        }
    
        //Get the current tab & notification box (container for tab UI).
        let tab = gBrowser.selectedTab;
        let browserForTab = gBrowser.getBrowserForTab( tab );
        let notificationBox = gBrowser.getNotificationBox( browserForTab );
        let ownerDocument = gBrowser.ownerDocument;
    
        //Create the <iframe> use
        //ownerDocument for the XUL namespace.
        let iframeEl = ownerDocument.createElement("iframe");
        iframeEl.id = id;
        iframeEl.setAttribute("src",chromeUrl);
        iframeEl.setAttribute("height", size.toString());
        iframeEl.setAttribute("width", size.toString());
    
        //Call createInterfacePanel, pass the size if it is to be a window.
        let splitterEl;
        if(location == "window" ) {
            splitterEl = createInterfacePanel(location, size, size
                                            ,id + "-splitter", chromeUrl, features);
            return [splitterEl, null];
        } else {
            splitterEl = createInterfacePanel(location, iframeEl, iframeEl
                                            ,id + "-splitter", chromeUrl, features);
        }
        return [splitterEl, iframeEl];
    }
    
    /**
     * Creates a panel within the current tab, or opens a window, for use as a
     *   user interface box. If not a window, it is associated with the current
     *   browser tab.
     * @param location 
     *        Placement of the panel [right|left|top|bottom|window]
     *        The default location is "right".
     * @param objectEl
     *        The element of an XUL object that will be inserted into
     *        the DOM such that it is within the current tab.
     *        Some examples of possible objects are <iframe>,
     *        <browser>, <box>, <hbox>, <vbox>, etc.
     *        If the location="window" and features is not a string
     *        and this is a number then it is used as the width of the
     *        window.
     *        If features is a string, it is assumed the width is
     *        set in that, or elsewhere (e.g. in the XUL).
     * @param sizeEl
     *        The element that contains attributes of "width" and 
     *        "height". If location=="left"|"right" then the 
     *        "height" attribute is removed prior to the objectEl
     *        being inserted into the DOM.
     *        A spearate reference for the size element in case the
     *        objectEl is a documentFragment containing multiple elements.
     *        However, normal usage is for objectEl === sizeEl when
     *        location != "window".
     *        When location == "window" and features is not a string,
     *        and sizeEl is a number then it is used as the height
     *        of the window.
     *        If features is a string, it is assumed the height is
     *        set in that, or elsewhere (e.g. in the XUL).
     * @param id
     *        The ID to assign to the <splitter>. The default is:
     *        "makyen-interface-panel-splitter".
     * @param chromeUrl
     *        This is the chrome://  URL to use for the contents
     *        of the window.
     *        the default is:
     *        "chrome://browser/content/devtools/framework/toolbox.xul"
     * @param features
     *        The features string for the window. See:
     *        https://developer.mozilla.org/en-US/docs/Web/API/Window.open
     * returns
     *        if location != "window":
     *           splitterEl, The element for the <splitter>.
     *        if location == "window":
     *           The windowObjectReference returned by window.open().
     *
     * Copyright 2014 by Makyen.
     * Released under the MPL 2.0. http://mozilla.org/MPL/2.0/.
     **/
    function createInterfacePanel(location,objectEl,sizeEl,id,chromeUrl,features) {
        //Set location default:
        location = typeof location !== "string" ? "right" : location;
        if(location == "window") {
            if(typeof features !== "string") {
                let width = "";
                let height = "";
                if(typeof objectEl == "number") {
                    width = "width=" + objectEl.toString() + ",";
                }
                if(typeof sizeEl == "number") {
                    height = "height=" + sizeEl.toString() + ",";
                }
                features = width + height
                           + "menubar=no,toolbar=no,location=no,personalbar=no"
                           + ",status=no,chrome=yes,resizable,centerscreen";
            }
        }
        id = typeof id !== "string" ? "makyen-interface-panel-splitter" : id;
        chromeUrl = typeof chromeUrl !== "string"
            ? "chrome://browser/content/devtools/framework/toolbox.xul"
            : chromeUrl;
    
        //Create some common variables if they do not exist.
        //  This should work from any Firefox context.
        //  Depending on the context in which the function is being run,
        //  this could be simplified.
        if (typeof window === "undefined") {
            //If there is no window defined, get the most recent.
            var window=Components.classes["@mozilla.org/appshell/window-mediator;1"]
                               .getService(Components.interfaces.nsIWindowMediator)
                               .getMostRecentWindow("navigator:browser");
        }
        if (typeof document === "undefined") {
            //If there is no document defined, get it
            var document = window.content.document;
        }
        if (typeof gBrowser === "undefined") {
            //If there is no gBrowser defined, get it
            var gBrowser = window.gBrowser;
        }
    
        //Get the current tab & notification box (container for tab UI).
        let tab = gBrowser.selectedTab;
        let browserForTab = gBrowser.getBrowserForTab( tab );
        let notificationBox = gBrowser.getNotificationBox( browserForTab );
        let ownerDocument = gBrowser.ownerDocument;
    
    
        //Create a Document Fragment.
        //If doing multiple DOM additions, we should be in the habit
        //  of doing things in a way which causes the least number of reflows.
        //  We know that we are going to add more than one thing, so use a
        //  document fragment.
        let docFrag = ownerDocument.createDocumentFragment();
    
        //ownerDocument must be used here in order to have the XUL namespace
        //  or the splitter causes problems.
        //  createElementNS() does not work here.
        let splitterEl = ownerDocument.createElement("splitter");
        splitterEl.id =  id ;
    
        //Look for the child element with class="browserSidebarContainer".
        //It is the element in procimity to which the <splitter>
        //and objectEl will be placed.
        let theChild = notificationBox.firstChild;
        while (!theChild.hasAttribute("class")
            || !theChild.getAttribute("class").contains("browserSidebarContainer")
        ) {
            theChild = theChild.nextSibling;
            if(!theChild) {
                //We failed to find the correct node.
                //This implies that the structure Firefox
                //  uses has changed significantly and it should 
                //  be assumed that the extension is no longer compatible.
                return null;
            }
        }
    
        let toReturn = null;
        switch(location) {
            case "window"    :
                return window.open(chromeUrl,"_blank",features);
                break;
            case "top"    :
                if(sizeEl) {
                    sizeEl.removeAttribute("width");
                }
                docFrag.appendChild(objectEl);
                docFrag.appendChild(splitterEl);
                //Inserting the document fragment results in the same
                //  DOM structure as if you Inserted each child of the
                //  fragment separately. (i.e. the document fragment
                //  is just a temporary container).
                //Insert the interface prior to theChild.
                toReturn = notificationBox.insertBefore(docFrag,theChild);
                break;
            case "bottom" :
                if(sizeEl) {
                    sizeEl.removeAttribute("width");
                }
                docFrag.appendChild(splitterEl);
                docFrag.appendChild(objectEl);
                //Insert the interface just after theChild.
                toReturn = notificationBox.insertBefore(docFrag,theChild.nextSibling);
                break;
            case "left"   :
                if(sizeEl) {
                    sizeEl.removeAttribute("height");
                }
                docFrag.appendChild(objectEl);
                //Splitter is second in this orientaiton.
                docFrag.appendChild(splitterEl);
                //Insert the interface as the first child of theChild.
                toReturn = theChild.insertBefore(docFrag,theChild.firstChild);
                break;
            case "right"  :
            default       :
                //Right orientaiton, the default.
                if(sizeEl) {
                    sizeEl.removeAttribute("height");
                }
                docFrag.appendChild(splitterEl);
                docFrag.appendChild(objectEl);
                //Insert the interface as the last child of theChild.
                toReturn = theChild.appendChild(docFrag);
                break;
        }
        return splitterEl;
    }
    

    Note: The code in toolbox-hosts.js uses the method getSidebarContainer() to find the container to which to append. There is no documentation on that method, so I used getNotificationBox().