Sunday, January 22, 2006

MacOS X menus are a bit less mystic to me after perusing the web and developer.apple.com for clues (and, of course, after writing globs of Objective-C++ code). In the end, it appears there are two classes of menu -- MacOS X-specific, and application specific -- with some blurring between the two mixed in.

In terms of MacOS X-specific menus, applications need to worry about the following:

-- creation of the menubar,
-- creation of the Apple (application) menu,
-- creation of the Window menu

All MacOS X applications must have an Apple menu, and a Window menu. A number of interfaces/methods into NSApplication exist to support these two menus (as well as related submenus, as we shall see).

The menubar is an instance of NSMenu. All one needs to do is allocate an instance of NSMenu, and then pass the result to NSApplication setMainMenu:, as in the following code:

NSMenu *m_menuMain = [[NSMenu alloc] initWithTitle: @""];
[NSApp setMainMenu: m_menuMain];

The menubar acts as a container for the application menus, including the Apple menu and the Window menu, discussed below.

An Apple menu has the following core attributes:
-- a name, which is assigned by MacOS X based on information present in the application bundle (Info.plist file),
-- (optionally) an About... menu item,
-- (optionally) a Preferences... menu item,
-- a Services menu item, which parents a services menu,
-- Hide, Hide Others, and Show All menu items (which cause the application windows to hide, the windows of all other applications to hide, and all windows (application and otherwise) to show, respectively,
-- a Quit menu item, which causes the NSApplication object to fall out of its event processing loop (and the application to exit)

To create the Apple menu, one does the following:
-- create an instance of NSMenu to represent the Apple menu (here I will assume it is stored in a variable name menuApp). The title of the menu can be set to anything, since OS X will override it, as described above,
-- add an instance of NSMenuItem to menuApp with the title "Preferences...",
-- add a separator menuitem.
So far, the code looks like this:

NSMenu *menuApp = [[NSMenu alloc] initWithTitle: @"Apple Menu"];

[menuApp addItemWithTitle:@"Preferences..." action:nil keyEquivalent:@""];
[menuApp addItem: [NSMenuItem separatorItem]];

Notice that the action: associated with the preferences menu item is nil. A preferences menuitem is not required. After all, if your application has no preferences, having that menu item is pointless. More on this later.

The next step is the creation of the Services submenu. Most of the menu items in the Services menu, once it is added to the application, are filled in by MacOS X. At this stage, I know how to create one and add it to the Apple menu. More details on exactly what an application may want to add to this menu will likely emerge, but for now, here are the bare-minimum steps:
-- create an instance of NSMenu to represent the services menu, and give it the title "Services"
-- invoke the NSApplication instances setServicesMenu: method, passing the services menu instance as an argument.
-- create an instance of NSMenuItem and give it a title "Services"
-- invoke the above NSMenuItem's setSubMenu: method, passing the services menu instance as an argrument.
-- add the menuItem to the apple menu.

Here is the code for the above:

NSMenu *menuServices = [[NSMenu alloc] initWithTitle: @"Services"];
[NSApp setServicesMenu:menuServices];

menuitem = [[NSMenuItem alloc] initWithTitle: @"Services"
action:nil keyEquivalent:@""];
[menuitem setSubmenu:menuServices];
[menuApp addItem: menuitem];
[menuitem release];

Next we have the Hide, Hide Others, and Show All menu items. Steps follow a pattern that should now be evident:
-- (optionally) add a separator menu item to group related menuitems,
-- create an instance of NSMenuItem, using initWithTitle to set the title, action: to set the action that will be invoked when the menuitem is selected by the user, and set the key equivalent. Speaking of key equivalents, these come in two forms: using a lowercase letter corresponds to clover-key, while an uppercase key corresponds to shift clover-key,
-- add the menuitem to the menu

An additional step is to assign a target object to handle the menu item. This is done by invoking the menuitem's setTarget: method, and passing that object as the argument.

With that, here is the code for the Hide, Hide Others, and Show All menu items. Notice the action arguments specify methods implemented by NSApplication, and setTarget: binds these to the NSApplication instance.

[menuApp addItem: [NSMenuItem separatorItem]];
menuitem = [[NSMenuItem alloc] initWithTitle:@"Hide"
action:@selector(hide:) keyEquivalent:@""];
[menuitem setTarget: NSApp];
[menuApp addItem: menuitem];
[menuitem release];
menuitem = [[NSMenuItem alloc] initWithTitle:@"Hide Others"
action:@selector(hideOtherApplications:) keyEquivalent:@""];
[menuitem setTarget: NSApp];
[menuApp addItem: menuitem];
[menuitem release];
menuitem = [[NSMenuItem alloc] initWithTitle:@"Show All"
action:@selector(unhideAllApplications:) keyEquivalent:@""];
[menuitem setTarget: NSApp];
[menuApp addItem: menuitem];
[menuitem release];

And, finally, here is the code for creating the Quit menuitem:

[menuApp addItem: [NSMenuItem separatorItem]];
menuitem = [[NSMenuItem alloc] initWithTitle:@"Quit"
action:@selector(terminate:) keyEquivalent:@"q"];
[menuitem setTarget: NSApp];
[menuApp addItem: menuitem];
[menuitem release];

Once we have the above code, all that remains is telling NSApplication that the above is our Apple menu, and then adding the menu to the menubar. The first step is done by making a call to an undocumented function, setAppleMenu:. Here is the code:

[NSApp setAppleMenu:menuApp];

As of MacOS X 10.4 (Tiger), setAppleMenu: was removed from Cocoa header files, and this may lead you to a compiler error. To get around the compiler error, one can add the following code prior to usage:

@interface NSApplication(NSWindowsMenu)
- (void)setAppleMenu:(NSMenu *)aMenu;
@end

This code may generate a warning, but should get you past the error (which is fatal).

To add the menubar, I coded up the following routine, which is also used to add the Window menu to the menubar:

PRStatus
CocoaMenubarImpl::AddToMenubar(NSMenu *menu)
{
NSMenuItem *dummyItem = [[NSMenuItem alloc] initWithTitle:@""
action:nil keyEquivalent:@""];
[dummyItem setSubmenu:menu];
[m_menubar addItem:dummyItem];
[dummyItem release];
return PR_SUCCESS;
}

As strange as that code looks, it works. It effectively creates a dummy menuitem adds that menuitem to the menubar, and then makes the menu you are adding a submenu of the menuitem that was added. This makes sense, since the menubar is a menu, the File menu, the Edit menu, and so on, are menuitems that have a submenu, which is the menu you are adding.



The Window menu is pretty much the same idea. The Window menu has two menu items, Minimize and Bring All to Front, which are bound to the performMiniaturize: and arrangeInFront: actions of the NSApplication instance, respectively. The Window menu is bound to the NSApplication object using NSApplication's setWindowsMenu: method. As with the Apple menu, AddToMenubar() is called to add the Window menu to the menubar. Here is the code for your perusal:

PRStatus
CocoaMenubarImpl::CreateWindowMenu()
{
NSMenu *menuWindows = [[NSMenu alloc] initWithTitle: @"Window"];

[menuWindows addItemWithTitle:@"Minimize"
action:@selector(performMiniaturize:) keyEquivalent:@""];
[menuWindows addItem: [NSMenuItem separatorItem]];
[menuWindows addItemWithTitle:@"Bring All to Front"
action:@selector(arrangeInFront:) keyEquivalent:@""];

[NSApp setWindowsMenu:menuWindows];
AddToMenubar(menuWindows);
[menuWindows release];
return PR_SUCCESS;
}






With the above knowledge, I modified the Cocoa concrete menubar implementation CocoaMenubarImpl so that it creates a menubar that, by default, contains an Apple menu and a Window menu. Here is the complete code for this implementation (some of which has already been presented):

PRStatus CocoaMenubarImpl::Create()
{
m_menubar = [[NSMenu alloc] initWithTitle: @""];
if (m_menubar) {
[NSApp setMainMenu: m_menubar];
CreateAppleMenu();
CreateWindowMenu();
return PR_SUCCESS;
}
return PR_FAILURE;
}

PRStatus
CocoaMenubarImpl::AddToMenubar(NSMenu *menu)
{
NSMenuItem *dummyItem = [[NSMenuItem alloc] initWithTitle:@""
action:nil keyEquivalent:@""];
[dummyItem setSubmenu:menu];
[m_menubar addItem:dummyItem];
[dummyItem release];
return PR_SUCCESS;
}

PRStatus
CocoaMenubarImpl::CreateAppleMenu()
{
NSMenuItem *menuitem;
// Create the application (Apple) menu.
NSMenu *menuApp = [[NSMenu alloc] initWithTitle: @"Apple Menu"];

NSMenu *menuServices = [[NSMenu alloc] initWithTitle: @"Services"];
[NSApp setServicesMenu:menuServices];

[menuApp addItemWithTitle:@"Preferences..." action:nil keyEquivalent:@""];
[menuApp addItem: [NSMenuItem separatorItem]];
menuitem = [[NSMenuItem alloc] initWithTitle: @"Services"
action:nil keyEquivalent:@""];
[menuitem setSubmenu:menuServices];
[menuApp addItem: menuitem];
[menuitem release];
[menuApp addItem: [NSMenuItem separatorItem]];
menuitem = [[NSMenuItem alloc] initWithTitle:@"Hide"
action:@selector(hide:) keyEquivalent:@""];
[menuitem setTarget: NSApp];
[menuApp addItem: menuitem];
[menuitem release];
menuitem = [[NSMenuItem alloc] initWithTitle:@"Hide Others"
action:@selector(hideOtherApplications:) keyEquivalent:@""];
[menuitem setTarget: NSApp];
[menuApp addItem: menuitem];
[menuitem release];
menuitem = [[NSMenuItem alloc] initWithTitle:@"Show All"
action:@selector(unhideAllApplications:) keyEquivalent:@""];
[menuitem setTarget: NSApp];
[menuApp addItem: menuitem];
[menuitem release];
[menuApp addItem: [NSMenuItem separatorItem]];
menuitem = [[NSMenuItem alloc] initWithTitle:@"Quit"
action:@selector(terminate:) keyEquivalent:@"q"];
[menuitem setTarget: NSApp];
[menuApp addItem: menuitem];
[menuitem release];

[NSApp setAppleMenu:menuApp];
AddToMenubar(menuApp);
[menuApp release];
return PR_SUCCESS;
}

PRStatus
CocoaMenubarImpl::CreateWindowMenu()
{
NSMenu *menuWindows = [[NSMenu alloc] initWithTitle: @"Window"];

[menuWindows addItemWithTitle:@"Minimize"
action:@selector(performMiniaturize:) keyEquivalent:@""];
[menuWindows addItem: [NSMenuItem separatorItem]];
[menuWindows addItemWithTitle:@"Bring All to Front"
action:@selector(arrangeInFront:) keyEquivalent:@""];

[NSApp setWindowsMenu:menuWindows];
AddToMenubar(menuWindows);
[menuWindows release];
return PR_SUCCESS;
}

By adding a <menubar> tag to the XUL markup, I now get a functional menubar with the desired Apple and Window menu at runtime. As can be seen in the earlier screenshots, the Apple menu is given the name of the executable (layout is the name of the binary that currently implements the runtime engine), and the MacOS X runtime has added the window named "Hello World App" to the Window menu. All of the menu items in the Window menu are functional without any additional code.

This isn't the end of the story, by any means -- I still need to experiment with the implementation of File and Edit menus, which most applications have. These menus are not MacOS X-specific, and represent the second class of menus identified at the beginning of this article. Also, adding the Window menu at the time the menubar is created may not work -- I may need to do it after all the user menus have been added, since the menus are listed in the order they are added to the menubar, and the Window menu usually follows the application-specific class of menus. And I haven't yet investigated the Help menu, how to handle selection of the Preferences menuitem, or have looked into adding an About... menu item to the Apple menu.