Motif Programming Manual (Volume 6A)

Previous Next Contents Document Index Motif Docs motifdeveloper.com Imperial Software Technology X-Designer

X-Designer - The Leading X/Motif GUI Builder - Click to download a FREE evaluation





In this chapter:



Chapter: 19

Menus



This chapter describes the different types of menus provided by the Motif toolkit. It also presents a number of ways to create menus in an application and talks about the issues involved in designing menu systems.

Menus provide the user with a set of choices in an application without complicating its normal visual appearance. These convenient mini-toolboxes are essential for the user who, like an auto mechanic that is busy working under the car, needs quick and convenient access to her tools without having to look or move away from her work. The Motif Style Guide provides for three different types of menus: PulldownMenus, PopupMenus, and OptionMenus. Despite the differences between the three types of menus, they all provide simple and convenient access to application functionality.

Menu Types

PulldownMenus that are posted from the MenuBar are the most common menus in an application.Figure 19-1 shows an example of a PulldownMenu.The menu pops up when the user presses the first mouse button on a CascadeButton.1As described in Chapter 4, The Main Window, CascadeButtons may be displayed as titles in a MenuBar or as menu items in a PulldownMenu. When the CascadeButton is a child of a MenuBar, the menu drops down below the button when the user clicks on it. When the CascadeButton is an item in an existing menu, the new menu pops up to the right of the item; it is sometimes referred to as a cascading menu or a pullright menu.

Figure  19-1 A PulldownMenu

Under certain conditions, it may be inconvenient for the user to stop what she is doing, move the mouse to the MenuBar to pulldown a menu, and then move the mouse back to where she was working. Having to move the mouse away, even to another part of the same window, can reduce productivity. A PopupMenu is one solution to this problem as it can provide immediate access to application functionality. PopupMenus are posted using the third mouse button and can be displayed anywhere in an application. Rather than having to move the mouse, the user can simply press the third mouse button to cause a PopupMenu to appear on the spot. This type of menu does not need to be associated with a visible user-interface element. In fact, PopupMenus are usually popped up from a work area or another region that is not affiliated with a user-interface component like a PushButton or CascadeButton. The only drawback to this design is that there is no indication to the novice user that the menu exists. Figure 19-2 shows a PopupMenu.

Figure  19-2 A PopupMenu

The OptionMenu combines the strengths of a PulldownMenu and a PopupMenu. Like a PulldownMenu, it is posted from a CascadeButton, but like a PopupMenu, it can be placed where it is needed. The CascadeButton is used to display the default choice for the menu. When the user presses the button, the alternate choices are displayed in a menu, as shown in Figure 19-3. Like a PulldownMenu, an OptionMenu is invoked using the first mouse button, but it is displayed on top of its associated CascadeButton rather than below it.

Figure  19-3 An OptionMenu

The use of the third mouse button to activate PopupMenus is in sharp contrast to PulldownMenus and OptionMenus, which are always invoked by the first mouse button. It may seem confusing to the user that some menus are invoked by the first button while others are invoked by the third. However, there is some consistency in the fact that PulldownMenus and OptionMenus are always attached to CascadeButtons, and buttons are always activated by the first mouse button. By specifying that PopupMenus use the third mouse button, the first mouse button is free to be used for other activities in an application work area, which is important since PopupMenus can be popped up anywhere in an application.

When the user posts a menu, it is only displayed until the user makes a selection, and then it is removed. A menu can have an additional feature that allows it to be torn off, so that it remains posted in its own window. The tear-off functionality is activated by a special tear-off button in the menu. The button displays a dashed line to indicate that you can tear off the menu, like you would tear a coupon out of a newspaper. When the user presses the tear-off button, the menu is placed in a separate window, and the user can make as many selections as she would like. Figure 19-4 shows a PulldownMenu that provides the tear-off capability.

Figure  19-4 A PulldownMenu with tear-off functionality

To make menus even more convenient to use, menu items can have mnemonics and accelerators associated with them. These devices are keyboard equivalents that allow the user to activate menu items using the keyboard rather than the mouse. For example, in Figure 19-1, the underlined letter in each menu item is its mnemonic. While the menu is posted, the user can type the specified character to activate that menu item. Accelerators are keystroke combinations that invoke a menu item even when the menu is not displayed. Accelerators typically use the CTRL or ALT key to distinguish them from ordinary keystrokes that are sent to the application. For example, again in Figure 19-1, the Ctrl+X accelerator allows the user to exit the application without accessing the menu.

Before we plunge into the details of menu creation, a word of warning to experienced X Toolkit programmers is in order. Motif does not use Xt's normal methods for creating and managing menus. In fact, you cannot use the standard Xt methods for menu creation or management without virtually re-implementing the Motif menu design.2In Xt, you would typically create an OverrideShell that contains a generic manager widget, followed by a set of PushButtons. To display the menu, you would pop up the shell using XtPopup(). The Motif toolkit abstracts the menu creation and management process using routines that make the shell opaque to the programmer.

Creating Simple Menus

In Chapter 4, The Main Window, we used the simple menu creation routines to build the MenuBar and its associated PulldownMenus. These routines are designed to be plug-and-play convenience routines; their only requirements are compound strings for the menu labels and a single callback function that is invoked when the user activates any of the menu items.

XmVaCreateSimpleMenuBar() creates a MenuBar, while XmVaCreateSimplePulldownMenu() generates a PulldownMenu and its associated items. These functions take a variable-length argument list of parameters that specify either the CascadeButtons for the MenuBar or the menu items for the PulldownMenu. You can also pass RowColumn-specific resource/value pairs to configure the RowColumn widget that manages the items in the menu. The functions are front ends for more primitive routines that actually create the underlying widgets, so they are convenient for many simple menu creation needs. You should review Chapter 4, for more information on how to use these functions.

Motif also provides simple creation routines for creating PopupMenus and OptionMenus. Both XmVaCreateSimplePopupMenu() and XmVaCreateSimpleOptionMenu() are very similar to the routines for creating PulldownMenus, so much of the information in Chapter 4 also applies to these functions.


Popup Menus

The only difference between XmVaCreateSimplePulldownMenu() and XmVaCreateSimplePopupMenu() is that the latter routine does not have a button parameter for specifying the CascadeButton used to display the menu. Since PopupMenus are not associated with CascadeButtons, this parameter isn't necessary. Example 19-1 demonstrates the creation of a simple PopupMenu.3

Example  19-1 The simple_popup.c program

/* simple_popup.c -- demonstrate how to use a simple popup menu.
** Create a main window that contains a DrawingArea widget, which
** displays a popup menu when the user presses the third mouse button.
*/

#include <Xm/RowColumn.h>
#include <Xm/MainW.h>
#include <Xm/DrawingA.h>

main (int argc, char *argv[])
{
    XmString       line, square, circle, exit_b, exit_acc;
    Widget         toplevel, main_w, drawing_a, popup_menu;
    void           popup_cb(Widget, XtPointer, XtPointer);
    XtAppContext   app;
    Arg            args[4];
    int            n;

    XtSetLanguageProc (NULL, NULL, NULL);
    toplevel = XtVaOpenApplication (&app, "Demos", NULL, 0, &argc, argv, NULL, 
                                    sessionShellWidgetClass, NULL);

    /* Create a MainWindow widget that contains a DrawingArea in
    ** its work window.
    */
    n = 0;
    XtSetArg (args[n], XmNscrollingPolicy, XmAUTOMATIC); n++;
    main_w = XmCreateMainWindow (toplevel, "main_w", args, n);

    /* Create a DrawingArea -- no actual drawing will be done. */
    n = 0;
    XtSetArg (args[n], XmNwidth, 500);  n++;
    XtSetArg (args[n], XmNheight, 500); n++;
    drawing_a = XmCreateDrawingArea (main_w, "drawing_a", args, n);
    XtManageChild (drawing_a);

    line = XmStringCreateLocalized ("Line");
    square = XmStringCreateLocalized ("Square");
    circle = XmStringCreateLocalized ("Circle");
    exit_b = XmStringCreateLocalized ("Exit");
    exit_acc = XmStringCreateLocalized ("Ctrl+C");
    popup_menu = XmVaCreateSimplePopupMenu (drawing_a, "popup", popup_cb,
                        XmNpopupEnabled, XmPOPUP_AUTOMATIC,
                        XmVaPUSHBUTTON, line, 'L', NULL, NULL,
                        XmVaPUSHBUTTON, square, 'S', NULL, NULL,
                        XmVaPUSHBUTTON, circle, 'C', NULL, NULL,
                        XmVaSEPARATOR,
                        XmVaPUSHBUTTON, exit_b, 'x', "Ctrl<Key>c", exit_acc,
                        NULL);
    XmStringFree (line);
    XmStringFree (square);
    XmStringFree (circle);
    XmStringFree (exit_b);
    XmStringFree (exit_acc);

    XtManageChild (main_w);
    XtRealizeWidget (toplevel);
    XtAppMainLoop (app);
}

/* popup_cb() -- invoked when the user selects an item in the popup menu */
void popup_cb (Widget menu_item, XtPointer client_data, XtPointer call_data)
{
    int item_no = (int) client_data;

    if (item_no == 3) /* Exit was selected -- exit */
        exit (0);

    /* Otherwise, just print the selection */
    puts (XtName (menu_item));
}
This program creates a standard MainWindow widget that contains a DrawingArea widget. The program does not do any drawing; it is just a skeleton that demonstrates how to attach a PopupMenu. The PopupMenu is created using XmVaCreateSimplePopupMenu() with the DrawingArea widget as its parent. The menu is popped up when the user presses the third mouse button in the DrawingArea, as shown in Figure 19-5.

Figure  19-5 Output of the simple_popup program

The menu contains four items, the last of which has the accelerator Ctrl<Key>C. Any time the user presses CTRL-C in the application, the callback routine associated with the menu is called as if the menu had been popped up and the Exit item had been selected. The popup_cb() routine either prints the name of the menu item or exits, depending on which item the user selected. Note that the name of the menu item does not correspond to its label. As described in Chapter 4, The Main Window, menu items are automatically given names of the form button_n, where n is assigned in order of menu item creation, starting at 0 (zero).

In Motif 1.2, PopupMenus are not automatically displayed by the toolkit: the programmer must install an event handler, or, for the DrawingArea, an XmNinputCallback, in order to catch ButtonPress events. Once caught, a call to XmMenuPosition() to place the menu at the cursor, followed by XtManageChild() of the menu itself are required to effect the posting.

In Motif 2.0 and later, this is not necessary because automatic popup is now supported. For the simple case, simply setting the XmNpopupEnabled resource to XmPOPUP_AUTOMATIC has the desired effect. Where there is potentially a choice between several popup menus in a given context, the resource XmNpopupHandlerCallback can be used to discriminate at the appropriate juncture. XmNpopupHandlerCallback is defined in both the Manager and Primitive classes, and is thus inherited by all widgets in the Motif set. Note that the resource is not defined for the Gadget class: you need to manipulate the required popup through the Manager parent. Where no callback is registered, the toolkit will select the menu to display.

The XmNpopupMenuHandler callback is passed a structure of type XmPopupHandlerCallbackStruct as the third parameter when invoked. The structure is defined as follows:

typedef struct
{
    int          reason;
    XEvent       *event;
    Widget       menuToPost;
    Boolean      postIt;
    Widget       target;
} XmPopupHandlerCallbackStruct;
The reason and event fields are the familiar elements found in all Motif callback structures; here, the reason field will have the value XmCR_POST or XmCR_REPOST. XmCR_POST is the normal value; XmCR_REPOST will occur if the menu is reposted as a result of event replay. menuToPost is the toolkit's suggestion of the menu to display: this element in the structure should be modified appropriately if a different popup menu is required. The target field holds the widget or gadget which the Manager believes is the source of the popup request. Lastly, postIt is a flag which indicates whether the posting action is to continue after the callback completes.

