Saturday, January 14, 2006

Menus represent more of an implementation challenge than perhaps any of the other widgets that have been implemented in the layout engine to this point. While there are many attributes shared by menu implementations on the various platforms we are looking at, there are significant differences as well, some of which have an effect on the architecture of the solution, and an impact on the how a developer specifies menus in markup.

Let's start our discussion of cross-platform menus by listing the cross-platform and platform-specific attributes of menus.

Cross Platform Menu Attributes

Menus have a name. The name of the menu identifies the general functionality that is provided by the menu.

Menus (naturally enough) contain menu items. Menu items, like menus, have names that are used to identify the functionality provided by the menu item. Examples of menu items are Open, Close, Copy, and Paste.

Applications invariably provide a File menu that contains some of the following menu items: New, used to create new documents, or content; Open, used to open existing documents located on persistent storage; Close, used to close the currently open document; Save, used to save the currently open document to persistent storage; Save As, used to select a file and save the currently open document to that file; Exit or Quit, used to close all open documents, and terminate the application; and Print (and possibly Print Setup), which can be used to configure and print content displayed in the currently active document. The File menu is always displayed as the leftmost menu in the menubar (except on MacOS X, see below).

Applications may, depending on their requirements, provide an Edit menu that contains some or all of the following menu items: Cut, used to remove selected content from the currently open document, and place it in the clipboard; Copy, to copy selected content from the currently open document, and place it in the clipboard; Paste, to paste the contents of the clipboard into the currently active document; and Select All, which selects all of the content managed by whatever widget currently has the input focus; Find and Find Next, which are used to find content in the current document.

Menus are organized horizontally in a container object known as a menu bar. Menu items are organized vertically in a menu.

Menus may, depending on their content, contain separators. A separator is a special instance of a menu item that, instead of displaying a label, displays a horizontal line. Unlike other menu items, a separator cannot be selected. Separators are used to define (and separate) related groups of menu items.

