Saturday, February 04, 2006

The menu implementation for Cocoa/MacOS X is largely complete. Best of all, I came up with a strategy that avoids #ifdefs in markup (something which is done in Mozilla's XUL that I find a bit unsavory). In this post, I will try and describe the Cocoa-based menu architecture. In following posts I will comment on changes needed to support Gtk+ on Linux and .NET Forms on Windows (once I know what those changes are, as the work to port this all to Gtk+ and .NET is pending).

Let's start with a look at the XML markup used to describe menus.

<menubar>
<menu id="file-menu" label="File" shortcut="f">
<menuitem class="separator"/>
<menuitem label="Open" shortcut="o" onclick="MenuFileOpen();"/>
<menuitem class="separator"/>
<menuitem label="Close" shortcut="w" class="close" onclick="MenuFileClose();"/>
<menuitem label= "Save" shortcut="s" class="save" onclick="MenuFileSave();"/>
<menuitem label= "Save As..." shortcut="S" class="saveAs" onclick="MenuFileSaveAs();"/>
<menuitem class="separator"/>
<menuitem label="Page Setup..." shortcut="P" class="pageSetup" onclick="MenuFilePageSetup();"/>
<menuitem label="Print" shortcut="p" class="print" onclick="MenuFilePrint();"/>
<menuitem class="separator"/>
<menuitem label="Quit" shortcut= "q" class="quit" onclick="MenuFileQuit();"/>
</menu>
<menu id="edit-menu" label="Edit" shortcut="e">
<menuitem label="Cut" shortcut="x" class="cut" onclick="MenuEditCut();"/>
<menuitem label="Copy" shortcut="c" class="copy" onclick="MenuEditCopy();"/>
<menuitem label="Paste" shortcut="v" class="paste" onclick="MenuEditPaste();"/>
<menuitem label="Clear" class="clear" onclick="MenuEditClear();"/>
<menuitem class="separator"/>
<menuitem label="Select All" shortcut="a" class="selectAll" onclick="MenuEditSelectAll();"/>
<menuitem label="Preferences..." shortcut="," class="preferences" onclick="MenuEditPreferences();"/>
</menu>
</menubar>

As you can see, there are three distinct tags - menubar, menu, and menuitem. The menubar tag is a simple container of menu tags. The menu tag describes each menu (e.g., File, Edit), and acts as a container of menuitems. Finally, menuitem describes clickable items in the containing menu.

Menus and menuitems both implement label and shortcut attributes. On Windows, menu shortcuts are used to activate the corresponding menu, while on MacOS X/Cocoa, a shortcut assigned to the menu is ignored. Menuitem shortcuts are supported by all three platform toolkits. Under Cocoa, the shortcut is case-sensitive; a lowercase letter is used to implement clover-letter, while an uppercase letter implements shift-clover-letter.

The onclick attribute assigns JavaScript code to a menuitem that will be executed when the menuitem is selected by the user.

The class attribute assigns a class to a menuitem. This class tells the layout engine that the menuitem is special in the sense that toolkit specific behaviors are associated with the menu item. When present, the class causes the concrete menuitem implementation to invoke platform specific behavior. The following classes have been implemented so far:

typedef enum
{
MenuItemClassNone,
MenuItemClassSeparator,
MenuItemClassClose,
MenuItemClassSave,
MenuItemClassSaveAs,
MenuItemClassPageSetup,
MenuItemClassPrint,
MenuItemClassQuit,
MenuItemClassCut,
MenuItemClassCopy,
MenuItemClassPaste,
MenuItemClassClear,
MenuItemClassSelectAll,
MenuItemClassPreferences
} MenuItemClass;

As you may notice, these menu items represent functionality closely coupled to behaviors that might be implemented by a toolkit, versus functionality that might be implemented by an application. In Cocoa, there are methods associated with AppKit classes for most of the above functionality. The following talks about the ones I have implemented so far:

MenuItemClassSeparator

In Cocoa, NSMenuItem has an attribute that allows one to identify itself as a separator. Doing so causes the appropriate rendering of the menu item as a separator. Separators are purely cosmetic, and cannot be selected by a user.

MenuItemClassClose

NSWindow implements a close method that will cause the window to close.

MenuItemClassPageSetup

The tookit invokes the following code in response to selection of menuitems of this class. The settings made by the user in the NSPageLayout dialog have global effect on the application:

[[NSPageLayout pageLayout] runModal];

MenuItemClassPrint

Similarly, the Cocoa class NSPrintOperation is used to implement printing, as in the following code:

[[NSPrintOperation printOperationWithView:nsView] runOperation]