Example 19-2 creates two popups, and adds an XmNpopupHandlerCallback choose_cb onto the DrawingArea. This randomly picks one of the two menus to display, depending upon the x coordinate of the Button event: if even, it displays menuA, otherwise menuB.4

Example  19-2 The choice_popup.c program

/* choice_popup.c -- demonstrate how to use a popup menu handler.
** Create a main window that contains a DrawingArea widget, which
** chooses between two popup menus when the user presses the third 
** mouse button.
*/

#include <Xm/RowColumn.h>
#include <Xm/MainW.h>
#include <Xm/DrawingA.h>

Widget menuA, menuB;

main (int argc, char *argv[])
{
    XmString       line, square, circle, exit, exit_acc;
    XmString       red, green, blue;
    Widget         toplevel, main_w, drawing_a;
    void           popup_cb(Widget, XtPointer, XtPointer);
    void           choose_cb(Widget, XtPointer, XtPointer);
    XtAppContext   app;
    Arg            args[4];
    int            n;

    XtSetLanguageProc (NULL, NULL, NULL);
    toplevel = XtVaOpenApplication (&app, "Demos", NULL, 0, &argc, argv, NULL, 
                                    sessionShellWidgetClass, NULL);

    /* Create a MainWindow widget that contains a DrawingArea in
    ** its work window.
    */
    n = 0;
    XtSetArg (args[n], XmNscrollingPolicy, XmAUTOMATIC); n++;
    main_w = XmCreateMainWindow (toplevel, "main_w", args, n);

    /* Create a DrawingArea -- no actual drawing will be done. */
    n = 0;
    XtSetArg (args[n], XmNwidth, 500);  n++;
    XtSetArg (args[n], XmNheight, 500); n++;
    drawing_a = XmCreateDrawingArea (main_w, "drawing_a", args, n);
    /* Callback to choose which popup menu to display */
    XtAddCallback (drawing_a, XmNpopupHandlerCallback, choose_cb, NULL);
    XtManageChild (drawing_a);

    line = XmStringCreateLocalized ("Line");
    square = XmStringCreateLocalized ("Square");
    circle = XmStringCreateLocalized ("Circle");
    exit = XmStringCreateLocalized ("Exit");
    exit_acc = XmStringCreateLocalized ("Ctrl+C");
    menuA = XmVaCreateSimplePopupMenu (drawing_a, "menuA", popup_cb,
                            XmNpopupEnabled, XmPOPUP_AUTOMATIC,
                            XmVaPUSHBUTTON, line, 'L', NULL, NULL,
                            XmVaPUSHBUTTON, square, 'S', NULL, NULL,
                            XmVaPUSHBUTTON, circle, 'C', NULL, NULL,
                            XmVaSEPARATOR,
                            XmVaPUSHBUTTON, exit, 'x', "Ctrl<Key>c", exit_acc,
                            NULL);
    XmStringFree (line);
    XmStringFree (square);
    XmStringFree (circle);
    XmStringFree (exit);
    XmStringFree (exit_acc);

    red = XmStringCreateLocalized ("Red");
    green = XmStringCreateLocalized ("Green");
    blue = XmStringCreateLocalized ("Blue");

    menuB = XmVaCreateSimplePopupMenu (drawing_a, "menuB", popup_cb,
                            XmNpopupEnabled, XmPOPUP_AUTOMATIC,
                            XmVaPUSHBUTTON, red, 'R', NULL, NULL,
                            XmVaPUSHBUTTON, green, 'G', NULL, NULL,
                            XmVaPUSHBUTTON, blue, 'B', NULL, NULL,
                            NULL);
    XmStringFree (red);
    XmStringFree (green);
    XmStringFree (blue);

    XtManageChild (main_w);
    XtRealizeWidget (toplevel);
    XtAppMainLoop (app);
}

/* popup_cb() -- invoked when the user selects an item in the popup menu */
void popup_cb (Widget menu_item, XtPointer client_data,
                XtPointer call_data)
{
    int item_no = (int) client_data;

    if (item_no == 3) /* Exit was selected -- exit */
        exit (0);

    /* Otherwise, just print the selection */
    puts (XtName (menu_item));
}

/* choose_cb() -- invoked when the user requests a popup menu */
void choose_cb (Widget menu_item, XtPointer client_data,
                    XtPointer call_data)
{
    XmPopupHandlerCallbackStruct *cbs =
                            (XmPopupHandlerCallbackStruct *) call_data;
    XButtonPressedEvent *bp = (XButtonPressedEvent *) cbs->event;

    if ((bp->x % 2) == 0) {
        cbs->menuToPost = menuA;
    }
    else {
        cbs->menuToPost = menuB;
    }
}

Cascading Menus

A cascading menu, or a pullright menu, is implemented as a PulldownMenu displayed from a menu item in another PulldownMenu or PopupMenu. The menu item that posts the cascading menu must be a CascadeButton. Example 19-3 demonstrates how to add a cascading menu using the simple menu routines. The program adds a Line Width menu item to the PopupMenu from Example 19-1. This menu item is a CascadeButton that posts a PulldownMenu created with XmVaCreateSimplePulldownMenu().5

Example  19-3 The simple_pullright.c program

/* simple_pullright.c -- demonstrate how to make a pullright menu
** using simple menu creation routines. Create a main window that
** contains a DrawingArea widget that displays a popup menu when the
** user presses the third mouse button.
*/

#include <Xm/RowColumn.h>
#include <Xm/MainW.h>
#include <Xm/DrawingA.h>

main (int argc, char *argv[])
{
    XmString       line, square, circle, weight, exit, exit_acc;
    XmString       w_one, w_two, w_four, w_eight;
    Widget         toplevel, main_w, drawing_a, cascade, popup_menu,
                   pullright;
    void           popup_cb(Widget, XtPointer, XtPointer);
    void           set_width(Widget, XtPointer, XtPointer);
    XtAppContext   app;
    Arg            args[4];
    int            n;

    XtSetLanguageProc (NULL, NULL, NULL);
    toplevel = XtVaOpenApplication (&app, "Demos", NULL, 0, &argc, argv, NULL, 
                                    sessionShellWidgetClass, NULL);

    /* Create a MainWindow widget that contains a DrawingArea in
    ** its work window.
    */
    n = 0;
    XtSetArg (args[0], XmNscrollingPolicy, XmAUTOMATIC); n++;
    main_w = XmCreateMainWindow (toplevel, "main_w", args, n);

    /* Create a DrawingArea -- no actual drawing will be done. */
    n = 0;
    XtSetArg (args[n], XmNwidth, 500); n++;
    XtSetArg (args[n], XmNheight, 500); n++;
    drawing_a = XmCreateDrawingArea (main_w, "drawing_a", args, n);
    XtManageChild (drawing_a);

    line = XmStringCreateLocalized ("Line");
    square = XmStringCreateLocalized ("Square");
    circle = XmStringCreateLocalized ("Circle");
    weight = XmStringCreateLocalized ("Line Width");
    exit = XmStringCreateLocalized ("Exit");
    exit_acc = XmStringCreateLocalized ("Ctrl+C");
    popup_menu = XmVaCreateSimplePopupMenu (drawing_a, "popup", popup_cb,
                            XmNpopupEnabled, XmPOPUP_AUTOMATIC,
                            XmVaPUSHBUTTON, line, 'L', NULL, NULL,
                            XmVaPUSHBUTTON, square, 'S', NULL, NULL,
                            XmVaPUSHBUTTON, circle, 'C', NULL, NULL,
                            XmVaCASCADEBUTTON, weight, 'W',
                            XmVaSEPARATOR,
                            XmVaPUSHBUTTON, exit, 'x', "Ctrl<Key>c", exit_acc,
                            NULL);
    XmStringFree (line);
    XmStringFree (square);
    XmStringFree (circle);
    XmStringFree (weight);
    XmStringFree (exit);

    /* create pullright for "Line Width" button -- this is the 4th item! */
    w_one = XmStringCreateLocalized ("1");
    w_two = XmStringCreateLocalized ("2");
    w_four = XmStringCreateLocalized ("4");
    w_eight = XmStringCreateLocalized ("8");
    pullright = XmVaCreateSimplePulldownMenu (popup_menu, "pullright",
                            3 /* menu item offset */, set_width,
                            XmVaPUSHBUTTON, w_one, '1', NULL, NULL,
                            XmVaPUSHBUTTON, w_two, '2', NULL, NULL,
                            XmVaPUSHBUTTON, w_four, '4', NULL, NULL,
                            XmVaPUSHBUTTON, w_eight, '8', NULL, NULL,
                            NULL);
    XmStringFree (w_one);
    XmStringFree (w_two);
    XmStringFree (w_four);
    XmStringFree (w_eight);

    XtManageChild (main_w);
    XtRealizeWidget (toplevel);
    XtAppMainLoop (app);
}

/* popup_cb() -- invoked when the user selects an item in the popup menu */
void popup_cb (Widget menu_item, XtPointer client_data,
                XtPointer call_data)
{
    int item_no = (int) client_data;
    if (item_no == 4) /* Exit was selected -- exit */
        exit (0);
    /* Otherwise, just print the selection */
    puts (XtName (menu_item));
}

/* set_width() -- called when items in the Line Width pullright menu
** are selected.
*/
void set_width (Widget menu_item, XtPointer client_data,
                XtPointer call_data)
{
    int item_no = (int) client_data;
    printf ("Line weight = %d\n", 1 << item_no);
}
In the call to XmVaCreateSimplePulldownMenu(), the PopupMenu is specified as the parent of the cascading menu. The button parameter is set to 3 to indicate that the fourth item in the PopupMenu posts the cascading menu. Figure 19-6 shows the output of the program.

Figure  19-6 Output of the simple_pullright program


Option Menus

An OptionMenu is similar to a PulldownMenu in that they are both associated with CascadeButtons. However, there are also several major differences between the two types of menus. In an OptionMenu, the CascadeButton is not part of a MenuBar. Instead, it is created as the child of a RowColumn widget that also contains a Label gadget.

Another difference is that the menu pops up on top of the CascadeButton, instead of dropping down from it. The label on the CascadeButton is one of the elements in the menu; the CascadeButton displays the current menu selection. The Motif toolkit handles the management of the PulldownMenu for the OptionMenu, so its handle is not available to you, nor does it need to be. Because of the design of the OptionMenu, it cannot have cascading menus.

Example 19-4 demonstrates the use of XmVaCreateSimpleOptionMenu(). The program uses a DrawingArea again, but now the user selects the drawing style from an OptionMenu that is displayed above the DrawingArea6.

Example  19-4 The simple_option.c program

/* simple_option.c -- demonstrate how to use a simple option menu.
** Display a drawing area. The user selects the drawing style from
** the option menu.
*/

#include <Xm/RowColumn.h>
#include <Xm/MainW.h>
#include <Xm/ScrolledW.h>
#include <Xm/DrawingA.h>
#include <Xm/PushB.h>

