Adding menus and submenus

The Hello World example in the previous sections shows the two most common ways to add menus. In this section we'll look into more specialized menus and what you can do with them.

Adding a New Menu

We already saw how to add menus in overlays and, as you may have imagined, you can nest submenus as deep as you want. You should avoid having deep menus or too many options, since they are confusing for most users.

If your extension requires custom XUL windows, you may also need to have menus on those windows. You can do this with a menubar. The menubar element should be a child of a toolbox element because it is treated like another toolbar on systems other than Mac OS X.

Mac OS X treats menus in a very different way than other systems. If your extension involves menus in any way, you should test it on Mac OS X to make sure everything works adequately.

The toolbox should be positioned near the top of the XUL document, and the code should be similar to this:

<toolbox>
  <menubar id="xulschoolhello-menubar">
    <menu id="xulschoolhello-greeting-menu" label="&xulschoolhello.greeting.label;">
      <menupopup>
        <menuitem label="&xulschoolhello.greet.short.label;"
          oncommand="XULSchoolChrome.GreetingDialog.greetingShort(event);" />
        <menuitem label="&xulschoolhello.greet.medium.label;"
          oncommand="XULSchoolChrome.GreetingDialog.greetingMedium(event);" />
        <menuitem label="&xulschoolhello.greet.long.label;"
          oncommand="XULSchoolChrome.GreetingDialog.greetingLong(event);" />
        <menuseparator />
        <menuitem label="&xulschoolhello.greet.custom.label;"
          oncommand="XULSchoolChrome.GreetingDialog.greetingCustom(event);" />
      </menupopup>
    </menu>
  </menubar>
</toolbox> 

This code displays a simple menu with options for 3 different types of greetings, a menuseparator, and finally an option to show a custom greeting. The separator is usually displayed as a horizontal line that creates a logical division between different types of menuitem elements, keeping everything more organized.

A menubar can hold one or more menu elements. Menus require a menupopup element as a container for its children, which are usually menuitem elements, but can also be menuseparator, or menu in order to have multiple nesting levels:

<toolbox>
 <menubar id="xulschoolhello-menubar">
    <menu id="xulschoolhello-greeting-menu" label="&xulschoolhello.greeting.label;">
      <menupopup>
        <menu id="xulschoolhello-greeting-sizes-menu" label="&xulschoolhello.greetingSizes.label;">
          <menupopup>
            <menuitem label="&xulschoolhello.greet.short.label;"
              oncommand="XULSchoolChrome.GreetingDialog.greetingShort(event);" />
            <menuitem label="&xulschoolhello.greet.medium.label;"
              oncommand="XULSchoolChrome.GreetingDialog.greetingMedium(event);" />
            <menuitem label="&xulschoolhello.greet.long.label;"
              oncommand="XULSchoolChrome.GreetingDialog.greetingLong(event);" />
          </menupopup>
        </menu>
        <menuitem label="&xulschoolhello.greet.custom.label;"
          oncommand="XULSchoolChrome.GreetingDialog.greetingCustom(event);" />
      </menupopup>
    </menu>
  </menubar>
</toolbox> 

In this case we grouped the 3 greeting items into a submenu. It doesn't make much sense to do that in this case because we end up with only two menu items, one of them being a menu with 3 child items.

You can also have menus that are filled dynamically. Instead of setting the menupopup directly in the XUL, you can use the onpopupshowing event to fill the children when the popup is about to be displayed. DOM functions like createElement and appendChild can be used to accomplish this.

If you have nothing to show on a menu, you should follow the standard used in Firefox: show a single disabled item with an "(Empty)" label.

If filling your menu takes a noticeable amount of time, you should not make Firefox (and your users) wait for it to fill up before displaying anything. It's best to show an item with a throbber image (see chrome://global/skin/icons/loading_16.png) so the user knows there's something going on, and asynchronously fill its contents. We'll look into some asynchronous techniques further ahead in the tutorial.

Adding Elements to Existing Menus

Just as explained in the previous sections, the best place to overlay your extension menu is inside the Tools menu. That is, unless there's a place inside the menu structure where your extension menus make more sense. If you're overlaying the Tools menu, your overlay code should have something like this:

<menupopup id="menu_ToolsPopup">
  <menu id="xulschoolhello-hello-menu" label="&xulschoolhello.hello.label;"
    accesskey="&xulschoolhello.helloMenu.accesskey;"
    insertafter="javascriptConsole,devToolsSeparator">
    <menupopup>
      <!-- Your menuitem goes here. -->
    </menupopup>
  </menu>
</menupopup> 

Now let's look at some specialized types of menu items.

Checkbox Menu Items

You can make a menuitem "checkable" to allow the user to enable/disable options using the menu. We use two attributes for this: type and checked. The type attribute must be set to "checkbox". You can set the checked attribute to "true" to check it by default.

The item's checked state changes when the user clicks on it. An example of one such item is the View > Status Bar item in the main Firefox menu.

Radio Menu Items

If you need to have a set of menuitem elements where only one of them has to be checked at any given moment, you should set the type to "radio". The name attribute is used to identify the items that belong to the radio group.

<menupopup oncommand="XULSchoolChrome.HW.GreetingDialog.greeting(event);">
  <menuitem type="radio" name="xulschoolhello-greeting-radio"
    label="&xulschoolhello.greet.short.label;" checked="true" />
  <menuitem type="radio" name="xulschoolhello-greeting-radio"
    label="&xulschoolhello.greet.medium.label;" />
  <menuitem type="radio" name="xulschoolhello-greeting-radio"
    label="&xulschoolhello.greet.long.label;" />
</menupopup> 

This is a modified version of the 3 greeting menus. It is now implemented as a radio menu where you pick one of the 3 available choices. The first one is checked by default. The oncommand attribute is set on the menupopup to avoid code duplication, since now the 3 items call the same function.

Another example of a menu like this is the View > Sidebars menu. Only one sidebar is visible at any given moment, and you can pick from several.

To add an icon to a menu or menuitem, set its class to "menu-iconic" or "menuitem-iconic" respectively, and set the image attribute or the list-style-image CSS property. Menu icons are typically 16px by 16px.

As mentioned earlier, menus are very different on Mac OS X. This is because menus on Mac are located in a single menu bar which is controlled by the operating system, as opposed to menus in other systems, which are entirely controlled by Firefox. Mac OS X also has menu standards, such as the positioning of certain items that are not used in other systems. Here's a list of the known issues we've run into when handling menus on Mac:

  • The About, Preferences and Quit menu items are located under the "Firefox" menu, not the usual places you would find them. You can access these items by id through the DOM, but their parent menu is not easily accessible.
  • We've run into bugs when adding, removing, enabling and disabling menu items dynamically, specially the root menu items (File, Edit, View, etc). You should carefully test this behavior to make sure it works properly in your extension.
  • Images in menu items may not appear, showing only a narrow segment of the image instead. This seems to happen when remote images are used.
  • Menu items are not dynamically updated while they are open. For example, you could have a menuitem that tells you the current time and is updated every second. On other systems you would be able to see the item update itself without having to close the menu and then reopen. This is not the case on Mac OS.

This tutorial was kindly donated to Mozilla by Appcoast.