Menu items may be associated with what is known (depending on who is describing them) as either a shortcut, a keyboard equivalent, or an accelerator (I'll refer to them here as shortcuts). A shortcut consists of one or more keys, usually prefixed by a special trigger key (e.g., ctrl) that is shared by all shortcuts. Whenever the keys associated with a shortcut are pressed by the user, the associated menu item is activated. For many, use of shortcuts is much more efficient than moving the mouse to a menu, opening the menu, locating and clicking on the menu item. Invariably, shortcuts are displayed in the menu along with the title of the associated menu item; doing so documents the shortcuts that are supported by the application. Shortcuts are never associated with separators.

Menu items can be active (enabled) or inactive (disabled). Inactive menu items are displayed in a dimmed font in menus, to communicate their inactive state to the user.

Menu items are associated with a command. When the menu item is selected by the user, the command is executed.

Now that we have looked at the plaform-independent attributes of menus, let's look at those attributes which are platform-specific.

Platform-Specific Menu Attributes

In Gtk+, menus can be torn away from the menu bar. Once torn away, the menu displays in its own window. There are some cases where tear-away menus are effective, but these cases can (in my opinion) be better satisfied by displaying the functionality of the menu in a floating toolbox/toolbar window.

On both Linux (Gtk+) and Windows, shortcuts are prefixed by the ctrl key. On MacOS X, the apple/clover key is used to prefix shortcuts.

Each toolkit/platform supports some for of help system, dictating what menu items are included in a Help menu, what shortcuts are used to access these menu items, and what actions result when menu items are selected by the user.

The same can be said about printing; each system implements a different mechanism for supporting printing and print setup.

MacOS X displays menus in a single menubar located on the desktop, while menubars in Linux and Windows are associated with windows. Since only one window can have the focus on either of these platforms, the difference is largely irrelevant.

What is relevant on MacOS is the "application" menu, which displays to the right of the system-managed Apple menu, and to the left of the application's File menu. The name assigned to the application menu is the name of the application, e.g., Firefox is the name of the application menu defined by the Firefox web browser. The MacOS X application menu has, by convention, content that conflicts with content typically found in the File, Edit, and Help menus associated with Linux and Windows applications:

-- The first menu item in the application menu is About (application name). This menu item would be found typically in the Help menu on the other platforms.

-- The preferences menu, normally found in the Edit menu in Linux and Windows applications, is located in the application menu on MacOS X.

-- The application menu contains a Quit menu item, which conflicts with the Quit (or Exit) menu item that would be located in the File menu on Linux or Windows.

MacOS X also requires applications to provide a Window menu. The Window menu contains two items at the top of the menu -- Minimize and Zoom -- which correspond the the minimize and zoom controls displayed at the extreme left edge of the window title bar. The other items in the Window menu can be used to select the application's active window from among windows already being displayed by the application, or predefined windows not yet visible (depending upon the design of the application).

Some of these differences, as I implied, may have an impact on markup used to define the menus. As a starting point, let's take a look at how Mozilla/Firefox defines the menus for the browser application. The markup I am going to discuss here can be found at http://lxr.mozilla.org/seamonkey/source/browser/base/content/browser-menubar.inc -- the line numbers may differ depending on changes made by Mozilla contributors. Below, I will copy and paste relevant sections of the content discussed, which should minimize the impact of such changes.

First off, the above file is actually an include; it is included by browser.xul (which defines the markup for the browser application window).

The following is the markup from browser-menubar.inc for the Edit menu:

83 <menu id="edit-menu" label="&editMenu.label;"
84 accesskey="&editMenu.accesskey;">
85 <menupopup id="menu_EditPopup">
86 <menuitem label="&undoCmd.label;"
87 key="key_undo"
88 accesskey="&undoCmd.accesskey;"
89 command="cmd_undo"/>
90 <menuitem label="&redoCmd.label;"
91 key="key_redo"
92 accesskey="&redoCmd.accesskey;"
93 command="cmd_redo"/>
94 <menuseparator/>
95 <menuitem label="&cutCmd.label;"
96 key="key_cut"
97 accesskey="&cutCmd.accesskey;"
98 command="cmd_cut"/>
99 <menuitem label="©Cmd.label;"
100 key="key_copy"
101 accesskey="©Cmd.accesskey;"
102 command="cmd_copy"/>
103 <menuitem label="&pasteCmd.label;"
104 key="key_paste"
105 accesskey="&pasteCmd.accesskey;"
106 command="cmd_paste"/>
107 <menuitem label="&deleteCmd.label;"
108 key="key_delete"
109 accesskey="&deleteCmd.accesskey;"
110 command="cmd_delete"/>
111 <menuseparator/>
112 <menuitem label="&selectAllCmd.label;"
113 key="key_selectAll"
114 accesskey="&selectAllCmd.accesskey;"
115 command="cmd_selectAll"/>
116 <menuseparator/>
117 <menuitem id="menu_find" label="&findOnCmd.label;" accesskey="&findOnCmd.accesskey;" key="key_find" command="cmd_find"/>
118 <menuitem label="&findAgainCmd.label;" accesskey="&findAgainCmd.accesskey;" key="key_findAgain" command="cmd_findAgain"/>
119 <menuseparator hidden="true" id="textfieldDirection-separator"/>
120 <menuitem id="textfieldDirection-swap"
121 command="cmd_switchTextDirection"
122 key="key_switchTextDirection"
123 label="&bidiSwitchTextDirectionItem.label;"
124 accesskey="&bidiSwitchTextDirectionItem.accesskey;"
125 hidden="true"/>
126 #ifdef XP_UNIX
127 #ifndef XP_MACOSX
128 <menuseparator/>
129 <menuitem id="menu_preferences"
130 label="&preferencesCmdUnix.label;"
131 accesskey="&preferencesCmdUnix.accesskey;"
132 oncommand="openPreferences();"/>
133 #endif
134 #endif
135 </menupopup>
136 </menu>

As you can see, the layout is very intuitive. On line 83, the menu element defines the Edit menu, and assigns it a name. menuitem tags are, naturally enough, children of the menu element, and define the items that are contained in the menu. menuitem tags have the same basic attributes as a menu, in addition to a command attribute that defines the code that is to be executed if the user selects that menuitem.

Also, as you can see, the code (starting around line 126) contains #ifdefs. These #ifdefs are used to add a Preferences menu item to the Edit menu on Unix systems (except for MacOS X, which the Mozilla build system also defines the XP_UNIX #define at compile time, since MacOS X, internally, is Unix-based). On MacOS X, preferences are located in the application menu, as was described earlier. On Windows, preferences are accessible in the Tools menu, using the Options menu item (all platforms have a Tools menu, but only the Windows version of Firefox provides an Options menu item in the Tools menu).

So, we see in Mozilla one way that menus can be made platform-specific -- use #ifdefs in the markup. Perhaps the low frequency of use -- very few other places in markup need this particular strategy -- makes the use of #ifdefs acceptable. Yet, when abused, #ifdefs are never a very good idea. On the otherhand, perhaps there is another way to do this that avoids #ifdefs, and I will spend a good bit of time looking for another way as I try and come up with a menu implementation for my layout engine.

As you can see, entities are used to define commandkey attributes (shortcuts). This implies that there are dtd files that are platform-specific. Using lxr to identify the definition of one of these entities, &printCmd.commandkey;, we see the following from browser.dtd:

24 <!ENTITY printCmd.commandkey "p">

Thus, there is some logic (perhaps outside of the XML parser) that, when adding items to the menu, concatenates the commandkey specified in markup with its prefix character (e.g., ctrl on Unix, or clover on MacOS X). I'll use that same strategy in the concrete menu classes that do the work of building the menus from markup.

Finally, there is the issue of how to deal with platform-specific behavior (the print and help commands, and so forth). This must be handled by providing platform-specific handlers that can be called from Javascript. Where this functionality lives is an issue that must still be considered, and there are two options. If the functionality is toolkit-specific, versus application-dependent, then a helper class (or a library of classes) registered as an object with the Javascript engine, and provided by the toolkit, would be an appropriate solution. Otherwise, platform-specific functionality can be provided by the application/component developer. The component library, of course, can abstract platform dependencies in the same ways that the layout engine does (e.g., by using factories).

As to supporting the MacOS X differences such as the application menu, and the Window menu, these are issues that will need to be considered in the design. Hopefully I will have some details in my next post.