main (int argc, char *argv[])
{
    XmString       draw_shape, line, square, circle;
    Widget         toplevel, main_w, rc, sw, drawing_a, option_menu, pb;
    void           option_cb(Widget, XtPointer, XtPointer);
    void           exit(int);
    XtAppContext   app;
    Arg            args[4];
    int            n;

    XtSetLanguageProc (NULL, NULL, NULL);
    toplevel = XtVaOpenApplication (&app, "Demos", NULL, 0, &argc, argv, NULL, 
                                    sessionShellWidgetClass, NULL);

    /* Create a MainWindow widget that contains a RowColumn
    ** widget as its work window.
    */
    main_w = XmCreateMainWindow (toplevel, "main_w", NULL, 0);
    rc = XmCreateRowColumn (main_w, "rowcol", NULL, 0);

    /* Inside RowColumn is the Exit pushbutton, the option menu and the
    ** scrolled window that contains the drawing area.
    */
    pb = XmCreatePushButton (rc, "Exit", NULL, 0);
    XtAddCallback (pb, XmNactivateCallback, (void (*)()) exit, NULL);
    XtManageChild (pb);

    draw_shape = XmStringCreateLocalized ("Draw Mode:");
    line = XmStringCreateLocalized ("Line");
    square = XmStringCreateLocalized ("Square");
    circle = XmStringCreateLocalized ("Circle");
    option_menu = XmVaCreateSimpleOptionMenu (rc,
                                "option_menu", draw_shape, 'D', 
                                0 /*initial menu selection*/, option_cb,
                                XmVaPUSHBUTTON, line, 'L', NULL, NULL,
                                XmVaPUSHBUTTON, square, 'S', NULL, NULL,
                                XmVaPUSHBUTTON, circle, 'C', NULL, NULL,
                                NULL);
    XmStringFree (line);
    XmStringFree (square);
    XmStringFree (circle);
    XmStringFree (draw_shape);
    XtManageChild (option_menu);
    /* Create a DrawingArea inside a ScrolledWindow */
    n = 0;
    XtSetArg (args[n], XmNscrollingPolicy, XmAUTOMATIC); n++;
    sw = XmCreateScrolledWindow (rc, "sw", args, n);

    n = 0;
    XtSetArg (args[n], XmNwidth, 500);  n++;
    XtSetArg (args[n], XmNheight, 500); n++;
    drawing_a = XmCreateDrawingArea (sw, "drawing_area", args, n);
    XtManageChild (drawing_a);
    XtManageChild (sw);
    XtManageChild (rc);
    XtManageChild (main_w);
    XtRealizeWidget (toplevel);
    XtAppMainLoop (app);
}

/* option_cb() -- invoked when the user selects an item in the
** option menu
*/
void option_cb (Widget menu_item, XtPointer client_data,
                XtPointer call_data)
{
    int item_no = (int) client_data;
    puts (XtName (menu_item));
}
The layout of the application is different from that in the previous examples because we use a separate ScrolledWindow for the DrawingArea. The RowColumn widget that contains the Exit button, the OptionMenu, and the ScrolledWindow is the work area for the MainWindow. Figure 19-7 shows the output of the program both before and after the OptionMenu is displayed. Notice how the label of the CascadeButton changes as you select alternate values from the menu.

Figure  19-7 Output of the simple_option program

Designing Menu Systems

The advantages of the simple menu creation routines are clear. It is easy to create menus with them, the code is extremely readable, and the job gets done without much room for error. Once the code is written, it is easy to modify the callback function, labels, mnemonics, and accelerators used by a menu.

There are also some disadvantages to using the simple menu creation functions. One problem is that they require a great deal of bulk to create a single menu. If an application needs to create a large number of menus, it has to use a lot of redundant code because the simple creation routines make it difficult to build a looping construct or a function to automate the process. Since the creation routines name the widgets using non-unique names, it is difficult to specify labels, mnemonics, and accelerators in a resource file. If these values are set using a creation routine, this point is irrelevant because the routines hard-code the values. The simple creation routines also make it impossible to specify different callback functions for menu items.

To get around the shortcomings of the simple creation routines, we are going to build a new system that is just as simple to use, but more dynamic and easy to modify. Before we can build our new system, we need to examine the advanced Motif menu creation routines and discuss the overall design of a menu system. We are going to start with the MenuBar and PulldownMenus because almost every application uses these components. Furthermore, everything there is to know about menus can be adapted from the design of a menu system that uses these menus.

Let's begin by examining the steps that you need to take to create a MenuBar and its associated PulldownMenus:

  1. Create a RowColumn widget for use as a MenuBar with XmCreateMenuBar().
  2. Create each PulldownMenu using XmCreatePulldownMenu ().
  3. Create the menu items (PushButtons, ToggleButtons, Separators, etc.) for each PulldownMenu.
  4. Create a CascadeButton for each menu in the MenuBar and attach the associated PulldownMenu to it.
  5. Manage the MenuBar with XtManageChild().
The program in Example 19-5 demonstrates these steps by creating a MenuBar that contains a single File PulldownMenu.7

Example  19-5 The file_menu.c program

/* file_menu.c -- demonstrate how to create a menu bar and pulldown
** menu using the Motif creation routines.
*/

#include <Xm/RowColumn.h>
#include <Xm/MainW.h>
#include <Xm/CascadeB.h>
#include <Xm/SeparatoG.h>
#include <Xm/PushBG.h>

