Sharing Motif Menus

by John Cwikla

Copyright © 1995,1996,1997,1998,1999,2000,2001,2002 John Cwikla, All Rights Reserved.

Being an X application programmer, as I browse the other windowing systems, one thing that plucks the strings of jealousy in my heart is menus. Whether a global menu ala the Macintosh or Next, or the MDI of Windows, there is always a sense of a "front" or "key" window to which the menus apply. Since the closest concept in X is the window with focus, the result is that any window in your application a menu can affect will need its own menus. This fact is visible in any program with multiple main windows per application. Since these menus are (most likely) the same from main window to main window, instead of duplicating the menu hierarchy (which may include tens of widgets), sharing the menus, and their items, between the main windows would be a big win.

I'm going to assume you know how to create Motif menus, if not check the reference section for some good places to start. Motif menus consist of three main pieces: a CascadeButton, a MenuShell and a RowColumn. Their relationship is defined as follows: the RowColumn is the child of the MenuShell. The RowColumn is the menu pane of the CascadeButton if it is has been set as the CascadeButton's XmNsubMenuId resource. This last relationship is the core of shared menus, more than one CascadeButton can have its XmNsubMenuId resource set to the same RowColumn widget.

You Probably Already Are Sharing Menus (or Pieces of Menus)....

As of Motif 1.2 (I have not moved on to 2.0 yet, so this may have changed) if a menu has multiple CascadeButtons, and you use the convenience function XmCreatePulldownMenu(), the Motif menu code does something a bit wacky behind your back: it reuses the same MenuShell as the parent shell of all the RowColumns at the same level of your menu! I can hear a couple of you in the back saying, "Impossible! A shell can only have one child!" This, however, is not true. A Shell can have any number of children, but can only have one managed child. Why is the Motif library doing this? I have no idea. My best guess would be that it is more efficient to manage/unmanage the RowColumn children as needed than to have multiple shells with all the event handlers, focus data, etc needed to implement the internals of the Motif MenuShells. So, if you are using XmCreatePulldownMenu() rather than creating the pieces by yourself, chances are your menus are a little more efficient than you thought. But this little feature will cause trouble later on...

Find Some Context

Before we delve too far, first step back and think about the consequences of sharing menus across multiple main windows. The main concern should be that these menus would appear to be context-less. Storing window specific information would be difficult, with only the XmNuserData fields to store information in, each new window would overwrite the data of the last. Or so it would seem...

First I will make an assumption: given a topmost shell in an application you will be able to retrieve some useful context information about that window. For instance, in my own program I have a container object that holds all the information my program needs on a per window basis. In my case I have:

typedef struct _Bob
{
	Widget shell;
	String name;
	/* all sorts of other useful stuff */
} *Bob;

For each main window, I allocate a struct _Bob, and the pointer is my associated context. (Quick trivia point: since I am part of Generation X I am forced to use Bob for all temporary variables and structures instead of the outdated and obviously less powerful foo.) Whether you have stored that context pointer in your own hashtable, an XmNuserData field, or some other creative way is immaterial as long as it does exist.. In any case, I will assume that there is a function:

XtPointer ShellToContext(Widget _shell);

that takes a shell and gets back an XtPointer to your context information. The final goal? The goal is to find the correct context associated with the current window when there is a callback from the shared menus.

A Doubly Linked List

If we were not going to use shared menu panes in our application, our menus would most likely have been created as children of the main window hierarchy. In that case, whenever we had a menu item callback, we could have used XtParent() to chase up the hierarchy to get to the top-most shell and call our ShellToContext() function, and all would be happy. Unfortunately, for shared menu panes, calling XtParent() will take you up to the "anchor" parent of shared menu, not the main window currently using the menu. In fact, there is no direct way to find which CascadeButton popped up the menu in the first place -- but we can keep track of this ourself.

The CascadeButtonWidget/CascadeButtonGadget classes both have an XmNcascadingCallback, called right before the menu specified in the XmNsubMenuId is popped up. Here's our chance! By adding an XmNcascadingCallback callback to the CascadeButton we can insert this information into the menu pane specified in the XmNsubMenuId. Then instead of using XtParent() to climb up the widget tree, retrieving the value in the menu pane's XmNsubMenuId will always give the widget that popped up the menu, enabling us to traverse our menus in two directions. These functions can now be added:

void CascadeButtonInsertParentCB(Widget _w, XtPointer _unused, XtPointer _nothing)
{
	Arg warg[1];
	int n;

	n = 0;
	XtSetArg(warg[n], XmNsubMenuId, (XtPointer)&menu); n++;
	XtGetValues(_w, warg, n);

	n = 0;
	XtSetArg(warg[n], XmNuserData, (XtPointer)_w); n++;
	XtSetValues(_w, warg, n);
}

Widget GetSharedMenuCurrentTopShell(Widget _w)
{
	XtPointer temp;
	Widget cur;
	Boolean done;
	Arg warg[1];
	int n;

	cur = _w;

	done = (_w == NULL); /* make sure NULL wasn't passed in. */

	while(!done)
	{
		if (XtParent(cur) == NULL)
			done = TRUE;
		else
		if (XmIsMenuShell(XtParent(cur)))
		{
			n = 0;
			XtSetArg(warg[n], XmNuserData, (XtPointer)&temp); n++;
			XtGetValues(cur, warg, n);
			cur = (Widget)_temp;
		}
		else
			cur = XtParent(cur);
	}

	return cur;
}

When adding a new CascadeButton to a menu add CascadeButtonInsertParentCB() as your XmNcascadingCallback. When you get, say, an XmNactivateCallback from an item in your menu, you can use GetSharedCurrentTopShell() to get the topmost shell of the hierarchy currently posting the shared menus. Finally, ShellToContext() can be called and you are on your way.

The Missing Details

Earlier, I made reference to the "anchor" widget that the menus were created from. What exactly am I talking about? If you are sharing menus, the real or "anchor" parent of the menus should not be destroyed! If you destroy that parent, goodbye menus! You need a parent widget, one that is most likely invisible to the user. One idea would be to use an unmapped menubar widget so you could automate the process of menu creation/sharing by looping over the menubar children. NOTE: In some earlier versions of Motif, 1.2.3 I think, would not allow this type of sharing of menus if the parent was unmapped as it wrongly checked to see if the parent of the menu had a state of IsVisible, not popping up the menu if it was not. The main point, once again, don't destroy you "anchor" parent.

What about the sharing of MenuShells that the Motif library is already doing if you call the convenience function XmCreatePulldownMenu()? This is not actually a problem. The XmNsubMenuId that is set in the CascadeButton is the RowColumn child of the MenuShell, not the MenuShell itself. So there is no problem with using the convenience functions, and their internal sharing, with the method I have presented today.

When and Why

The program I write is quite large, and the number of items in my menus can be quite large (for fonts, sizes, colors, regular items etc.) By sharing menus across all my main windows, I can shrink the time a main window needs to create its internals by a significant amount.

Another benefit of this approach is for cross-platform development. We share a majority of the menu code between our platforms. All the other platforms have one menu hierarchy per application, and our internal menu code reflects that. Now, by retrieving the current window context, I can use that with the shared menus, and not need to make any radical changes to the cross-platform code. This would not have been true if I had not shared menus.

Until next time...

References

Young, Douglas A., The X Window System Programming and Applications with Xt OSF/Motif Edition, Prentice Hall, 1990