Contextual Menu Basics
Volume Number: 14 (1998)
Issue Number: 1
Column Tag: Toolbox Techniques
Contextual Menu Basics
by Steve Sheets
Adding Support for the new Mac OS 8 Contextual Menu API; even for applications running without Apple's Contextual Menu Manager
A New OS, A New API, A New Logical Interface
The original thought that went into the Macintosh design was "ease of use". This would be the computer for "the rest of us" who were computer novices or worse. After awhile, novices become power users who want quicker ways to do things they already know how to do. You can see this in the menus of the Macintosh. The user has menu items that he can select with the mouse, but he also has Command-key short cuts. You also can see it in drawing palettes that allow simple commands, yet can be double clicked for more in depth configuration.
The contextual menu feature introduced with Mac OS 8 continues this idea. While holding down the Control key, a user can click the mouse while above some information he wants to manipulate. The information could be selected text, a picture, some database tables or anything we traditionally think of as exportable data. A popup menu appears to provide him with all the commands related to this data. Even if no data is selected, the user can see commands related to the window, document or application.
Contextual menus provide no new commands that are not already available. Rather it groups the commands in a quick table to be read and selected. Instead of selecting the data and then selecting the command, with one quick click, the user can see what can be accomplish. Searching for familiar commands becomes much easier. (For example, "is Find under Edit, View or Text?") Even if the user knows exactly where the command is in the normal menu, a Control-click on the selected data is faster. Power users will quickly adopt Contextual Menus to get their work done.
Whenever Apple introduced a new "logical interface", they provide a new API to developers so that we can provide this new feature to users. Luckily, the API for contextual menu is very small and involves concepts similar to those in the Drag Manager or even the Scrap Manager. This article explains what a developer must know to simply add contextual menus to his application. As an additional benefit, the article will show how easily Contextual-Menu-like features can be added to an application runing an older version of Mac OS, where Apple has not provided the Contextual Menu Manager.
Extension Modules
There is one exception to the idea that Contextual Menus should not provide commands not listed anywhere else. On a PowerPC Mac with the Contextual Menu API installed, extension modules can add additional functions. These code fragments must be installed in a special folder in the System Folder before startup. They are then loaded and called whenever a contextual menu is created. By looking at the data selected (text, picture, file, folder or even no data), the extensions can add commands to the contextual menu. These plug-ins have only read access to the selected data, so they can not modify the data, but they can return results in the Clipboard or by sending an Apple event. The hosting application need not concern itself with an extension module; all of a plug-in's functions are done without the application's knowledge. The Contextual Menu Extension Module API will be explained in an upcoming article. Until then, just be aware that it is available, but only supported on PowerPC machines.
Adding Commands
The key to determining what to put into a contextual menu is the emphasis -- contextual menus. Ignore that the feature is a popup menu. There are many locations to place commands, or allow short cuts to global settings. The user has selected some content, which he wants to modify. Add commands that are based on the content of what is clicked. In a drawing program, clicking drawing elements (circles, rect, lines) should show the commands that affect the element (rotate, change line thickness, change color). In a word processor, commands that change the style of the text or the arrangement of the document are good choices. Window commands can be available, even if the user clicks a portion of the window that has no data. Think twice before adding an application command. For example, Quit is a poor choice; it already has a short cut in the menu. The API provides a way for sub menus also to be added to the contextual menu. If too many commands need to be added, this is a good way to subdivide the list.
The API
There are only 3 new calls in the Contextual Menu API. In their easiest explanation, the calls initialize the API, check if the contextual menu should appear, and display the Contextual Menu. These functions should be called only if the API is installed on the Mac. Use Gestalt to check if this is the case. The gestaltContextualMenuPresent bit of the gestaltContextual- MenuAttr Gestalt attribute indicates if the Contextual Menu Manager is installed.
If the Contextual Menu Manager is installed, call InitContextualMenus(). This will register the application as a client of the Contextual Menu Manager. The routine returns noErr (0) if the initialization succeeds. This check & initialization should occur immediately after the standard toolbox initialization.
Listing 1: ContextualMenuFun.cp
Check if Manager Available, if so Init It
long a_result;
if (Gestalt(gestaltContextualMenuAttr, &a_result) == noErr)
g_have_contextual_menus = BitTst(&a_result,
31-gestaltContextualMenuPresent);
else
g_have_contextual_menus = false;
if (g_have_contextual_menus)
g_have_contextual_menus = (InitContextualMenus()==noErr);
To indicate he wants the contextual menu, the user does a special click on some selected data. For Mac OS 8, this is done by holding down the Control key while doing a standard mouse click. However, there is no reason that in the future this special click could not be done some other way (such as a right mouse click on a two-button mouse). For this reason, the Contextual Menu Manager provides a call that checks if the special click is being done. IsShowContextualMenuClick() should be called after the application is passed any non-NullEvent event. The function is passed the event record and returns true if the special click occured. If the function returns false, the program should continue normally.
Listing 2: ContextualMenuFun.cp
Parse Event, checking if the event is Contextual Menu special click. If so, call routine to handle this.
a_flag = WaitNextEvent(everyEvent, &g_record, 10, NULL);
if (g_have_contextual_menus) {
// if Contextual Menus event,
if (IsShowContextualMenuClick(&g_record)) {
// Handle Contextual Menus
G_ContextualMenu(g_record);
a_flag = false;
}
}
// Handle Normal Events if flag true
The most important routine of the Contextual Menu API is ContextualMenuSelect(). It should be called only when IsShowContextualMenuClick() has returned true. Not only does this routine have several parameters, the application must do several preparation tasks before it can be called.
Assuming the application knows which window was chosen, the application should check if this window is the currently selected window. If it is not, it should immediately be made the current selected window, and redrawn. Next, if some content data was selected, some sort of visual feedback should be drawn to indicate this. Inverting, either the data or the outline of the data is a common method to show what was chosen.
Next, a menu handle should be created and inserted into the menu bar just as if the PopUpMenuSelect() call was going to be made. At this point, the various menu items can be added to the menu handle. Sub-menus also can be added. If the Contextual Menu API adds any items to the list (Help or plug-ins), the Contextual Menu Manager will handle parsing these items; your application need not worry about them.
Now call ContextualMenuSelect(). Pass it the created menu handle, the point that was clicked (in global coordinates), a flag that must always be set to false (originally intended to indicate if the clicked area has a Balloon Help item, but now reserved for Apple's use), the Help type needed, the Help string, a pointer to the Apple event descriptor handling the selected data (named inSelection), a pointer to a long integer (resulting outUserSelectionType to explain what happened), and two pointers to short integers (resulting menu ID & menu item).
The Help type parameter can be one of three values: kCMHelpItemNoHelp, kCMHelpItemAppleGuide, kCMHelpItemOtherHelp. If it is kCMHelpItemNoHelp, then there is no help associated with this contextual menu. The Contextual Menu Manager will put the word "Help" in the front of the menu, but disable it. If the parameter is kCMHelpItemAppleGuide, the Contextual Menu Manager will put the name of the main Apple Guide file into the front of the menu. If the parameter is kCMHelpItemOtherHelp, the Contextual Menu Manager will put the name passed in the Help string into the front of the menu.
If there is no selected data (or if the application does not want to pass the data out), the inSelection parameter can be NULL. Otherwise, it should be a valid Apple event descriptor. After the ContextualMenuSelect() call, the application is responsible for disposing of this descriptor if needed. For simple data structures, the descriptor has a handle to the data (be it text or a picture) as well as the data type. Applications that support scriptability can pass an object specifier pointing to the selected data as the descriptor. This allows some Contextual Menu extensions to send an Apple event back to the current application to change the selected data. Refer to Inside Mac: Interapplication Communication for more information about Apple event descriptors and object specifiers.
If the ContextualMenuSelect() call returns successfully, its result is noErr (0). In this case, outUserSelectionType will indicate what happened. If the user fails to select a menu or a plug-in, the constant kCMNothingSelected is returned. If the user selected Show Balloon Help, the Contextual Menu Manager will handle this call, and the constant kShowBalloonSelected is returned (the application should do nothing). If the user selected the Help menu item, kCMShowHelpSelected is returned. The Application should call the appropriate code, Apple Guide or another help system, to display the appropriate help window. If the user selected a menu item, kCMMenuItemSelected is returned, and the menu ID & menu item are also returned.
Before the application parses the result, it should remove any hiliting it placed on the data. The commands the user selected might change this data, so inverting it later may give the wrong results. The application should delete and dispose of the menu handle. Lastly, if the Apple event descriptor was created using Apple event calls, it should be disposed. Listing 3 shows an example of how to setup the basic ContextualMenuSelect() call:
Listing 3: ContextualMenuFun.cp
Sample ContextualMenuSelect call to handle creating & displaying menu.
UInt32 a_type;
SInt16 a_menu_id;
UInt16 a_menu_item;
Boolean a_balloon_available = false;
Str255 a_help_str = "\pContextualMenuFun Help...";
UInt32 a_help_type = kCMHelpItemOtherHelp;
AEDesc a_descr;
AEDesc* a_descr_ptr = NULL;
// Create Menu
MenuHandle a_menu_handle = NewMenu(255, "\pX");
if (a_menu_handle) {
SetPort(a_window_ptr);
// If Window not front most, bring it front and draw on it
if (FrontWindow()!=a_window_ptr) {
SelectWindow(a_window_ptr);
G_Draw(a_window_ptr);
}
// If Color Window, hilite picture (invert frame)
if (a_window_ptr!=g_window_about) {
SetRect(&g_rect, 0, 0, 200, 200);
PenMode(srcXor);
FrameRect(&g_rect);
PenNormal();
}
// Insert Menu into Menu list
InsertMenu(a_menu_handle, -1);
// Fill Item on menu base on Front Window
G_ContextualMenu_Fill(a_menu_handle, a_window_ptr);
// Fill Descriptor with Pic, if possible
a_descr_ptr = &a_descr;
a_descr.descriptorType = typePict;
a_descr.dataHandle = (Handle) g_pic_1;
// Call to Handle Popup
OSStatus a_status = ContextualMenuSelect(a_menu_handle,
p_event_record.where, a_balloon_available,
a_help_type, a_help_str, a_descr_ptr, &a_type,
&a_menu_id, &a_menu_item);
// Unhilite
if (a_window_ptr!=g_window_about) {
SetRect(&g_rect, 0, 0, 200, 200);
PenMode(srcXor);
FrameRect(&g_rect);
PenNormal();
}
// If call was ok,
if (a_status==noErr) {
// If normal item selected, handle it
if (a_type==kCMMenuItemSelected) {
G_ContextualMenu_Action(a_menu_id, a_menu_item,
a_window_ptr);
}
// If help selected, handle it
else if (a_type==kCMShowHelpSelected) {
if (g_window_about) {
ShowWindow(g_window_about);
SelectWindow(g_window_about);
}
}
}
// Delete Menu
DeleteMenu(255);
DisposeMenu(a_menu_handle);
}
Poor Man's Contextual Menu
Contextual menus were introduced with Mac OS 8. The Contextual Menu Manager also can be installed seperately in any Mac OS 7.6 and later system; although, this is uncommon and is not approved by Apple. While it is always nice to support the latest API, what about the majority of your users who will not be using Mac OS 8 for a time (if ever)? That is where the code G_PoorMan_ContextualMenu provides similar contextual menu functions for these older machines.
G_PoorMan_ContextualMenu uses the PopupMenuSelect() function to create it's own contextual menu. The extension modules will not work with Poor Man's Contextual Menu. Remember, plug-ins are not supported under Mac OS 8 running on a 68040 machine. The code calls the same functions as the normal contextual menu handler, so the developer only has to modify G_ContextualMenu_Fill & G_ContextualMenu_Action for it to work under Contextual Menu Manager API or the G_PoorMan_ContextualMenu code.
G_PoorMan_ContextualMenu must be called whenever the user has clicked the mouse within a window (such as inContent, inDrag, and inGoAway regions). The routine is passed the window the user selected and returns True if this action invokes a Poor Man's Contextual Menu. In that case, the program should not continue to process the event because it has been dealt with.
The routine first checks to make sure that the Contextual Menu API is not installed, and that the current event is a mouse-down in a window. Then it creates a menu handle, to be used by the PopUpMenuSelect() routine. Just like the Contextual Menu API, the Poor Man version has to bringing the window to the top and giving feedback to the selected item. Then the Help item is added to the menu. This is different from the Contextual Menu API, which handles the Help menu. Since this menu may appear on the Help window, the code accounts for this by disabling the Help item in that case. The last bit of setup has G_ContextualMenu_Fill being called for the menu items to be added.
Once the menu is fully created, PopUpMenuSelect() is called to process the popup menu. If an item is selected, the menu item number is examined. If it is one, then the Help menu item was selected, and the Help window should appear. Any other number would cause G_ContextualMenu_Action to be called with the correct number. Finally the temporary popup-menu handle is deleted and disposed.
Listing 4: G_PoorMan_ContextualMenu
Poor Man Contextual Menu (ie. no Manager), Return true if event handled
Boolean G_PoorMan_ContextualMenu(WindowPtr p_window_ptr)
{
Boolean a_flag = false;
// No Manager, event mouse down in Window & Control Key down
if (!g_have_contextual_menus) {
if ((g_record.what==mouseDown) && p_window_ptr) {
if ((g_record.modifiers & controlKey)!=0) {
// Event handled here!
a_flag = true;
Str255 a_temp_str = "\p?";
MenuHandle a_menu_handle = NewMenu(255, a_temp_str);
if (a_menu_handle) {
SetPort(p_window_ptr);
// If Window not front most, bring it front and draw on it
if (FrontWindow()!=p_window_ptr) {
SelectWindow(p_window_ptr);
G_Draw(p_window_ptr);
}
// If Color Window, hilite picture (invert frame)
if (p_window_ptr!=g_window_about) {
SetRect(&g_rect, 0, 0, 200, 200);
PenMode(srcXor);
FrameRect(&g_rect);
PenNormal();
}
// Insert Menu into Menu list
InsertMenu(a_menu_handle, -1);
// Add About (Dim if About Window on top)
if (p_window_ptr==g_window_about)
AppendMenu(a_menu_handle,
"\p(About ContextualMenuFun...;(-");
else
AppendMenu(a_menu_handle,
"\pAbout ContextualMenuFun...;(-");
// Fill Item on menu base on Front Window
G_ContextualMenu_Fill(
a_menu_handle, p_window_ptr);
// Call to Handle Popup
long a_result = PopUpMenuSelect(a_menu_handle,
g_record.where.v, g_record.where.h, 1);
// Unhilite
if (p_window_ptr!=g_window_about) {
SetRect(&g_rect, 0, 0, 200, 200);
PenMode(srcXor);
FrameRect(&g_rect);
PenNormal();
}
// If call was ok,
if (a_result) {
short a_menu_id = HiWord(a_result);
short a_menu_item = LoWord(a_result);
if (a_menu_id==255) {
// If help selected, handle it
if (a_menu_item==1) {
if (g_window_about) {
ShowWindow(g_window_about);
SelectWindow(g_window_about);
}
}
else {
// If normal item selected, handle it (Adjust for Help Menu Item)
a_menu_item -= 2;
G_ContextualMenu_Action(a_menu_id,
a_menu_item, p_window_ptr);
}
}
}
// Delete Menu
DeleteMenu(255);
DisposeMenu(a_menu_handle);
}
}
}
}
return a_flag;
}
Sample Code
ContextualMenuFun was created to show how easy it is to provide contextual menus to the user. Most Macintosh developers will recognize their way around the standard portions of the program. All the code to handle details of a Contextual Menu is inside of the G_ContextualMenu routine. The Non-Contextual Menu Manager version of the code is inside G_PoorMan_ContextualMenu. Both routines call G_ContextualMenu_Fill & G_ContextualMenu_Action. G_ContextualMenu_Fill is passed the menu handle & selected window pointer, and fills the menu with items. Assuming the user has chosen a command, G_ContextualMenu_Action is passed the clicked window, the selected menu ID & menu item. G_ContextualMenu_Action parses this information, and takes action. The code was written to be expanded, which is why menu ID is passed. Remember, sub menus can be added so the menu ID is not always the same. The selected window is passed so commands can be more window and content sensitive.
The new "logical interface" begs for an object oriented approach to handling the contextual menus. A clicked visual object calls the standard routine to set up the contextual menu, using the API or not, which calls a virtual method to fill in the menu handle with commands. The standard routine would then display the menu. Assuming the user selects an item, another virtual method would be called to take action. Controls and field objects could even put their commands into the menu and then call the owning window for the window to put in its own commands, and so on up the command chain. Commercial or in-house frameworks easily could be modified this way to support the Contextual Menu API.
Steve Sheets has been happily programming the Macintosh since 1983, which makes him older than he wishes, but not as young as he acts. Being a native Californian, his developer wanderings have led him to Northern Virginia. For those interested, his non-computer interests involve his family (wife & 2 daughters), Society for Creative Anachronism (Medieval Reenactment) and Martial Arts (Fencing & Tai Chi). He is currently an independent developer, taking on any and all projects and can be reached by sending mail to MageSteve@aol.com.