printOperationWithView in the above code requires an NSView instance. To obtain one, I query the layout engine for an instance of the containing window, then query the Cocoa window implementation to determine the associated view instance. This is possible because each CocoaWidgetImpl maintains a copy of its associated view, and a method that can be used to retrieve it. Here is the code that accomplishes this:

NSView *nsView;

// get the view of the containing window

WidgetImpl *top = GetRootWidget();

if (top) {
CocoaWindowImpl *winImpl = dynamic_cast<CocoaWindowImpl *>(top);
if (winImpl) {
nsView = winImpl->GetView();
if (nsView)
[[NSPrintOperation printOperationWithView:nsView] runOperation];
}
}

MenuItemClassQuit

Quit is implemented by passing control to the singleton concrete CocoaAppImpl object that manages the main loop of the application:

CocoaAppImpl *appImpl = CocoaAppImpl::GetCocoaAppImplInstance();
if (appImpl)
{
ret = appImpl->Shutdown();
}

Clipboard/Pasteboard Functions

The next several menu items implement clipboard related functionality. Clipboard functionality in Cocoa is implemented by way of a Pasteboard server, but at least for now, I can rely on functionality built into NSTextView. Given an instance of NSTextView, all I need to do is invoke the appropriate method. Assuming the following declaration:

NSTextView *m_text;

the correspond calls are:
 
[m_text cut:nil]; // MenuItemClassCut
[m_text copy:nil]; // MenuItemClassCopy
[m_text paste:nil]; // MenuItemClassPaste
[m_text delete:nil]; // MenuItemClassClear
[m_text selectAll:nil]; // MenuItemClassSelectAll

However, there is the issue of *which* widget should be handing the clipboard operation. In a window containing several instances of NSTextView, one must identify the widget that currently has the input focus, or, in Cocoa terminology, is the "first responder". To determine this, one needs to once again get an instance of the containing Window widget, and then query it for this information. Here is the code that does this:

NSWindow *nsWindow = NULL;

WidgetImpl *top = GetRootWidget();

if (top) {
CocoaWindowImpl *winImpl = dynamic_cast<CocoaWindowImpl *>(top);
if (winImpl)
nsWindow = winImpl->GetNSWindow();
}

if (nsWindow)
{
NSView *firstResponder = [nsWindow firstResponder];
if (firstResponder) {
...
}
}

Once we have the firstResponder, we can then notify it to perform the operation. But how is that done? We have two choices. First, we could iterate the DOM and query all widgets in the Window for their NSView instances, and compare these to the firstResponder view returned by the above code. But, doing that can be expensive. A better way is to associate a "delegate" with each abstract text widget, and then send a message to the delegate. The handler of that message can then interact with the actual Cocoa text widget to perform the operation.