main (int argc, char *argv[])
{
    Widget        toplevel, main_w, menu_w, file_w, cascade_w, push_b, sep_w;
    XmString      label_str;
    XtAppContext  app;
    Arg           args[4];
    int           n;

    XtSetLanguageProc (NULL, NULL, NULL);
    toplevel = XtVaOpenApplication (&app, "Demos", NULL, 0, &argc, argv, NULL, 
                                    sessionShellWidgetClass, NULL);

    n = 0;
    XtSetArg (args[n], XmNscrollingPolicy, XmAUTOMATIC); n++;
    main_w = XmCreateMainWindow (toplevel, "main_w", args, n);
    menu_w = XmCreateMenuBar (main_w, "MenuBar", NULL, 0);

    /* create the "File" Menu */
    file_w = XmCreatePulldownMenu (menu_w, "FilePullDown", NULL, 0);

    /* create the "File" button (attach Menu via XmNsubMenuId) */
    label_str = XmStringCreateLocalized ("File");

    n = 0;
    XtSetArg (args[n], XmNmnemonic, `F');          n++;
    XtSetArg (args[n], XmNlabelString, label_str); n++;
    XtSetArg (args[n], XmNsubMenuId, file_w);      n++;
    cascade_w = XmCreateCascadeButton (menu_w, "File", args, n);
    XtManageChild (cascade_w);
    XmStringFree (label_str);

    /* Now add the menu items */
    push_b = XmCreatePushButtonGadget (file_w, "Open", NULL, 0);
    XtManageChild (push_b);
    push_b = XmCreatePushButtonGadget (file_w, "Save", NULL, 0);
    XtManageChild (push_b);
    sep_w = XmCreateSeparatorGadget (file_w, "separator", NULL, 0);
    XtManageChild (sep_w);
    push_b = XmCreatePushButtonGadget (file_w, "Exit", NULL, 0);
    XtManageChild (push_b);

    XtManageChild (menu_w);
    XtManageChild (main_w);
    XtRealizeWidget (toplevel);
    XtAppMainLoop (app);
}
The code follows the steps that we just outlined. The MenuBar is created as a child of the MainWindow, and the PulldownMenu is created as a child of the MenuBar. The CascadeButton acts as the File title item in the MenuBar, so it is also created as the child of the MenuBar. Both the menu title and the PulldownMenu are children of the MenuBar. The CascadeButton sets its XmNsubMenuId resource to the PulldownMenu so that when the button is selected, it knows which PulldownMenu to display. When you create a PulldownMenu using the simple menu creation routine, it sets this resource behind the scenes.

We also set the label of the CascadeButton using the XmNlabelString resource. This value is a compound string, just as in the simple creation function. If we had not set the label directly, the name of the widget itself would appear as the label, and we could override it with a specification in a resource file. Since we are not using the simple creation routine, we can choose whether or not we hard-code the label for the CascadeButton. After we create the items in the menu, we manage the MenuBar using XtManageChild(). The output of Example 19-5, both before and after the PulldownMenu is posted, is shown in Figure 19-8.

Figure  19-8 Output of the file_menu program


Menu Titles

The titles in a MenuBar are actually the labels of the CascadeButtons. The labels can be specified using the XmNlabelString resource, either in the application code or in a resource file. Every CascadeButton must have a submenu associated with it via the XmNsubMenuId resource. When the user selects the CascadeButton, the associated PulldownMenu is displayed. You should never attach a callback function directly to a CascadeButton in the MenuBar as it would confuse the user. Callback functions should only be attached to menu items in PulldownMenus that are posted from the MenuBar.

The PulldownMenu that is associated with a CascadeButton is created using XmCreatePulldownMenu(). This routine returns the RowColumn widget that manages the menu items. The routine creates the RowColumn as a child of a MenuShell widget. Since the routine returns the RowColumn widget, the resource list provided to the function only sets resources for the RowColumn widget, not for the MenuShell that contains it.

Menu titles should not be dynamically created or destroyed. An application should not make the MenuBar disappear or add new titles to the MenuBar while the application is running. All of the titles in the MenuBar must be available to the user when the MainWindow is visible. You can, however, deactivate an entire menu by changing the XmNsensitive resource on the CascadeButton widget that acts as its title, as discussed in the Building Pulldown Menus Section.


Menu Items

The items in a menu are actually the labels of the PushButtons that make up the menu. Unlike the File title item in the MenuBar, we chose not to use hard-coded values for the menu item strings, so the strings can be set in a resource file. While our menu only contains PushButton gadgets, a PulldownMenu can also contain ToggleButtons, Separators, and CascadeButtons.

You can install a callback routine for each of the items in a menu, or you can install an XmNentryCallback for the RowColumn widget to act on behalf of all the menu items. This resource specifies a callback function that overrides the XmNactivateCallback used by Pushbuttons and the XmNvalueChangedCallback used byToggleButtons. Using this resource generates a design that is similar to the simple menu routines described earlier. See Chapter 8, Manager Widgets, for details on this generic RowColumn resource.

As with the title items, menu items should not be dynamically created or destroyed since it may confuse the user. However, there is one exception to this guideline. If a menu contains items that keep track of a dynamic list of objects, such as the open files in a text editor, the menu items should change to reflect the current state of the application.


Mnemonics

Mnemonics help users traverse the menu system and select actual menu items without having to use the mouse. In Example 19-5, we used the XmNmnemonic resource to attach the mnemonic "F" to the File menu, which allows the user to use the key sequence ALT-F to open or close the menu without using the mouse. The XmNmnemonic resource is defined by the Label class, but it is only used by PushButtons, ToggleButtons, and CascadeButtons when these objects are used in a menu system.

A mnemonic is represented visually by the underlining of the mnemonic character in the label string. In this case, the "F" in the word "File" is underlined. If the label does not contain the mnemonic character, there is no visual feedback for the mnemonic, but it still functions. When a mnemonic is specified, the character can be either uppercase or lowercase, but the distinction only affects which letter is underlined. For operational purposes, mnemonics are case insensitive.

Our example only provided a mnemonic for the entire menu, but mnemonics can be set on menu items as well. When a PulldownMenu is displayed, the user can activate a menu item simply by typing the letter represented by its mnemonic. (The ALT key is not used once the menu is displayed.) If the user activates a menu item using a mnemonic, the callback function for the menu is called just as if the user had selected it with the mouse.

Mnemonics are set on MenuBar titles and menu items in the same way. To illustrate, let's add a mnemonic to the Exit item in our File menu. We could, as in Example 19-5, set the mnemonic directly in the creation of the item, as follows:

n = 0;
XtSetArg (args[n], XmNmnemonic, `x'); n++;
push_b = XmCreatePushButtonGadget (file_w, "Exit", args, n);
Strictly speaking, since the internal representation of an XmNmnemonic is as a KeySym, and not a char, the following method of assigning a mnemonic to a widget, using the Xt conversion mechanisms, is preferred:

XrmValue from_value, to_value; /* For resource conversion */
... 
n = 0;
from_value.addr = "x";
from_value.size = strlen(from_value.addr) + 1;
to_value.addr = NULL;
XtConvertAndStore (file_w, XmRString, &from_value, XmRKeySym, &to_value);
if (to_value.addr) {
    XtSetArg (args[n], XmNmnemonic, (*((KeySym*) to_value.addr))); n++;
}
push_b = XmCreatePushButtonGadget (file_w, "Exit", args, n);
While these methods accomplish the task, one problem with them is that the mnemonic is hard-coded in the widget, while the label is not. Consider the following resource specification in a resource file:

*Exit.labelString: Quit
This resource sets the label for the item button to "Quit", but since the mnemonic for the button is hard-coded to "x", there is visual feedback, and the mnemonic itself is counter-intuitive.

The best way to handle this situation is to specify both the label string and the mnemonic in the same place: a resource file or application code. For example:

*Exit.labelString: Exit
*Exit.mnemonic: x
Setting both of these resources in the same way helps ensure that an application has a consistent interface.


Accelerators

The purpose of menu accelerators is to provide the user with the ability to activate menu items in a PulldownMenu without having to display the menu at all. In Figure 19-1, the Quit menu item displayed the accelerator Ctrl+C to indicate that the user could press the CTRL-C keyboard sequence to activate that menu item and quit the application.

To install a accelerator on a menu item, use the XmNaccelerator resource to specify the accelerator translation and XmNacceleratorText to provide visual feedback to the user.8These resources are defined by the Label class, but they only work for PushButtons and ToggleButtons in menus. The syntax for the accelerator is exactly the same as for a translation table, except that you do not specify an action function with the event sequence. The accelerator for the Quit button in Figure 19-1 is specified as "Ctrl<Key>C". (For information on how to specify translation tables, see Volume 4, X Toolkit Intrinsics Programming Manual.

However, the string that is displayed for the accelerator is not the same as the accelerator translation because it would be confusing for most users. Instead, you should display something like "^C", "Ctrl-C", or "Ctrl+C", as these make it reasonably clear what the user is expected to type. (The latter is the convention recommended by the Motif Style Guide, though all three forms are frequently used.) Since this resource specifies displayable text, you cannot use a common C string; the text must be given as a compound string.

For example, the following code demonstrates how to install an accelerator for the Exit button in Example 19-5.

XmString accel_text = XmStringCreateLocalized ("Ctrl+C");
...
n = 0;
XtSetArg (args[n], XmNaccelerator, "Ctrl<Key>C");   n++;
XtSetArg (args[n], XmNacceleratorText, accel_text); n++;
push_b = XmCreatePushButtonGadget (file_w, "Exit", args, n);
XmStringFree (accel_text);
As with mnemonics, the resources for the accelerator itself and the text used to display the accelerator can either be set directly in application code or specified in a resource file. Both of the resources should be specified in the same way, so that they are always consistent.


The Help Menu

Motif specifies various ways for the user to get help. She can use the HELP or F1 keys on the keyboard, the Help button in a dialog box, or the Help title on the MenuBar. This title provides the highest level of help for your application, so it should not provide too much detail about lower-level functions in the program. When you create a PulldownMenu for this title, it should provide items that give the user access to the help system. Figure 19-9 shows a common Help menu.

Figure  19-9 A Help menu from the MenuBar

The choices shown in Figure 19-9 are recommended by the Motif Style Guide; if they apply to your application, you should use them.There is usually an item on the Help menu that gives the user a brief overview of how to use the help system. You should consult the Motif Style Guide for details on what kind of help each of the above selections should provide. It is usually a good idea to have an item that displays an index of the type of help that is available in an application. An example of help index dialog is shown in Figure 19-10. See Chapter 27, Advanced Dialog Programming, for a discussion of help dialogs.

Figure  19-10 A Help index dialog

Creating a Help menu is just like creating any other menu, except that once you have created the CascadeButton, you should set the XmNmenuHelpWidget resource for the MenuBar. This resource specifies which CascadeButton is placed to the far right in the MenuBar, which is where the Style Guide states that the Help menu must be positioned. Example 19-6 contains a routine that demonstrates how to build a Help menu and attach it to the MenuBar. In this example, we present an alternate approach to creating MenuBar titles and their associated PulldownMenus.

Example  19-6 The BuildHelpMenu() routine

void BuildHelpMenu (Widget menu_b)
{
    void         do_help(Widget, XtPointer, XtPointer);
    Widget       help_m, widget;
    Arg          args[4];
    int          i, n;
    static char  *h_items[] = { "On Context", NULL, "On Help", "On Window", 
                                "On Keys", "Index", "Tutorial", "On Version" };

    /* Help menu */
    help_m = XmCreatePulldownMenu (menu_b, "HelpPullDown", NULL, 0);

    n = 0;
    XtSetArg (args[n], XmNsubMenuId, help_m); n++;
    widget = XmCreateCascadeButton (menu_b, "Help", args, n);
    XtManageChild (widget);

    /* tell the MenuBar that this is the help widget */
    XtVaSetValues (menu_b, XmNmenuHelpWidget, widget, NULL);

    /* Now add the menu items to the pulldown menu */
    for (i = 0; i < XtNumber (h_items); i++) {
        if (h_items[i] != NULL) {
            widget = XmCreatePushButtonGadget (help_m, h_items[i], NULL, 0);
            XtAddCallback (widget, XmNactivateCallback, do_help, 
                                (XtPointer) h_items[i]);
        }
        else
            widget = XmCreateSeparatorGadget (help_m, "sep", NULL, 0);

        XtManageChild (widget);
    }
}
Much of the work required to create a PulldownMenu is involved in creating the menu items. We can optimize the code by using a loop that creates individual items based on the names provided in a static array. If you want to add a new help item to the list, you just need to add its name to the h_items list. A NULL entry causes a Separator gadget to be added to the menu. In Example 19-6, we specify the same callback function for each item in the menu; the client_data is the same as the name of the menu item. In the General Menu Creation Techniques Section we expand on this approach to build arbitrary menus for the MenuBar.


Sensitivity

As we mentioned earlier, MenuBar titles and menu items should not be dynamically created or destroyed. They may, however, be activated or deactivated using XtSetSensitive(). When a CascadeButton or a menu item is insensitive, it is greyed out, and the user is unable to display the associated menu or activate the menu item.

For CascadeButtons, insensitivity has the additional effect of preventing the user from accessing any of the items on the associated menu, including access through mnemonics and accelerators, since the menu cannot be displayed. The menu and all its items are completely unavailable until the sensitivity of the CascadeButton is reset. An alternate way to disable an entire menu is to set the PulldownMenu pane insensitive. This approach has the advantage of still allowing the user to display the menu and see all the items, while making the items unavailable.

For example, take an editor program. If the user is not editing a file, it doesn't make sense to have the Save item in the File menu selectable. Once the user starts editing a file, the Save button is sensitized so that the user can select it. Since the user cannot select the item until its sensitivity is reset, it is important that the application do so at the appropriate time. Another less realistic example, but one that we can demonstrate, involves a menu item that pops up a dialog. As long as that dialog is up, the user cannot reselect the menu item again. For purposes of this demonstration, let's say that the Open item pops up a FileSelectionDialog and desensitizes itself. When the dialog is dismissed, the menu item is re-sensitized.9

To implement this behavior, we specify a callback routine for the Open menu item that creates a FileSelectionDialog and sets the item insensitive. We also specify a callback routine for the dialog box that resets the menu item's sensitivity. The code fragment in Example 19-7 shows these callback routines.

Example  19-7 The reset_sensitive() and open_callback() routines

/* reset_sensitive() -- generalized routine that resets the
** sensitivity on the widget passed as the client_data parameter
** in a call to XtAddCallback().
*/
void reset_sensitive (Widget w, XtPointer client_data, XtPointer call_data)
{
    Widget reset_widget = (Widget) client_data;

    XtSetSensitive (reset_widget, True);
}

/* open_callback() -- the callback routine for when the "Open"
** menu item is selected from the "File" title in the MenuBar.
*/
void open_callback (Widget menu_item, XtPointer client_data,
                    XtPointer call_data)
{
    Widget dialog, parent = menu_item;

    /* Get the window manager shell widget associated with item */
    while (!XtIsWMShell (parent))
        parent = XtParent (parent);

    /* turn off the sensitivity for the Open button... */
    XtSetSensitive (menu_item, False);

    dialog = XmCreateFileSelectionDialog (parent, "files", NULL, 0);

    /* Add callback routines to respond to OK button selection here. */
    /* Make sure that if the dialog is popped down or destroyed, the
    ** menu_item's sensitivity is reset.
    */
    XtAddCallback (XtParent (dialog), /* dialog's _parent_ */
    XmNpopdownCallback, reset_sensitive, (XtPointer) menu_item);
    XtAddCallback (dialog, XmNdestroyCallback,
                        reset_sensitive, (XtPointer) menu_item);
    XtManageChild (dialog);
}
The open_callback() function is called whenever the user activates the Open menu item on the File menu. The first thing open_callback() does is find the nearest WMShell widget associated with the menu item. We do not want the MenuShell here, as we need a non-transient widget to act as the parent for the FileSelectionDialog. If the menu item is used as the parent for the dialog, when the menu is popped down, the dialog is also popped down because it is a secondary window.

We set the menu item's sensitivity to False, which prevents the user from selecting the item again. In order to be notified when the FileSelectionDialog is dismissed, we add callback routines for XmNpopdownCallback and XmNdestroyCallback. In both cases, the Open menu item needs to be reset so that the user can select it again. The only thing in open_callback() is a callback function that opens the selected file when the user selects the OK button. This functionality is beyond the scope of this chapter; see Chapter 6, Selection Dialogs, for details.


Tear-Off Menus

Motif provides a feature that allows menus to be torn off and placed in separate windows. From the user's perspective, tear-off menus make it easy to make repeated menu selections. Normally, when the user posts a menu, it is only displayed until she makes a selection, and then it is removed. If the menu has been torn off, however, it is displayed in a separate window, and the user can make as many selections as she wants without having to repost it each time.

Tear-off behavior is provided for all of the Motif menu types, but the behavior is disabled by default. When tear-off functionality is enabled in a menu, the first item in the menu is a tear-off button. The button displays a dashed line to indicate that the user can tear off the menu, much as she would tear a coupon out of a newspaper. If the user selects the tear-off button, the menu is placed in a separate window with limited window manager decorations. The window can be moved, so the user can position it in a convenient location. The menu remains torn off until the user cancels the menu by pressing the ESCAPE key within the window.

Tear-off functionality is controlled by the XmNtearOffModel resource of the RowColumn widget. This resource is only valid when the RowColumn is being used as a PulldownMenu or a PopupMenu. The resource can have one of the following values: XmTEAR_OFF_ENABLED or XmTEAR_OFF_DISABLED. By default, the resource is set to XmTEAR_OFF_DISABLED, so if you want to provide tear-off functionality in the menus in your application, you must set the resource for all of your menu panes. Figure 19-4 showed a PulldownMenu both before and after being torn off.

You can use the following resource specification to enable tear-off functionality for all menus:10

*tearOffModel: TEAR_OFF_ENABLED
Some applications use menus in such a way that they need to keep track of when the menu is popped up and popped down. For example, an application might use some ToggleButtons in a PulldownMenu to allow the user to set state variables for the program. If the application also provides another interface for changing the variables, such as a command-line, the application needs to know when the menu is popped up so that it can make sure the ToggleButtons are set appropriately. If you think that your application state may be affected by the user enabling tear-off functionality in her resource files, you should either explicitly disable the feature in your code, or install some callbacks to keep track of the tear-off state.

The RowColumn widget provides two callback resources that allow an application to keep track of tear-off menus. The XmNtearOffMenuActivateCallback routine is called when a menu is torn off; XmNtearOffMenuDeactivateCallback is called when the torn-off menu is dismissed. These callbacks provide a way for you to perform any special processing that is necessary for handling tear-off menus.

Motif also provides access to the tear-off button with the XmGetTearOffControl() routine. This routine takes a menu pane and returns the widget ID of the tear-off button in the menu, if there is one. Otherwise the routine returns NULL.The tear-off button has a Separator-like appearance; you can specify its background, foreground, and top and bottom shadow colors using the standard resources, as well as the XmNseparatorType resource. You can also set these resources in a resource file using the name of the button, which is TearOffControl.

In Motif 2.0 and later, the title of the dialog which contains the torn-off menu can be set through the RowColumn resource XmNtearOffTitle. This resource is a compound strings value, and you are referred to Chapter 25, Compound Strings, for more information on.

General Menu Creation Techniques

Now we have addressed each of the fundamental elements of the MenuBar and the resources used to provide the user with the appropriate feedback. Using this information, we can generalize the way we build MenuBars, enabling us to create arbitrarily large MenuBars and PulldownMenus using a substantially smaller amount of code.

In the examples that follow, we use many of the recommended elements for a standard Motif MenuBar. You can adjust the algorithms and data structures to fit the needs of your own application. Although we use hard-coded values for widget resources, this technique is by no means a requirement, nor should it be construed as recommended usage. If you choose to specify resources in a resource file, you should write an application defaults file that contains the appropriate resource values.


Building Pulldown Menus

Let's begin by identifying each of the attributes of a menu item:

Label

Mnemonic

Accelerator

Accelerator text

Callback routine

Callback data

Using this information, we can construct a data structure that describes all of the important aspects of a menu item. We define the MenuItem structure as follows:

typedef struct _menu_item {
    char         *label;          /* the label for the item */
    WidgetClass  *class;          /* pushbutton, label, separator,... */ 
    char         mnemonic;        /* mnemonic; NULL if none */
    char         *accelerator;    /* accelerator; NULL if none */
    char          *accel_text;     /* to be converted to compound string */
    void         (*callback)();   /* routine to call; NULL if none */
    XtPointer    callback_data;   /* client_data for callback() */
} MenuItem;
To create a PulldownMenu, all we need to do is initialize an array of MenuItem structures and pass it to a routine that iterates through the array and creates the items using the appropriate information. For example, the following declaration describes the elements for a File menu:

MenuItem file_items[] = {
{"New", &xmPushButtonGadgetClass, 'N', NULL, NULL, do_open, NEW},
{"Open...", &xmPushButtonGadgetClass, 'O', NULL, NULL, do_open, OPEN},
{"Save", &xmPushButtonGadgetClass, 'S', NULL, NULL, do_save, SAVE},
{"Save As...", &xmPushButtonGadgetClass, 'A',  NULL, NULL, do_save, SAVE_AS},
{"Print...", &xmPushButtonGadgetClass, 'P', NULL, NULL, do_print, NULL},
{"", &xmSeparatorGadgetClass, NULL, NULL, NULL, NULL, NULL},
{"Exit", &xmPushButtonGadgetClass, 'x', "Ctrl<Key>C", "Ctrl+C", do_quit, NULL},
{NULL, NULL, NULL, NULL, NULL, NULL, NULL}
};
Each element in the MenuItem data structure is filled with default values for each menu item. If a resource value is not meaningful, or is not going to be hard-coded, we initialize the field to NULL. If you don't need a callback function or client data for an item, the field may be set to NULL. The only field that cannot be NULL in a valid entry is the widget class. The final terminating NULL entry indicates the end of the list.

We have not specified any accelerators except for the Exit item. The Separator gadget is completely unspecified, since none of the resources even apply to Separators. This design makes modification and maintenance very simple. If you want to add an accelerator for the Save item, all you need to do is change the appropriate fields in the data structure, instead of having to search through the source code looking for where that item is created.

One particular point of interest is the way the WidgetClass field is initialized. It is declared as a pointer to a widget class rather than just a widget class, so we initialize the field with the address of the widget class variable that is declared in the widget's header file. The use of &xmPushButtonGadgetClass is one such example. The structure must be initialized this way because the compiler requires a specific value in order to initialize a static data structure. The xmPushButtonWidgetClass pointer does not have a value until the program is actually running, but the address of the variable does have a value. Once the program is running, the pointer can be dereferenced to access the real PushButton widget class.

Now we can write a routine that uses the MenuItem data structure to create a PulldownMenu. The BuildPulldownMenu() function is shown in Example 19-8. The routine loops through each element in an array of pre-initialized MenuItem structures and creates menu items based on the information.

Example  19-8 The BuildPulldownMenu() routine

Widget BuildPulldownMenu (Widget parent, char *menu_title,
                            char menu_mnemonic, Boolean tear_off,
                            MenuItem *items)
{
    Widget       pulldown, cascade, widget;
    int          i, n;
    XmString     str;
    Arg          args[4];

    pulldown = XmCreatePulldownMenu (parent, "_pulldown", NULL, 0);

    if (tear_off)
        XtVaSetValues (pulldown, XmNtearOffModel, XmTEAR_OFF_ENABLED, NULL);

    str = XmStringCreateLocalized (menu_title);
    n = 0;
    XtSetArg (args[n], XmNsubMenuId, pulldown); n++;
    XtSetArg (args[n], XmNlabelString, str); n++;
    XtSetArg (args[n], XmNmnemonic, menu_mnemonic); n++;
    cascade = XmCreateCascadeButton (parent, menu_title, args, n);
    XtManageChild (cascade);
    XmStringFree (str);

    /* Now add the menu items */    
    for (i = 0; items[i].label != NULL; i++) {
        widget = XtVaCreateManagedWidget (items[i].label, *items[i].class, 
                                        pulldown, NULL);
        if (items[i].mnemonic) 
            XtVaSetValues (widget, XmNmnemonic, items[i].mnemonic, NULL);
        if (items[i].accelerator) {
            str = XmStringCreateLocalized (items[i].accel_text);
            XtVaSetValues (widget, XmNaccelerator, items[i].accelerator, 
                                XmNacceleratorText, str, NULL);
            XmStringFree (str);
        }
        if (items[i].callback) 
            XtAddCallback (widget, XmNactivateCallback, items[i].callback, 
                                    (XtPointer) items[i].callback_data);
    }

    return cascade;
}
The function takes five parameters. parent is a handle to a MenuBar widget that must have already been created, menu_title indicates the title of the menu, menu_mnemonic specifies the mnemonic, tear_off indicates whether or not the menu can be torn off, and items is an array of MenuItem structures.

The first thing the routine does is create a PulldownMenu. Since the name of this widget is not terribly important, we use a predefined name, prefixed with an underscore, to indicate that the name is not intended to be referenced in a resource file. This use of the underscore is our own convention, by the way, not one adopted by the X Toolkit Intrinsics. We came up with this "unwritten rule" because Xt has no such naming conventions for widgets that do not wish to have their resources specified externally.

After creating the PulldownMenu, the routine creates the CascadeButton that acts as the title for the menu on the MenuBar. The name of the widget is taken from the second parameter, menu_title. The routine also sets the mnemonic and the XmNtearOffModel resource at this point. All MenuBar titles should have mnemonics associated with them.

Now the function loops through the array of MenuItem structures creating menu items until it finds an entry with a NULL name. We use this value as an end-of-menu indicator in our initialization. When each widget is created, the mnemonic, accelerator, and callback function are added only if they are specified in the MenuItem structure.

BuildPulldownMenu() must be called from another function that passes the appropriate data structures and other parameters. In our design, this would be the routine that creates the MenuBar itself. Example 19-9 shows the code for the CreateMenuBar() routine. This simple function creates a MenuBar widget, calls BuildPulldownMenu() for each menu, manages the MenuBar, and returns it to the calling function.

Example  19-9 The CreateMenuBar() routine

Widget CreateMenuBar (Widget MainWindow)
{
    Widget mbar, widget;
    Widget BuildPulldownMenu (Widget, char *, char, Boolean , MenuItem *);

    mbar = XmCreateMenuBar (MainWindow, "MenuBar", NULL, 0);

    (void) BuildPulldownMenu (mbar, "File", 'F', True, file_items);
    (void) BuildPulldownMenu (mbar, "Edit", 'E', True, edit_items);
    (void) BuildPulldownMenu (mbar, "View", 'V', True, view_items);
    (void) BuildPulldownMenu (mbar, "Options", 'O', True, options_items);
    widget = BuildPulldownMenu (mbar, "Help", 'H', True, help_items);

    XtVaSetValues (mbar, XmNmenuHelpWidget, widget, NULL);
    XtManageChild (mbar);
    return mbar;
}
Each call to BuildPulldownMenu() passes an array of pre-initialized MenuItem structures. The Help menu is a special case, so we set the XmNmenuHelpWidget resource to let the MenuBar know which item it is. By setting the resource to the CascadeButton returned by the function, the MenuBar knows that this button should be placed to the far right. The only parameter to the CreateMenuBar() function is the MainWindow widget that is the parent of the MenuBar that is returned.


Building Cascading Menus

We can add pullright menus to our menu creation methodology quite easily by adding to the MenuItem data structure and making a slight modification to the CreatePulldownMenu() function. As we learned from the simple menu creation routines, a cascading menu is really a PulldownMenu that is associated with a CascadeButton. We also know that we can attach a menu to a CascadeButton by setting the XmNsubMenuId resource to the handle of the PulldownMenu. We begin by modifying the MenuItem structure as follows:

typedef struct _menu_item {
    char               *label;        /* the label for the item */
    WidgetClass        *class;        /* pushbutton, label, separator... */
    char               mnemonic;      /* mnemonic; NULL if none */
    char               *accelerator;  /* accelerator; NULL if none */
    char               *accel_text;   /* to convert to compound string */
    void               (*callback)(); /* routine to call; NULL if none */
    XtPointer          callback_data; /* client_data for callback() */
    struct _menu_item  *subitems; /* pullright menu items, if not NULL */
} MenuItem;
The new field at the end of the structure is a pointer to another array of MenuItem structures. If this pointer is not NULL, the menu item has a cascading submenu that is described by subitems. Example 19-10 shows an example of creating a cascading menu. This program uses a modified version of BuildPulldownMenu() that calls itself to create cascading menus.11

Example  19-10 The build_menu.c program

/* build_menu.c -- Demonstrate the BuildPulldownMenu() routine and
** how it can be used to build pulldown -and- pullright menus.
** Menus are defined by declaring an array of MenuItem structures.
*/

#include <Xm/RowColumn.h>
#include <Xm/MainW.h>
#include <Xm/DrawingA.h>
#include <Xm/CascadeBG.h>
#include <Xm/PushB.h>
#include <Xm/PushBG.h>
#include <Xm/ToggleB.h>
#include <Xm/ToggleBG.h>

typedef struct _menu_item {
    char              *label;        /* the label for the item */
    WidgetClass       *class;        /* pushbutton, label, separator... */
    char              mnemonic;      /* mnemonic; NULL if none */
    char              *accelerator;  /* accelerator; NULL if none */
    char                 *accel_text;   /* to be converted to compound string */
    void              (*callback)(); /* routine to call; NULL if none */
    XtPointer         callback_data; /* client_data for callback() */
    struct _menu_item *subitems;     /* pullright menu items, if not NULL */
} MenuItem;

/* Pulldown menus are built from cascade buttons, so this function
** also includes pullright menus. Create the menu, the cascade button
** that owns the menu, and then the submenu items.
*/
Widget BuildPulldownMenu (Widget parent, char *menu_title,
                        char menu_mnemonic,
                        Boolean tear_off,
                        MenuItem *items)
{
    Widget       PullDown, cascade, widget;
    int          i;
    XmString     str;
    Arg          args[8];
    int          n;

    PullDown = XmCreatePulldownMenu (parent, "_pulldown", NULL, 0);

    if (tear_off) 
        XtVaSetValues (PullDown, XmNtearOffModel, XmTEAR_OFF_ENABLED, NULL);
    str = XmStringCreateLocalized (menu_title);
    n = 0;
    XtSetArg (args[n], XmNsubMenuId, PullDown); n++;
    XtSetArg (args[n], XmNlabelString, str); n++;
    XtSetArg (args[n], XmNmnemonic, menu_mnemonic); n++;
    cascade = XmCreateCascadeButtonGadget (parent, menu_title, args, n);
    XtManageChild (cascade);
    XmStringFree (str);

    /* Now add the menu items */
    for (i = 0; items[i].label != NULL; i++) {
        /* If subitems exist, create the pull-right menu by calling this
        ** function recursively. Since the function returns a cascade
        ** button, the widget returned is used. 
        */
        if (items[i].subitems)
            widget = BuildPulldownMenu (PullDown, items[i].label, items[i].
                                    mnemonic, tear_off, items[i].subitems);
        else
            widget = XtVaCreateManagedWidget (items[i].label,
                                            *items[i].class,
                                            PullDown,
                                            NULL);

        /* Whether the item is a real item or a cascade button with a
        ** menu, it can still have a mnemonic.
        */
        if (items[i].mnemonic)
            XtVaSetValues (widget, XmNmnemonic, items[i].mnemonic, NULL);

        /* any item can have an accelerator, except cascade menus. But,
        ** we don't worry about that; we know better in our declarations.
        */
        if (items[i].accelerator) {
            str = XmStringCreateLocalized (items[i].accel_text);
            XtVaSetValues (widget, XmNaccelerator, items[i].accelerator, 
                                        XmNacceleratorText, str, NULL);
            XmStringFree (str);
        }

        if (items[i].callback) {
            String resource;

            if (XmIsToggleButton (widget) || (XmIsToggleButtonGadget (widget))
                resource = XmNvalueChangedCallback;
            else
                resource = XmNactivateCallback;
 
            XtAddCallback (widget, resource, items[i].callback,
                            (XtPointer) items[i].callback_data);
        }
    }

    return cascade;
}

/* callback functions for menu items declared later... */
void set_weight (Widget widget, XtPointer client_data, XtPointer call_data)
{
    int weight = (int) client_data;
    printf ("Setting line weight to %d\n", weight);
}

void set_color (Widget widget, XtPointer client_data, XtPointer call_data)
{
    char *color = (char *) client_data;
    printf ("Setting color to %s\n", color);
}

void set_dot_dash (Widget widget, XtPointer client_data, XtPointer call_data)
{
    int dot_or_dash = (int) client_data;
    printf ("Setting line style to %s\n", dot_or_dash? "dot" : "dash");
}

MenuItem weight_menu[] = {
    { " 1 ", &xmPushButtonGadgetClass, `1', NULL, NULL, set_weight, 
                (XtPointer) 1, (MenuItem *) NULL },
    { " 2 ", &xmPushButtonGadgetClass, `2', NULL, NULL, set_weight, 
                (XtPointer) 2, (MenuItem *) NULL },
    { " 3 ", &xmPushButtonGadgetClass, `3', NULL, NULL, set_weight, 
                (XtPointer) 3, (MenuItem *) NULL },
    { " 4 ", &xmPushButtonGadgetClass, `4', NULL, NULL, set_weight, 
                (XtPointer) 4, (MenuItem *) NULL },
    { NULL. NULL, NULL, NULL, NULL, NULL, NULL, NULL }
};

MenuItem color_menu[] = {
    { "Cyan", &xmPushButtonGadgetClass, `C', "Alt<Key>C", "Alt+C", set_color, 
                (XtPointer) "cyan", (MenuItem *) NULL },
    { "Yellow", &xmPushButtonGadgetClass, `Y', "Alt<Key>Y", "Alt+Y", set_color, 
                (XtPointer) "yellow", (MenuItem *) NULL },
    { "Magenta", &xmPushButtonGadgetClass, `M', "Alt<Key>M", "Alt+M", 
                set color,(XtPointer) "magenta", (MenuItem *) NULL },
    { "Black", &xmPushButtonGadgetClass, `B', "Alt<Key>B", "Alt+B", set_color, 
                (XtPointer) "black", (MenuItem *) NULL }, 
    { NULL. NULL, NULL, NULL, NULL, NULL, NULL, NULL }
};

MenuItem style_menu[] = {
    { "Dash", &xmPushButtonGadgetClass, `D', NULL, NULL, set_dot_dash, 
                (XtPointer) 0, (MenuItem *) NULL },
    { "Dot", &xmPushButtonGadgetClass, `o', NULL, NULL, set_dot_dash, 
                (XtPointer) 1, (MenuItem *) NULL },
    { NULL. NULL, NULL, NULL, NULL, NULL, NULL, NULL }
};

MenuItem drawing_menus[] = {
    { "Line Weight", &xmCascadeButtonGadgetClass, `W', NULL, NULL, 0, 0, 
                weight_menu },
    { "Line Color", &xmCascadeButtonGadgetClass, `C', NULL, NULL, 0, 0, 
                color_menu },
    { "Line Style", &xmCascadeButtonGadgetClass, `S', NULL, NULL, 0, 0, 
                style_menu },
    { NULL. NULL, NULL, NULL, NULL, NULL, NULL, NULL }
};

main (int argc, char *argv[])
{
    Widget         toplevel, main_w, menubar, drawing_a;
    XtAppContext   app;
    Arg            args[4];
    int            n;

    XtSetLanguageProc (NULL, NULL, NULL);
    toplevel = XtVaOpenApplication (&app, "Demos", NULL, 0, &argc, argv, NULL, 
                                    sessionShellWidgetClass, NULL);

    /* Create a MainWindow widget that contains a DrawingArea in
    ** its work window.
    */
    n = 0;
    XtSetArg (args[n], XmNscrollingPolicy, XmAUTOMATIC); n++;
    main_w = XmCreateMainWindow (toplevel, "main_w", args, n);
    menubar = XmCreateMenuBar (main_w, "menubar", NULL, 0);
    BuildPulldownMenu (menubar, "Lines", `L', True, drawing_menus);
    XtManageChild (menubar);

    /* Create a DrawingArea -- no actual drawing will be done. */
    n = 0;
    XtSetArg (args[n], XmNwidth, 500);  n++;
    XtSetArg (args[n], XmNheight, 500); n++;
    drawing_a = XmCreateDrawingArea (main_w, "drawing_a", args, n);
    XtManageChild (drawing_a);
    XtManageChild (main_w);
    XtRealizeWidget (toplevel);
    XtAppMainLoop (app);
}
The majority of this program is composed of the new version of BuildPulldownMenu() and the menu and submenu declarations. All the menus and menu items are declared in reverse order because the cascading menu declaration must exist before the menu is actually referenced. The output of the program is shown in Figure 19-11.

Figure  19-11 Output of the build_menu program

All we have to do to get BuildPulldownMenu() to create a cascading menu is add code that checks whether or not the current menu has a submenu. If it does, the routine calls itself to create the submenu. Because the function creates and returns a CascadeButton, the return value can be used as the menu item in the menu that is currently being built. We have to create the cascading menu first because it has to exist before it can be attached to a CascadeButton. Recursion handles this problem for us by creating the deepest submenus first, which ensures that all the necessary submenus are built before their CascadeButtons require them.

We also added support for ToggleButtons to this version of BuildPulldownMenu(), even though our menus do not contain any ToggleButtons. The only change that we have to make here involves the callback function. Since ToggleButtons have an XmNvalueChangedCallback, while PushButtons have an XmNactivateCallback, we check the class of the item being added and specify the appropriate callback resource in our call to XtAddCallback().


Building Popup Menus

To further demonstrate the flexibility of our design and to exploit the similarities between PulldownMenus, PopupMenus, and cascading menus, we can easily modify the BuildPulldownMenu() routine to support any of these menu types. We only need to specify a new parameter indicating which of the two menu types to use. Since Motif already defines the values XmMENU_PULLDOWN and XmMENU_POPUP in <Xm/Xm.h>, we use those values. We have also given the function a more generic name, BuildMenu(), as shown in Example 19-11.12

Example  19-11 The BuildMenu() routine

Widget BuildMenu (Widget parent, int menu_type, char *menu_title,
                char menu_mnemonic, Boolean tear_off, MenuItem *items)
{
    Widget       menu, cascade, widget;
    int          i;
    XmString     str;
    Arg          args[8];
    int          n;

    if (menu_type == XmMENU_PULLDOWN)
        menu = XmCreatePulldownMenu (parent, "_pulldown", NULL, 0);
    else {
        n = 0;
        XtSetArg (args[n], XmNpopupEnabled, XmPOPUP_AUTOMATIC_RECURSIVE); n++;
        menu = XmCreatePopupMenu (parent, "_popup", args, n);
    }

    if (tear_off) 
        XtVaSetValues (menu, XmNtearOffModel, XmTEAR_OFF_ENABLED, NULL);

    if (menu_type == XmMENU_PULLDOWN) {
        str = XmStringCreateLocalized (menu_title);
        n = 0;
        XtSetArg (args[n], XmNsubMenuId, menu);         n++;
        XtSetArg (args[n], XmNlabelString, str);        n++;
        XtSetArg (args[n], XmNmnemonic, menu_mnemonic); n++;
        cascade = XmCreateCascadeButtonGadget (parent, menu_title, args, n);
        XtManageChild (cascade);
        XmStringFree (str);
    }

    /* Now add the menu items */
    for (i = 0; items[i].label != NULL; i++) {
        /* If subitems exist, create the pull-right menu by calling this
        ** function recursively. Since the function returns a cascade
        ** button, the widget returned is used. 
        */
        if (items[i].subitems)
            widget = BuildMenu (menu, XmMENU_PULLDOWN,
                                items[i].label,
                                items[i].mnemonic,
                                tear_off,
                                items[i].subitems);
        else
            widget = XtVaCreateManagedWidget (items[i].label,
                                *items[i].class,
                                menu,
                                NULL);

        /* Whether the item is a real item or a cascade button with a
        ** menu, it can still have a mnemonic.
        */
        if (items[i].mnemonic)
            XtVaSetValues (widget, XmNmnemonic, items[i].mnemonic, NULL);

        /* any item can have an accelerator, except cascade menus. But,
        ** we don't worry about that; we know better in our declarations.
        */
        if (items[i].accelerator) {
            str = XmStringCreateLocalized (items[i].accel_text);
            XtVaSetValues (widget, XmNaccelerator, items[i].accelerator, 
                            XmNacceleratorText, str, NULL);
            XmStringFree (str);
        }

        if (items[i].callback) {
            String resource;

            if (XmIsToggleButton (widget) || (XmIsToggleButtonGadget (widget))
                resource = XmNvalueChangedCallback;
            else
                resource = XmNactivateCallback;

            XtAddCallback (widget, resource, items[i].callback,
                                (XtPointer) items[i].callback_data);
        }
    }

    return (menu_type == XmMENU_PULLDOWN? cascade: menu);
}
All of the original functionality is maintained; we only added a few lines to support popup menus. Namely, when XmMENU_POPUP is passed as the menu_type parameter, the function XmCreatePopupMenu() is called, and the menu itself is returned. Otherwise the routine returns a CascadeButton. If any of the menu items have cascading menus, we continue what we were doing before for submenus.

Now we can build PopupMenus, but what we really need to talk about is when you should use PopupMenus in an application. The Motif Style Guide has very little to say about when and how popup menus should be used. One guideline is that PopupMenus should only be used as a redundant means of activating application functionality, since they do not make themselves apparent to the user. The single requirement is that PopupMenus use the third mouse button, which leads to the question: how do you get the necessary events on an arbitrary widget so that you can pop up a menu?

The design of PopupMenus in the Motif 1.2 toolkit requires you to dig into lower-level Xt event-handling mechanisms in order to post a PopupMenu. In Motif 2.0 and later, this is not necessary: we can set the XmNpopupEnabled resource to XmPOPUP_AUTOMATIC or XmPOPUP_AUTOMATIC_RECURSIVE on the menu, and the toolkit does the rest. In Example 19-11, we specified the value as XmPOPUP_AUTOMATIC_RECURSIVE so that widgets in the hierarchy can inherit our popup menus: we may wish to display a popup menu for a Gadget, and inherit the popup from the manager parent. If we need to choose between a number of popup menus at any given point, we only need to register an XmNpopupHandlerCallback on the widget where the popup is required. Example 19-12 demonstrates how to display a PopupMenu for an arbitrary widget.

In the example, we parent the popup menu off the RowColumn rowcol: pressing the 3rd mouse button over either the PushButton widget or gadget children displays the menu. If we only wanted to display the menu for the PushButton widget, we would parent the menu off the button itself. This program uses the BuildMenu() routine from Example 19-11, so we do not show it in this example.13

Example  19-12 The popups.c program

/* popups.c -- demonstrate the use of a popup menus in an arbitrary
** widget. Display two PushButtons. The second one has a popup
** menu attached to it that is activated with the third
** mouse button.
*/

#include <Xm/LabelG.h>
#include <Xm/PushBG.h>
#include <Xm/PushB.h>
#include <Xm/ToggleBG.h>
#include <Xm/ToggleB.h>
#include <Xm/SeparatoG.h>
#include <Xm/RowColumn.h>
#include <Xm/FileSB.h>
#include <Xm/CascadeBG.h>

Widget       toplevel;
extern void  exit(int);
void         open_dialog_box(Widget, XtPointer, XtPointer);

/* callback for pushbutton activation */
void put_string (Widget w, XtPointer client_data, XtPointer call_data)
{
    String str = (String) client_data;
    puts (str);
}

typedef struct _menu_item {
    char               *label;
    WidgetClass        *class;
    char               mnemonic;
    char               *accelerator;
    char               *accel_text;
    void               (*callback)();
    XtPointer          callback_data;
    struct _menu_item  *subitems;
} MenuItem;

MenuItem file_items[] = {
    {"File Items", &xmLabelGadgetClass, NULL, NULL, NULL, NULL, NULL, NULL},
    {"_sep1", &xmSeparatorGadgetClass, NULL, NULL, NULL, NULL, NULL, NULL},
    {"New", &xmPushButtonGadgetClass, 'N', NULL, NULL, put_string, "New", 
                            NULL},
    {"Open...", &xmPushButtonGadgetClass, 'O', NULL, NULL, open_dialog_box, 
                            (XtPointer) XmCreateFileSelectionDialog, NULL},
    {"Save", &xmPushButtonGadgetClass, 'S', NULL, NULL, put_string, "Save", 
                            NULL},
    {"Save As...", &xmPushButtonGadgetClass, 'A', NULL, NULL, open_dialog_box, 
                            (XtPointer) XmCreateFileSelectionDialog, NULL},
    {"Exit", &xmPushButtonGadgetClass, 'x', "Ctrl<Key>C", "Ctrl+C", exit, NULL, 
                            NULL},
    {NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL}
};

/* build_menu.c -- Demonstrate the BuildMenu() routine and
** how it can be used to build pulldown -and- pullright menus.
** Menus are defined by declaring an array of MenuItem structures.
*/

/* Pulldown menus are built from cascade buttons, so this function
** also includes pullright menus. Create the menu, the cascade button
** that owns the menu, and then the submenu items.
*/
Widget BuildMenu (Widget parent, int menu_type, char *menu_title,
                char menu_mnemonic, Boolean tear_off, MenuItem *items)
{
    Widget       menu, cascade, widget;
    int          i;
    XmString     str;
    Arg          args[8];
    int          n;

    if (menu_type == XmMENU_PULLDOWN)
        menu = XmCreatePulldownMenu (parent, "_pulldown", NULL, 0);
    else {
        n = 0;
        XtSetArg (args[n], XmNpopupEnabled, XmPOPUP_AUTOMATIC_RECURSIVE); n++;
        menu = XmCreatePopupMenu (parent, "_popup", args, n);
    }

    if (tear_off)
        XtVaSetValues (menu, XmNtearOffModel, XmTEAR_OFF_ENABLED, NULL);

    if (menu_type == XmMENU_PULLDOWN) {
        str = XmStringCreateLocalized (menu_title);
        n = 0;
        XtSetArg (args[n], XmNsubMenuId, menu);         n++;
        XtSetArg (args[n], XmNlabelString, str);        n++;
        XtSetArg (args[n], XmNmnemonic, menu_mnemonic); n++;
        cascade = XmCreateCascadeButtonGadget (parent, menu_title, args, n);
        XtManageChild (cascade);
        XmStringFree (str);
    }

    /* Now add the menu items */
    for (i = 0; items[i].label != NULL; i++) {
        /* If subitems exist, create the pull-right menu by calling this
        ** function recursively. Since the function returns a cascade
        ** button, the widget returned is used.
        */

        if (items[i].subitems)
            widget = BuildMenu (menu, XmMENU_PULLDOWN,
                                items[i].label,
                                items[i].mnemonic,
                                tear_off,
                                items[i].subitems);
        else
            widget = XtVaCreateManagedWidget (items[i].label, *items[i].class, 
                                            menu, NULL);

        /* Whether the item is a real item or a cascade button with a
        ** menu, it can still have a mnemonic.
        */
        if (items[i].mnemonic)
            XtVaSetValues (widget, XmNmnemonic, items[i].mnemonic, NULL);

        /* any item can have an accelerator, except cascade menus. But,
        ** we don't worry about that; we know better in our declarations.
        */
        if (items[i].accelerator) {
            str = XmStringCreateLocalized (items[i].accel_text);

            XtVaSetValues (widget, XmNaccelerator, items[i].accelerator, 
                            XmNacceleratorText, str, NULL);
            XmStringFree (str);
        }

        if (items[i].callback) {
            String resource;

        if (XmIsToggleButton (widget) || XmIsToggleButtonGadget (widget))
            resource = XmNvalueChangedCallback;
        else
            resource = XmNactivateCallback;

        XtAddCallback (widget, resource, items[i].callback,
                                (XtPointer) items[i].callback_data);
        }
    }

    return (menu_type == XmMENU_PULLDOWN)? cascade: menu;
}

main (int argc, char *argv[])
{
    Widget         button, rowcol, popup;
    XtAppContext   app;

    XtSetLanguageProc (NULL, NULL, NULL);
    toplevel = XtVaOpenApplication (&app, "Demos", NULL, 0, &argc, argv, NULL, 
                                    sessionShellWidgetClass, NULL);

    /* Build a RowColumn to contain two PushButtons */
    rowcol = XtVaCreateManagedWidget ("rowcol", xmRowColumnWidgetClass, 
                                    toplevel, NULL);

    /* The first PushButton is a gadget, just to show that the Motif 2.x 
    ** popup routines work for Gadgets as well as widgets.
    */
    button = XmCreatePushButtonGadget (rowcol, "Button 1", NULL, 0);
    XtAddCallback (button, XmNactivateCallback, put_string, "Button 1");
    XtManageChild (button);

    /* This PushButton is a widget.
    */
    button = XmCreatePushButton (rowcol, "Button 2", NULL, 0);
    XtAddCallback (button, XmNactivateCallback, put_string, "Button 2");
    XtManageChild (button);

    /* build the menu... */
    popup = BuildMenu (rowcol, XmMENU_POPUP, "Stuff", NULL, True, file_items);

    XtRealizeWidget (toplevel);
    XtAppMainLoop (app);
}

/* open_dialog_box() -- callback for some of the menu items declared
** in the MenuItem struct. The client data is the creation function
** for the dialog. Associate the dialog with the menu
** item via XmNuserData so we don't have to keep a global and
** don't have to repeatedly create one.
*/
void open_dialog_box (Widget w, XtPointer client_data, XtPointer call_data)
{
    Widget (*func)() = (Widget (*)()) client_data;
    Widget dialog = NULL;

    /* first see if this menu item's dialog has been created yet */
    XtVaGetValues (w, XmNuserData, &dialog, NULL);

    if (!dialog) {
        /* menu item hasn't been chosen yet -- create the dialog.
        ** Use the toplevel as the parent because we don't want the
        ** parent of a dialog to be a menu item.
        */
        dialog = (*func)(toplevel, "dialog", NULL, 0);
        XtVaSetValues (XtParent (dialog), XmNtitle, XtName (w), NULL);
        XtVaSetValues (dialog, XmNautoUnmanage, True, NULL);

        /* store the newly created dialog in the XmNuserData for the menu
        ** item for easy retrieval next time. (see get-values above.)
        */
        XtVaSetValues (w, XmNuserData, dialog, NULL);
    }
    XtManageChild (dialog);

    /* If the dialog was already open, XtPopup does nothing. In
    ** this case, at least make sure the window is raised to the top
    ** of the window tree (or as high as it can get).
    */
    XRaiseWindow (XtDisplay (dialog), XtWindow (XtParent (dialog)));
}
The output of the program is shown in Figure 19-12.

Figure  19-12 Output of the popups program

The program displays two PushButtons, one of which is a gadget and the other a widget. In Motif 1.2, we would need to catch ButtonPress events by specifically asking for them using XtAddEventHandler(). This routine requires a widget because it needs a window. To add an event handler for a gadget, you would have to install it on the gadget's parent, which is a manager widget. Any time a ButtonPress event occurs in the manager, the event handler would be called, so the event handler would have to check the coordinates of the event and see if it happened within the boundaries of the gadget. In Motif 2.0 and later, none of this is necessary: the toolkit does it all for us, and we only need to specify the XmNpopupEnabled and XmNpopupHandlerCallback resources as required. Motif 1.2 code will still work, however: XtAddEventHandler() takes the following form:

void XtAddEventHandler ( Widget           w,
                         EventMask        event_mask,
                         Boolean          nonmaskable,
                         XtEventHandler   proc,
                         XtPointer        client_data)
The widget parameter specifies the widget on which the event handler is to be installed, while event_mask identifies the events that are being handled. We would specify ButtonPressMask to indicate that we are interested in ButtonPress events. The nonmaskable argument indicates whether or not the event handler should be called on non-maskable events. We would specify False since we are not interested in the events. The final arguments specify the event handler routine and the client data that is passed to it. This routine would have to position the required popup menu (XmMenuPosition()), and display it (XtManageChild()). These are now called by Motif for us internally. See Volume 4, X Toolkit Intrinsics Programming Manual, for a complete list of event masks and more detailed information about XtAddEventHandler().

The RowColumn widget has a resource that you can set on PopupMenus called XmNmenuPost, which allows you to specify an alternate button to post the menu.

You may have noticed that the PopupMenu shown in Figure 19-12 has accelerators associated with it. These accelerators only take effect if the input focus is in the widget that contains the menu.


Building Option Menus

In this final section on generalized menu creation methods, we examine how to create OptionMenus using the BuildMenu() function. In this case, the underlying function is XmCreateOptionMenu(), which is another convenience routine provided by the Motif toolkit. The routine creates a RowColumn widget that manages the Label and CascadeButton widgets that define the OptionMenu, but we must create the actual PulldownMenu ourselves. The final version of the BuildMenu() function is shown in Example 19-13.

Example  19-13 The build_option.c program

/* build_option.c -- The final version of BuildMenu() is used to
** build popup, option, pulldown -and- pullright menus. Menus are
** defined by declaring an array of MenuItem structures as usual.
*/

#include <Xm/MainW.h>
#include <Xm/ScrolledW.h>
#include <Xm/PanedW.h>
#include <Xm/RowColumn.h>
#include <Xm/DrawingA.h>
#include <Xm/CascadeBG.h>
#include <Xm/ToggleB.h>
#include <Xm/ToggleBG.h>
#include <Xm/PushB.h>
#include <Xm/PushBG.h>

typedef struct _menu_item {
    char           *label;        /* the label for the item */
    WidgetClass    *class;        /* pushbutton, label, separator... */
    char           mnemonic;      /* mnemonic; NULL if none */
    char           *accelerator;  /* accelerator; NULL if none */
    char           *accel_text;   /* to be converted to compound string */
    void           (*callback)(); /* routine to call; NULL if none */
    XtPointer       callback_data; /* client_data for callback() */
    struct _menu_item  *subitems; /* pullright menu items, if not NULL */
} MenuItem;

/* Build popup, option and pulldown menus, depending on the menu_type.
** It may be XmMENU_PULLDOWN, XmMENU_OPTION or XmMENU_POPUP. Pulldowns
** return the CascadeButton that pops up the menu. Popups return the menu.
** Option menus are created, but the RowColumn that acts as the option
** "area" is returned unmanaged. (The user must manage it.) 
** Pulldown menus are built from cascade buttons, so this function 
** also builds pullright menus. The function also adds the right 
** callback for PushButton or ToggleButton menu items.
*/
Widget BuildMenu (Widget parent, int menu_type, char *menu_title,
char menu_mnemonic, Boolean tear_off, MenuItem *items)
{
    Widget       menu, cascade, widget;
    int          i;
    XmString     str;
    Arg          args[4];
    int          n;

    if (menu_type == XmMENU_PULLDOWN || menu_type == XmMENU_OPTION)
        menu = XmCreatePulldownMenu (parent, "_pulldown", NULL, 0);
    else if (menu_type == XmMENU_POPUP) {
        n = 0;
        XtSetArg (args[n], XmNpopupEnabled, XmPOPUP_AUTOMATIC_RECURSIVE); n++;
        menu = XmCreatePopupMenu (parent, "_popup", args, n);
    }
    else {
        XtWarning ("Invalid menu type passed to BuildMenu()");
        return NULL;
    }

if (tear_off)
    XtVaSetValues (menu, XmNtearOffModel, XmTEAR_OFF_ENABLED, NULL);

    /* Pulldown menus require a cascade button to be made */
    if (menu_type == XmMENU_PULLDOWN) {
        str = XmStringCreateLocalized (menu_title);
        n = 0;
        XtSetArg (args[n], XmNsubMenuId, menu); n++;
        XtSetArg (args[n], XmNlabelString, str); n++;
        XtSetArg (args[n], XmNmnemonic, menu_mnemonic); n++;
        cascade = XmCreateCascadeButtonGadget (parent, menu_title, args, n);
        XtManageChild (cascade);
        XmStringFree (str);
    }
    else if (menu_type == XmMENU_OPTION) {
        /* Option menus are a special case, but not hard to handle */
        str = XmStringCreateLocalized (menu_title);
        n = 0;
        XtSetArg (args[n], XmNsubMenuId, menu); n++;
        XtSetArg (args[n], XmNlabelString, str); n++;

        /* This really isn't a cascade, but this is the widget handle
        ** we're going to return at the end of the function.
        */
        cascade = XmCreateOptionMenu (parent, menu_title, args, n);
        XmStringFree (str);
    }

    /* Now add the menu items */

    for (i = 0; items[i].label != NULL; i++) {
        /* If subitems exist, create the pull-right menu by calling this
        ** function recursively. Since the function returns a cascade
        ** button, the widget returned is used.
        */

        if (items[i].subitems) {
            if (menu_type == XmMENU_OPTION) {
                XtWarning ("You can't have submenus from option menus.");
                continue;
            }
            else {
                widget = BuildMenu (menu, XmMENU_PULLDOWN,
                items[i].label,
                items[i].mnemonic,
                tear_off,
                items[i].subitems);
            }
        }
        else {
            widget = XtVaCreateManagedWidget (items[i].label,
            *items[i].class, menu, NULL);
        }

        /* Whether the item is a real item or a cascade button with a
        ** menu, it can still have a mnemonic. 
        */

        if (items[i].mnemonic) 
            XtVaSetValues (widget, XmNmnemonic, items[i].mnemonic, NULL);

        /* any item can have an accelerator, except cascade menus. But,
        ** we don't worry about that; we know better in our declarations.
        */

        if (items[i].accelerator) {
            str = XmStringCreateLocalized (items[i].accel_text);
            XtVaSetValues (widget, XmNaccelerator, items[i].accelerator, 
                            XmNacceleratorText, str, NULL);
            XmStringFree (str);
        }
        if (items[i].callback) {
            String resource;

            if (XmIsToggleButton (widget) || XmIsToggleButtonGadget (widget))
                resource = XmNvalueChangedCallback;
            else
                resource = XmNactivateCallback;

            XtAddCallback (widget, resource, items[i].callback, 
                            (XtPointer) items[i].callback_data);
        }
    }

    /* for popup menus, just return the menu; pulldown menus, return
    ** the cascade button; option menus, return the thing returned
    ** from XmCreateOptionMenu(). This isn't a menu, or a cascade button!
    */

    return (menu_type == XmMENU_POPUP? menu: cascade);
}

MenuItem drawing_shapes[] = {
    { "Lines", &xmPushButtonGadgetClass, `L', NULL, NULL, 0, 0, NULL },
    { "Circles", &xmPushButtonGadgetClass, `C', NULL, NULL, 0, 0, NULL },
    { "Squares", &xmPushButtonGadgetClass, `S', NULL, NULL, 0, 0, NULL },
    { NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL }
};

main (int argc, char *argv[])
{
    Widget            toplevel, main_w, pane, sw, drawing_a, menu, 
                      option_menu;
    Arg               args[4];
    int               n;
    XtAppContext      app;
    XtWidgetGeometry  geom;

    XtSetLanguageProc (NULL, NULL, NULL);
    toplevel = XtVaOpenApplication (&app, "Demos", NULL, 0, &argc, argv, NULL, 
                                    sessionShellWidgetClass, NULL);

    main_w = XmCreateMainWindow (toplevel, "main_w", NULL, 0);

    /* Use a PanedWindow widget as the work area of the main window */
    pane = XmCreatePanedWindow (main_w, "pane", NULL, 0);

    /* create the option menu -- don't forget to manage it. */
    option_menu = BuildMenu (pane, XmMENU_OPTION, "Shapes", `S', True, 
                                drawing_shapes);
    XtManageChild (option_menu);

    /* Set the OptionMenu so that it can't be resized */
    geom.request_mode = CWHeight;
    XtQueryGeometry (option_menu, NULL, &geom);
    XtVaSetValues (option_menu, XmNpaneMinimum, geom.height, XmNpaneMaximum, 
                    geom.height, NULL);

    /* The scrolled window (which contains the drawing area) is a child
    ** of the PanedWindow; its sibling, the option menu, cannot be resized,
    ** so if the user resizes the toplevel shell, *this* window will resize. 
    */
    n = 0;
    XtSetArg (args[n], XmNscrollingPolicy, XmAUTOMATIC); n++;
    sw = XmCreateScrolledWindow (pane, "sw", args, n);

    /* Create a DrawingArea -- no actual drawing will be done. */
    n = 0;
    XtSetArg (args[n], XmNwidth, 500);  n++;
    XtSetArg (args[n], XmNheight, 500); n++;
    drawing_a = XmCreateDrawingArea (sw, "drawing_a", args, n);
    XtManageChild (drawing_a);

    XtManageChild (sw);
    XtManageChild (pane);
    XtManageChild (main_w);
    XtRealizeWidget (toplevel);
    XtAppMainLoop (app);
}
There are two particularly interesting features of this program. First, of course, is the modification of the BuildMenu() function. As the comments in the code indicate, the function now fully supports all of the Motif menu types.We use XmCreatePulldownMenu() to create the menu pane that is posted from the CascadeButton of the OptionMenu.This menu pane is attached to the OptionMenu by setting the XmNsubMenuId as usual. As we loop through the menu items that are to be placed in the menu, we prevent the creation of a pullright menu in an OptionMenu, as cascading menus are not allowed in OptionMenus.

When BuildMenu() is used to create an OptionMenu, the function returns the RowColumn widget that is returned by XmCreateOptionMenu(), even though it is not really a CascadeButton as the variable name might indicate. The calling function needs the RowColumn widget so that it can manage the OptionMenu by calling XtManageChild(). (The call to XtManageChild() might be another automated part of BuildMenu() if you want to modify it.)

The other interesting feature of the program is the layout of the MainWindow. The MainWindow widget has a single PanedWindow widget as its child because we wish to retain the vertical stacking relationship between the OptionMenu and the DrawingArea. Another advantage of using the PanedWindow is that we can set the maximum and minimum height of each pane. The user can resize the entire window using the window manager, but we don't want the OptionMenu to change size, so we allow the ScrolledWindow to absorb the size fluctuations.

Summary

Menus are basically simple objects that provide the user with access to application functionality. While the simple menu creation routines are handy for basic prototyping and other simple application constructs, their usefulness is limited once you begin to develop larger-scale applications.

We have described the design of a general menu creation routine, so it should be clear that you only need two things to create an arbitrary number of menus: predefined arrays of MenuItem structures and the BuildMenu() function. Since initializing an array of MenuItem objects is very simple, our method is convenient and also more powerful than the simple menu creation routines. We have defined our own data type and generalized the routine to build menus so that you can use and modify these functions however you like, to conform to the needs of your application.

Exercises

This chapter could go on forever discussing more and more things you can do with menus. However, the goal was to present you with the fundamental concepts and design considerations behind menus. From this information, you should be able to teach yourself new techniques that we haven't touched upon.In that spirit, you should be able to do the following exercises based on the material covered in this chapter.

  1. Create a MainWindow widget that has a MenuBar that contains at least the File, Edit, and Help menus, an OptionMenu, and a PopupMenu that pops up from a DrawingArea widget. First implement the menus using the simple menu creation routines, and then implement them using the BuildMenu() function.
  2. Initialize a MenuItem structure whose fields are all set to NULL except for the menu items' names, callback routines, and widget classes, and then write a resource file that generates a usable menu.
  3. Modify the MenuItem structure and the BuildMenu routine so that you can specify the initial sensitivity for menu items.
  4. Modify BuildMenu() to recognize when the menu it is about to build is a RadioBox. You may choose to implement this behavior by passing a new parameter to the function or by examining the children in the MenuItem list to see if they are ToggleButtons. You will need to modify the MenuItem structure by adding another Boolean field to allow each element to indicate whether it is a radio button or a plain ToggleButton. See Chapter 4, The Main Window, for a discussion of RadioBoxes in menus.


1 The button that posts the menu is typically user-settable, since left-handed users may want to reverse the default button bindings.

2 If you need to port an Athena or OPEN LOOK-based application to Motif, you will probably have to reimplement your menu design.

3 XtVaAppInitialize() is considered deprecated in X11R6. The XmNpopupEnabled resource is modified in Motif 2.0 and later to support the values XmPOPUP_AUTOMATIC, XmPOPUP_AUTOMATIC_RECURSIVE, XmPOPUP_DISABLED, XmPOPUP_KEYBOARD.

4 XtVaAppInitialize() is considered deprecated in X11R6. The XmNpopupHandlerCallback resource, and the automatic popup of menus. are only available in Motif 2.0 and later.

5 XtVaAppInitialize() is considered deprecated in X11R6. The XmNpopupEnabled resource is modified in Motif 2.0 and later to support the values XmPOPUP_AUTOMATIC, XmPOPUP_AUTOMATIC_RECURSIVE, XmPOPUP_DISABLED, XmPOPUP_KEYBOARD.

6 XtVaAppInitialize() is considered deprecated in X11R6.

7 XtVaAppInitialize() is considered deprecated in X11R6.

8 A side effect of the implementation of Motif accelerators is that you cannot install your own accelerators using the standard methods provided by the X Toolkit Intrinsics (such as XtInstallAccelerators() or XtInstallAllAccelerators() ).These functions will not work, and you may interfere with the Motif accelerator mechanism by attempting to use them.

9 This behavior is not a great design. The dialog really should be cached, and the menu item should remain sensitive. If the item is reselected, the dialog should be re-mapped or raised to the top of the window stack, if necessary.

10 Motif 1.2 does not install a resource converter for the XmNtearOffModel resource: you need to call XmRepTypeInstallTearOffModelConverter () manually. Motif 2.0 installs a converter for the type automatically, and thus XmRepTypeInstallTearOffModelConverter() is deprecated in later versions of the toolkit.

11 XtVaAppInitialize() is considered deprecated in X11R6.

12 Automatic popup as provided by the toolkit is only available from Motif 2.0 onwards.

13 XtVaAppInitialize() is considered deprecated in X11R6. There is no XmNpopupHandlerCallback resource in Motif 1.2: an event handler must be installed in order to display a popup menu, which means that Gadgets did not support popup menus in this version of the toolkit.






X-Designer - The Leading X/Motif GUI Builder - Click to download a FREE evaluation

Previous Next Contents Document Index Motif Docs motifdeveloper.com Imperial Software Technology X-Designer




Copyright © 1991, 1994, 2000, 2001, 2002 O'Reilly & Associates, Inc., Antony Fountain and Jeremy Huxtable. All Rights Reserved.