We can obtain the delegate assocated with a widget by invoking the delegate method on the firstResponder, and once we have the delegate, we can invoke any method that it implements. In the following code, I obtain the delegate from the firstResponder, and then invoke its onCut method (this code is used to handle the selection of the Cut menu item; similar code is used to handle Copy, Paste, Select All, and Clear:

NSTextField *field = [firstResponder delegate];
if (field) {
[field onCut];
}

You are probably wondering now about the onCut function, and its implementation. In short, each CocoaTextWidgetImpl concrete class instance allocates an object, implemented in Objective-C, that acts as the widget's delegate. This class, named PasteboardAction because it is used to handle pasteboard-specific functionality, is defined as:

#import <Foundation/NSObject.h>

#import "pasteboardhandler.h"

@interface PasteboardAction : NSObject
{
PasteboardHandler *m_handler;
}
- (void) setHandler: (PasteboardHandler *) handler;
- (void) onCut;
- (void) onCopy;
- (void) onPaste;
- (void) onClear;
- (void) onSelectAll;
@end

PasteboardAction, as you can see, implements a member variable of type PasteboardHandler, which is a base class that is inherited by CocoaTextWidgetImpl. When one of the functions onCut, onCopy, etc. is invoked by a CocoaMenuItem widget instance, m_handler is used to call the associated CocoaTextWidgetImpl instance, which then invokes cut, copy, etc. on the NSTextView widget instance that it maintains. Here is the code that allocates the instance of PasteboardAction, and the code that makes that action a delegate of the NSTextView widget instance:

PasteboardAction *action;

action = [PasteboardAction alloc];

[action setHandler: this];

// create the text

m_text = [[NSTextView alloc] initWithFrame:graphicsRect];

if (m_text) {

[m_text setDelegate: action];

...
}

The implementation of onCut in PasteboardAction is:

- (void) onCut
{
m_handler->Cut();
}

Cut(), which is implemented by CocoaTextWidgetImpl(), simply calls the cut method of its NSTextView instance:

// pasteboardhandler implementation

void CocoaTextImpl::Cut()
{
if (m_text)
[m_text cut: nil];
}

So, in summary, this is what happens when one of the selection-based menuitems is selected (in the following, the Copy menuitem will be used as the example). Each menuitem has a target that will be invoked when the menuitem is clicked. This target, which is an object, implements a method named onClick. The following is the code for onClick:

- (void) onClick:(id) sender
{
m_handler->HandleCommand();
}


The handler is an instance of CocoaMenuItemImpl. CocoaMenuItemImpl::HandleCommand() implements a switch statement, with cases for each MenuItemClass. If the menuitem corresponds to a special menuitem class (in this case, MenuItemClassCopy), we find the firstResponder (the widget with the focus), and we invoke its onCopy method. The CocoaTextWidgetImpl widget that implements onCopy (via an inherited delegate class PasteboardAction) will then invoke CocoaTextWidgetImpl's Copy method, which in turn calls the native copy method on the NSTextView widget instance that it maintains.

If that is a little hard to follow, the following stack should help:

0 CocoaTextImpl::Copy (this=0x121f0d0) at cocoatextimpl.mm:176
#1 0x003550e8 in -[PasteboardAction onCopy] (self=0x1272090, _cmd=0x357778) at cocoapasteboardaction.mm:21
#2 0x00352e18 in CocoaMenuItemImpl::HandleCommand (this=0x121cfa0) at cocoamenuitemimpl.mm:297
#3 0x00353a60 in -[MenuItemAction onClick:] (self=0x1259730, _cmd=0x3571a4, sender=0x1259740) at cocoamenuitemaction.mm:15
#4 0x9372d270 in -[NSApplication sendAction:to:from:] ()
#5 0x93787aa4 in -[NSMenu performActionForItemAtIndex:] ()
#6 0x93787828 in -[NSCarbonMenuImpl performActionWithHighlightingForItemAtIndex:] ()
#7 0x9368eb20 in _NSHandleCarbonMenuEvent ()
#8 0x9368c484 in _DPSNextEvent ()
#9 0x9368bdc8 in -[NSApplication nextEventMatchingMask:untilDate:inMode:dequeue:] ()
#10 0x9368830c in -[NSApplication run] ()
#11 0x0034eea4 in CocoaAppImpl::MainLoop (this=0x121da70) at cocoaappimpl.mm:29
#12 0x0000b06c in App::MainLoop (this=0x121bab0) at app.cpp:28
#13 0x000043ec in main (argc=3, argv=0xbffff8d0) at layout.cpp:378
Current language: auto; currently objective-c++

MenuItemClassPreferences

The final menuitem class of interest is MenuItemClassPreferences. If the layout engine sees a menu with this menuitem class on Cocoa, it will create instances of CocoaMenuItemImpl, but the corresponding NSMenuItem instance will be the NSMenuItem instance associated with the Preferences... menu item previously created in the Apple menu (if you recall from an earlier post, CocoaMenubarImpl creates Apple and Window menu items in addition to the menus that are specified in markup. In Cocoa, the Preferences menuitem is always placed in the Apple menu. On Gtk+ or .NET, the Preferences... menu will not be overridden like this, and will be created as a normal menuitem below whichever menu the programmer specifies in markup (likely the Edit menu). This is the strategy I used to eliminate the need for the #ifdefs that are used in Mozilla's XUL to isolate platform-specific menuitems. Here is the code that detects the Preferences menuitem and overrides it by looking up the menuitem associated with the Apple menu preferences menuitem:

PRStatus CocoaMenuItemImpl::Create()
{
...
switch (menuItemClass)
{
case MenuItemClassQuit:
menuItem = menuBarImpl->GetQuitMenuItem();
m_isOverride = true;
create = false;
add = false;
break;
case MenuItemClassPreferences:
menuItem = menuBarImpl->GetPreferencesMenuItem();
m_isOverride = true;
create = false;
add = false;
break;
...

where GetPreferencesMenuItem() is implemented by CocoaMenubarImpl and simply returns the NSMenuItem object maintained by CocoaMenubarImpl as a member variable. Notice in the above code, a similar strategy is employed for the Quit menu item, which is also implemented in the Apple menu (versus the File menu in Gtk+ and .NET-based applications).

Some todos include implementing Save and Save As..., which will likely require platform-specific file picker dialogs, and special handling in the toolkit. But first, I need to take the above and port it to .NET and Gtk+. Look for details in an upcoming post.