Contents Previous Next

22 Advanced Dialog Programming


This chapter describes some Motif features that have not been described, or at least not completely, in earlier chapters. The topics, which all deal with dialogs, include the creation of multi-stage help systems, the development of WorkingDialogs that allow the user to interrupt long-running tasks, and a method for dynamically changing the pixmaps displayed in a dialog.

In one sense, this chapter isn't about dialogs at all, but about various aspects of X programming that become most evident when working with dialogs. Here we address some issues involved in creating multi-stage help systems, we show you how to create a WorkingDialogs that allows the user to interrupt a long-running task, and we describe a method for dynamically changing the pixmaps that are displayed in a dialog. These topics explore some of the most interesting problems in this book.

These topics take us deeper into the lower layers of X than anything we've discussed so far in this book. You should have a good basic understanding of X event-processing, as implemented both in Xlib and Xt. Otherwise, be prepared to refer frequently to Volume One, Xlib Programming Manual, and Volume Four, X Toolkit Intrinsics Programming Manual, when faced with references to lower-level functions.

22.1 Help Systems

The Motif Style Guide doesn't have much to say about how help is presented to the user, although it does discuss the ways in which the user can request help from an application. The user can request help by selecting the Help button in a dialog box, by choosing help items from the Help menu in the MenuBar, or by pressing the HELP or F1 key on the keyboard. Help information should be presented clearly, so that it is accessible and beneficial to users. You should also maintain consistency in a help system, so that the user can become familiar with the style of help that you provide.

The easiest and most straightforward method of presenting help information involves creating an InformationDialog with the necessary text displayed as the XmNmessageString. the source code demonstrates how to display a help dialog when the user presses the Help button in another dialog box. XtSetLanguageProc() is only available in X11R5; there is no corresponding function in X11R4. XmStringCreateLocalized() is only available in Motif 1.2; XmStringCreateSimple() is the corresponding function in Motif 1.1. XmFONTLIST_DEFAULT_TAG replaces XmSTRING_DEFAULT_CHARSET in Motif 1.2.

   /* simple_help.c -- create a PushButton that posts a dialog box
    * that entices the user to press the help button.  The callback
    * for this button displays a new dialog that gives help.
    */
   #include <Xm/MessageB.h>
   #include <Xm/PushB.h>

   main(argc, argv)
   int argc;
   char *argv[];
   {
       Widget toplevel, button;
       XtAppContext app;
       XmString label;
       void pushed();

       XtSetLanguageProc (NULL, NULL, NULL);

       toplevel = XtVaAppInitialize (&app, "Demos", NULL, 0,
           &argc, argv, NULL, NULL);

       label = XmStringCreateLocalized ("Push Me");
       button = XtVaCreateManagedWidget ("button",
           xmPushButtonWidgetClass, toplevel,
           XmNlabelString,          label,
           NULL);
       XtAddCallback (button, XmNactivateCallback,
           pushed, "You probably need help for this item.");
       XmStringFree (label);

       XtRealizeWidget (toplevel);
       XtAppMainLoop (app);
   }

   #define HELP_TEXT "This is the help information.0ow press 'OK'"

   /* pushed() -- the callback routine for the main app's pushbutton. */
   void
   pushed(w, client_data, call_data)
   Widget w;
   XtPointer client_data;
   XtPointer call_data;
   {
       char *text = (char *) client_data;
       Widget dialog;
       XmString t = XmStringCreateLocalized (text);
       Arg args[5];
       int n;
       void help_callback(), help_done();

       n = 0;
       XtSetArg (args[n], XmNautoUnmanage, False); n++;
       XtSetArg (args[n], XmNmessageString, t); n++;
       dialog = XmCreateMessageDialog (XtParent(w), "notice", args, n);
       XmStringFree (t);

       XtUnmanageChild (
           XmMessageBoxGetChild (dialog, XmDIALOG_CANCEL_BUTTON));

       XtAddCallback (dialog, XmNokCallback, help_done, NULL);
       XtAddCallback (dialog, XmNhelpCallback, help_callback, HELP_TEXT);

       XtManageChild (dialog);
       XtPopup (XtParent (dialog), XtGrabNone);
   }

   /*
    * help_callback() -- callback routine for the Help button in the
    * original dialog box that displays an InformationDialog based on the
    * help_text parameter.
    */
   void
   help_callback(parent, client_data, call_data)
   Widget parent;
   XtPointer client_data;
   XtPointer call_data;
   {
       char *help_text = (char *) client_data;
       Widget dialog;
       XmString text;
       void help_done();
       Arg args[5];
       int n;

       n = 0;
       text = XmStringCreateLtoR (help_text, XmFONTLIST_DEFAULT_TAG);
       XtSetArg (args[n], XmNmessageString, text); n++;
       XtSetArg (args[n], XmNautoUnmanage, False); n++;
       dialog = XmCreateInformationDialog (parent, "help", args, n);
       XmStringFree (text);

       XtUnmanageChild (  /* no need for the cancel button */
           XmMessageBoxGetChild (dialog, XmDIALOG_CANCEL_BUTTON));
       XtSetSensitive (   /* no more help is available. */
           XmMessageBoxGetChild (dialog, XmDIALOG_HELP_BUTTON), False);
       /* the OK button will call help_done() below */
       XtAddCallback (dialog, XmNokCallback, help_done, NULL);

       /* display the help text */
       XtManageChild (dialog);
       XtPopup (XtParent (dialog), XtGrabNone);
   }

   /* help_done() -- called when user presses "OK" in dialogs. */
   void
   help_done(dialog, client_data, call_data)
   Widget dialog;
   XtPointer client_data;
   XtPointer call_data;
   {
       XtDestroyWidget (dialog);
   }
The main window contains a PushButton that posts a simple MessageDialog. This dialog, as you can tell from the figure, contains a Help button, that pops up an InformationDialog. This dialog is intended to provide help text for the user.

figs.eps/V6a.21.01.eps.png
Output of simple_help.c


The callback routine for the Help button is installed using the XmNhelpCallback. This routine pops up an InformationDialog that contains some predefined text. Obviously, this text is for demonstration purposes only. We used XmStringCreateLtoR() to display the text, instead of XmStringCreateLocalized(), since the help message contains newline characters. You could also use XmStringCreateLtoR() to specify an alternate font for use in your help system, but we are not taking advantage of this feature. See Chapter 19, Compound Strings, for more information on how you can use compound strings to display text using different fonts.

The XmNhelpCallback resource serves as the callback for any widget that wishes to provide help information; every Motif widget has an XmNhelpCallback resource associated with it. Whenever the user presses the HELP key on the keyboard (if one exists and the X server is set up correctly), By default, Sun workstations do not generate the proper event when the HELP key is pressed, and your mileage may vary for other computers. the XmNhelpCallback is invoked for the widget that has the keyboard focus. The F1 key also serves as a help key for compatibility with Microsoft Windows and to compensate for any computer that may not have a HELP key. The F1 key works by default, but it may be remapped to perform another function in the user's .mwmrc file.

If a widget does not have an XmNhelpCallback function installed, Motif climbs the widget tree, searching for the nearest ancestor that has a help callback. If you assign help callbacks to widgets, we recommend that you provide specific help information for individual interface components, such as PushButtons, Lists, and Text widgets, and more general information for manager widgets. It is possible to design an elaborate context-sensitive help system for an application by installing help callback routines for the widgets in the interface and providing relevant help information throughout the hierarchy.

Although simple_help.c is rather contrived, we can use it to examine the different actions the user might take within a help system. You can think of the Push Me button as any widget in an application on which the user might want help. When the button is activated, the user is presented with a MessageDialog that undoubtedly requires help. The user can select the Help button or press the F1 or HELP keys to access the help information. Since the InformationDialog is modeless, as it should be, the user can either close the InformationDialog or the original MessageDialog.

Since the InformationDialog is a child of the MessageDialog, if the MessageDialog is destroyed, the InformationDialog is also destroyed. Similarly, if the MessageDialog is unmapped, so is the InformationDialog. In general, when you display an InformationDialog, you should remove it if the user unmanages, destroys, or otherwise disables the dialog from which it was posted because if the help dialog remains posted, it could confuse the user. By making the InformationDialog the child of the original dialog, you can let the parent-child interaction handle this behavior.

22.1.1 Multi-level Help

Developing a help systen may involve providing multiple levels of help information. If the user has already posted an InformationDialog, it is possible to display an additional dialog if the user requests help in the original dialog. However, multiple help windows can confuse the user, so they should be avoided. A better solution is to display the new help text in the same InformationDialog, so that all of the help information is displayed in the same place. the source code shows new help_callback() and help_done() routines that implement this technique. XmStringCreateLocalized() is only available in Motif 1.2; XmStringCreateSimple() is the corresponding function in Motif 1.1. XmFONTLIST_DEFAULT_TAG replaces XmSTRING_DEFAULT_CHARSET in Motif 1.2.

   #define MAX_HELP_STAGES 3
   char *help_text[3][5] = {
       {
           "You have reached the first stage of the help system.",
           "If you need additional help, select the 'More Help' button.",
           "You may exit help at any time by pressing 'Done'.",
           NULL,
       },
       {
           "This is the second stage of the help system.  There is",
           "more help available.  Press 'More Help' to see more.",
           "Press 'Previous' to return to the previous help message,",
           "or press 'Done' to exit the help system.",
           NULL,
       },
       {
           "This is the last help message you will see on this topic.",
           "You may either press 'Previous' to return to the previous",
           "help level, or press 'Done' to exit the help system.",
           NULL,
       }
   };

   /* help_callback() -- callback routine for the Help button in the
    * original dialog box.  The routine also serves as its own help
    * callback for displaying multiple levels of help messages.
    */
   void
   help_callback(parent, client_data, call_data)
   Widget parent;
   XtPointer client_data;
   XtPointer call_data;
   {
       static Widget dialog; /* prevent multiple help dialogs */
       XmString text;
       char buf[BUFSIZ], *p;
       static int index;
       int i;
       void help_done();
       int index_incr = (int) client_data;

       if (dialog && index_incr == 0) {
           /* user pressed Help button in MesageDialog again.  We're
            * already up, so just make sure we're visible and return. */
           XtPopup (XtParent (dialog), XtGrabNone);
           XMapRaised (XtDisplay (dialog), XtWindow (XtParent (dialog)));
           return;
       }

       if (dialog)
           index += index_incr; /* more/previous help; change index */
       else {
           /* We're not up, so create new Help Dialog */
           Arg args[5];
           int n;

           /* Action area button labels. */
           XmString done = XmStringCreateLocalized ("Done");
           XmString cancel = XmStringCreateLocalized ("Previous");
           XmString more = XmStringCreateLocalized ("More Help");

           n = 0;
           XtSetArg (args[n], XmNautoUnmanage, False); n++;
           XtSetArg (args[n], XmNokLabelString, done); n++;
           XtSetArg (args[n], XmNcancelLabelString, cancel); n++;
           XtSetArg (args[n], XmNhelpLabelString, more); n++;
           dialog = XmCreateInformationDialog (parent, "help", args, n);

           /* pass help_done() the address of "dialog" so it can reset */
           XtAddCallback (dialog, XmNokCallback, help_done, &dialog);

           /* if more/previous help, recall ourselves with increment */
           XtAddCallback (dialog, XmNcancelCallback, help_callback, -1);
           XtAddCallback (dialog, XmNhelpCallback, help_callback, 1);

           /* If our parent dies, we must reset "dialog" to NULL! */
           XtAddCallback (dialog, XmNdestroyCallback, help_done, &dialog);

           XmStringFree (done);   /* once dialog is created, these */
           XmStringFree (cancel); /* strings are no longer needed. */
           XmStringFree (more);

           index = 0; /* initialize index--needed for each new help stuff */
       }

       /* concatenate help text into a single string with newlines */
       for (p = buf, i = 0; help_text[index][i]; i++) {
           p += strlen (strcpy (p, help_text[index][i]));
           *p++ = '0;
           *p = 0;
       }

       text = XmStringCreateLtoR (buf, XmFONTLIST_DEFAULT_TAG);
       XtVaSetValues (dialog, XmNmessageString, text, NULL);
       XmStringFree (text); /* after set-values, free unneeded memory */

       /* If no previous help msg, set "Previous" to insensitive. */
       XtSetSensitive (
           XmMessageBoxGetChild (dialog,XmDIALOG_CANCEL_BUTTON), index > 0);
       /* If no more help, set "More Help" insensitive. */
       XtSetSensitive (XmMessageBoxGetChild (
           dialog, XmDIALOG_HELP_BUTTON), index < MAX_HELP_STAGES-1);

       /* display the dialog */
       XtManageChild (dialog);
       XtPopup (XtParent (dialog), XtGrabNone);
   }

   /* help_done () -- callback used to set the dialog pointer
    * to NULL so it can't be referenced again by help_callback().
    * This function is called from the Done button in the help dialog.
    * It is also our XmNdestroyCallback, so reset our dialog_ptr to NULL.
    */
   void
   help_done(dialog, client_data, call_data)
   Widget dialog;
   XtPointer client_data;
   XtPointer call_data;
   {
       Widget *dialog_ptr;

       if (!client_data) { /* destroy original MessageDialog */
           XtDestroyWidget (dialog);
           return;
       }

       dialog_ptr = (Widget *) client_data;
       if (!*dialog_ptr) /* prevent unnecessarily destroying twice */
           return;
       XtDestroyWidget (dialog); /* this might call ourselves.. */
       *dialog_ptr = NULL;
   }

In our help system, each level has a new help string that needs to be displayed. All of the help text is displayed in the same InformationDialog. The dialog for the first level of help is shown in the figure.

figs.eps/V6a.21.02.eps.png
Displaying multiple levels of help text

The help_callback routine addresses several problems that arise when dealing with the added complexity of a multi-level help system. Since many dialogs may be trying to pop up the same InformationDialog, the routine uses a static variable for the dialog to prevent multiple instances of the dialog. This variable allows the routine to keep track of when the dialog is active and when it is dormant.

The routine is conceptually recursive, in that it is used as the callback routine for the buttons in the help dialog. The client_data is used as an index into the help_text array. When this parameter is 0, the routine was called by the original MessageDialog. Otherwise, the routine was invoked as a result of the user pressing the Previous button or the More Help button. In this case, the index is changed so that the help text changes.

If the InformationDialog has already been created and the user presses the Help button anyway, the dialog is remapped and raised to the top of the screen using XMapRaised() . If the parent dialog is unmapped or destroyed, the InformationDialog is also unmapped or destroyed. In order to maintain the correct state information, we install an XmNdestroyCallback to monitor the destruction of the InformationDialog. When the dialog is destroyed, we need to reset the handle to the dialog to NULL so that we cannot reference the destroyed dialog again from help_callback() the next time help is requested.

All of our help text is fairly short, but if you need to provide help text that longer, you may want to use a ScrolledText object in your help dialog. With a ScrolledText object, you can display text of an arbitrary length without worrying about screen real estate. This technique is explained in Chapter 7, Custom Dialogs.

22.1.2 Context-sensitive Help

Although the user can access the help system by using the HELP or F1 keys, this interface is somewhat cumbersome and it doesn't work for widgets like Labels that do not process input events. You can provide a more intuitive interface that allows the user to point-and-click directly on a widget to obtain help. The Motif Style Guide refers to this style of help as context-sensitive help.

Context-sensitive help is make possible by the XmTrackingEvent() routine, which takes the following form:

   Widget
   XmTrackingEvent(widget, cursor, confine_to, event)
       Widget  widget;
       Cursor  cursor;
       Boolean confine_to;
       XEvent *event;
The routine invokes a server-grab on the pointer, changes the pointer shape to that specified by the cursor parameter, and waits until the user presses a mouse button. The routine returns the widget on which the user pressed the button. If the confine_to parameter is True, the cursor is confined to the window of the specified widget. This window is also used as the owner of the pointer grab. The event parameter returns the actual event performed by the user.

XmTrackingEvent() is new in Motif 1.2; it replaces the existing XmTrackingLocate() routine. XmTrackingEvent() should be used in place of the older routine because it works for all widgets, regardless of whether they handle input events. For example, if the user presses the mouse button over a Label widget, XmTrackingEvent() returns the Label, while XmTrackingLocate() would return the parent of the Label.

An application usually provides context-sensitive help through an item on the Help menu. the source code shows the query_for_help() callback routine that could be used for such a menu item. XmTrackingEvent() is only available in Motif 1.2; XmTrackingLocate() is the corresponding function in Motif 1.1.

   #include <X11/cursorfont.h>

   Widget toplevel;

   void
   query_for_help(widget, client_data, call_data)
   Widget          widget;
   XtPointer       client_data;
   XtPointer       call_data;
   {
       Cursor                cursor;
       Display               *display;
       Widget                help_widget;
       XmAnyCallbackStruct   *cbs, *newcbs;
       XEvent                *event;

       cbs = (XmAnyCallbackStruct *) call_data;
       display = XtDisplay (toplevel);
       cursor = XCreateFontCursor (display, XC_hand2);

       help_widget = XmTrackingEvent (toplevel, cursor, True, &event);
       while (help_widget != NULL) {
           if (XtHasCallbacks (help_widget, XmNhelpCallback) ==
               XtCallbackHasSome ) {
               newcbs->reason = XmCR_HELP;
               newcbs->event = event;
               XtCallCallbacks (help_widget, XmNhelpCallback,
                   (XtPointer) newcbs);
               help_widget = NULL;
           }
           else
               help_widget = XtParent (help_widget);
       }
       XFreeCursor (display, cursor);
   }
When the user selects the menu item for context-sensitive help, query_for_help() is invoked. This routine calls XmTrackingEvent() to allow the user to specify a widget on which to see help information. The confine_to parameter is set to True, so the pointer is constrained to the window of the toplevel widget. We use toplevel so that the user can select any component in the entire application.

XmTrackingEvent() changes the pointer to the specified cursor to provide visual feedback that the application is in a new state. Since the user is expected to click on a object, the routine uses the XC_hand2 glyph that shows a pointing hand. The cursor is created using XCreateFontCursor(). See Volume One, Xlib Programming Manual, for more information.

If the user clicks on any valid widget within the application, XmTrackingEvent() returns the ID for that widget. The widget itself is not activated and it does not receive any events that indicate that anything has happened at all. If the user does not click on a valid widget, the function returns NULL. If XmTrackingEvent() returns a widget ID, we use XtCallCallbacks() to activate the XmNhelpCallback for the widget. If the widget does not have a help callback, query_for_help() climbs the widget tree looking for an ancestor widget with a help callback.

While the confine_to flag makes XmTrackingEvent() useful for constraining mouse movement, you should use this feature with caution. Once the cursor is confined to the window, the server grab is not released until the user presses the mouse button. We also advise caution if you are using a debugger while working with this function. If the debugger stops at a breakpoint while the function is invoked, you will have to log in remotely and kill the debugger process to release the pointer grab. If you kill the process, you will have to shut down the computer.

22.2 Working Dialogs

The Motif WorkingDialog is used to inform the user that an application is busy processing, so that it doesn't have the time to handle other actions the user may take. For example, if your application is busy trying to figure out the complete value of pi , the user is probably going to have to wait for the application to respond to her next action. The delay occurs because the application code has control, rather than Xt. When Xt has control, it processes events and dispatches them to the appropriate widgets in the application. If a widget has a callback installed for an event, Xt returns control to the application. While the application has control, there is no way for the window system to service any requests the user may happen to make.

In the meantime, the application is faced with the dilemma of how it is going to process events that happen in the interim. While your application is busy number-crunching, the user is frantically pounding on the Stop button and hoping that the application will figure out that she really didn't want it to figure out the complete value of pi, but instead to print out the recipe for cherry pie.

What the application needs to do is to find a way to do the necessary work for callback routine and process events at the same time. The solution is conceptually simple: the application should periodically check to see if there are any events in the input queue, and if there are, process and dispatch them. The implementation of this solution, on the other hand, is quite a different story. There are a number of different approaches you can take, depending on the nature of the work you are trying to do. Let's examine four of the options:

You can mix and match some of these techniques. Say the user wants to send a large PostScript file to a laser printer. When she clicks on the Print button, you can post a WorkingDialog that reports that the file is being printed and the user must wait. Additionally, you could provide an option that allows the user to send the file to the printer in the background. In this case, you can send the file to the printer in small chunks using work procedures.

The four methods fall into two basic categories:

Work procedures and timers return control to Xt and allow it to process events as normal. In turn, Xt gives control back to the application for short intervals every now and then. When the application maintains control, it can query and process X events whenever it wants. While this process is more complicated, it does make it easier for the application to control its own processing.

In all four situations, you can decide whether or not to display a WorkingDialog. If you want to give the user the ability to terminate the work in progress, you can provide a Stop button in the dialog. Otherwise, you can simply display the dialog for informational purposes. If you do not want the user to interact with other windows in the application while the WorkingDialog is being displayed, you can make the dialog modal as described in Section #smodaldlg.

22.2.1 Using Work Procedures

Work procedures in Xt are extremely simple in design. They are typically used by applications that can process tasks in the background. When a work procedure is used in conjunction with a WorkingDialog, the application can provide feedback on the status of the task. Say the user wants to load a large bitmap into a window. The nature of your application requires you to load the file from disk into client-side memory, perform some bitmap manipulation, and then send the bitmap to the X server to be loaded into a pixmap. If you suspect that this task might take a long time and you want to allow the user to interrupt it, you can use work procedures and a WorkingDialog.

Unfortunately, demonstrating such a task is difficult, due to its extremely complex nature. The bitmap loading operation requires a great deal of image-handling code that is a distraction from the issue at hand, which is installing a work procedure. To get around this problem, we present a short, abstract program that demonstrates the use of a work procedure. In the source code we represent a time-consuming task by counting from 0 to 20000. XtSetLanguageProc() is only available in X11R5; there is no corresponding function in X11R4. XmStringCreateLocalized() is only available in Motif 1.2; XmStringCreateSimple() is the corresponding function in Motif 1.1.

   /* working.c -- represent a complicated, time-consuming task by
    * counting from 0 to 20000 and provide feedback to the user about
    * how far we are in the process.  The user may terminate the process
    * at any time by selecting the Stop button in the WorkingDialog.
    * This demonstrates how a WorkingDialog can be used to allow the
    * user to interrupt lengthy procedures.
    */
   #include <Xm/MessageB.h>
   #include <Xm/PushB.h>

   #define MAXNUM 20000

   void done();

   /* Global variables */
   static int           i = 0;
   static XtWorkProcId  work_id;

   main(argc, argv)
   int argc;
   char *argv[];
   {
       XtAppContext  app;
       Widget        toplevel, button;
       XmString      label;
       void          pushed();

       XtSetLanguageProc (NULL, NULL, NULL);

       toplevel = XtVaAppInitialize (&app, "Demos",
           NULL, 0, &argc, argv, NULL, NULL);

       label = XmStringCreateLocalized ("Press Here To Start A Long Task");
       button = XtVaCreateManagedWidget ("button",
           xmPushButtonWidgetClass, toplevel,
           XmNlabelString,          label,
           NULL);
       XtAddCallback (button, XmNactivateCallback, pushed, app);
       XmStringFree (label);

       XtRealizeWidget (toplevel);
       XtAppMainLoop (app);
   }

   /* pushed() -- the callback routine for the main app's pushbutton. */
   void
   pushed(w, client_data, call_data)
   Widget w;
   XtPointer client_data;
   XtPointer call_data;
   {
       XtAppContext  app = (XtAppContext) client_data;
       Widget        dialog;
       XmString      stop_txt;
       Arg           args[5];
       int           n;
       Boolean       count();

       /* Create the dialog -- the "cancel" button says "Stop" */
       n = 0;
       stop_txt = XmStringCreateLocalized ("Stop");
       XtSetArg(args[n], XmNcancelLabelString, stop_txt); n++;
       dialog = XmCreateWorkingDialog (w, "working", args, n);
       XmStringFree (stop_txt);

       work_id = XtAppAddWorkProc (app, count, dialog);

       XtUnmanageChild (XmMessageBoxGetChild (dialog, XmDIALOG_OK_BUTTON));
       XtUnmanageChild (XmMessageBoxGetChild (dialog, XmDIALOG_HELP_BUTTON));

       /* Use cancel button to stop counting. True = remove work proc */
       XtAddCallback (dialog, XmNcancelCallback, done, True);

       XtManageChild (dialog);
       XtPopup (XtParent (dialog), XtGrabNone);
   }

   /* count() -- work procedure that counts to MAXNUM.  When we get there,
    * change the "Stop" button to say "Done".
    */
   Boolean
   count(client_data)
   XtPointer client_data;
   {
       Widget dialog = (Widget) client_data;
       char buf[64];
       XmString str, button;
       Boolean finished = False;

       /* If we printed every number, the flicker is too fast to read.
        * Therefore, just print every 1000 ticks for smoother feedback.
        */
       if (++i % 1000 != 0)
           return finished;

       /* display where we are in the counter. */
       sprintf (buf, "Counter: %d", i);
       str = XmStringCreateLocalized (buf);
       XtVaSetValues (dialog, XmNmessageString, str, NULL);
       XmStringFree (str);

       if (i == MAXNUM) {
           i = 0;
           finished = True;
           button = XmStringCreateLocalized ("Done");
           XtVaSetValues (dialog, XmNcancelLabelString, button, NULL);
           XmStringFree (button);
           XtRemoveCallback (dialog, XmNcancelCallback, done, True);
           XtAddCallback (dialog, XmNcancelCallback, done, False);
           XMapRaised (XtDisplay (dialog), XtWindow (XtParent (dialog)));
       }

       /* Return either True, meaning we're done and remove the work proc,
        * or False, meaning continue working by calling this function.
        */
       return finished;
   }

   /* done () -- user pressed "Stop" or "Done" in WorkingDialog. */
   void
   done(dialog, client_data, call_data)
   Widget dialog;
   XtPointer client_data;
   XtPointer call_data;
   {
       Boolean remove_work_proc = (Boolean) client_data;

       if (remove_work_proc) {
           i = 0;
           XtRemoveWorkProc (work_id);
       }
       XtDestroyWidget (dialog);
   }
The main application simply displays a button. When the user presses the button, the application starts counting and displays a WorkingDialog. The user can press Stop at any time during the process. If the user allows the application to finish counting, the button is changed from Stop to Done. the figure shows both states of the WorkingDialog.

figs.eps/V6a.21.03.eps.png
Output of working.c

This program is designed to demonstrate how a work procedure and a WorkingDialog can interact. The callback for the button in the application creates a WorkingDialog using ­XmCreateWorkingDialog(). The callback routine also installs a work procedure using XtAppAddWorkProc(). This function takes the following form:

   XtWorkProcId
   XtAppAddWorkProc(app_context, proc, client_data)
       XtAppContext app_context;
       XtWorkProc   proc;
       XtPointer    client_data;
The WorkingDialog is used as the client data for the count() work procedure, so that the procedure can update the dialog. To allow the user to interrupt the counting operation, we install done() as the XmNcancelCallback resource. If the user presses the Stop button, this routine is invoked. The routine stops the counting operation by removing the work procedure using XtRemoveWorkProc().

During the counting operation, Xt calls the work procedure when there are no events that need to be processed. The work procedure increments the global counter variable, i. Each time i reaches an increment of 1000, the XmNmessageString for the WorkingDialog is updated to inform the user about the progress of the operation. The work procedure returns True when the task is complete, which causes Xt to remove the procedure from the list of work procedures being called. When count() returns False, Xt continues to call the routine when the application is idle.

If the user allows the task to complete, the work procedure changes the action button to say Done and removes the XmNcancelCallback. The procedure then reinstalls the callback in order to change the client data from True to False. The client data must be set to False so that done() does not try to remove the work procedure. Since the work procedure returns True in this case, Xt removes the procedure for us.

The work procedure also calls XMapRaised() to ensure that the dialog is visible when the operation completes. The user must explicitly press the Done button to remove the dialog. Another approach is to call XtDestroyWidget() to remove the dialog when the processing is done. In this case, the user is not notified that the operation has finished, but she also does not have to respond to the dialog.

An application can install multiple work procedures, but Xt only processes one procedure at a time. The last work procedure installed has the highest priority, so it is the first one called, except if one work procedure installs another work procedure. In this case, the new procedure has a lower priority than the current one.

As you can see from running the program in the source code work procedures are called extremely frequently. In any real application, however, the task that is being performed is going to be more sophisticated and time-consuming than our example here. It is important that the operations you perform in a work procedure do not take too much time, or response time will suffer. A work procedure should return frequently enough to allow Xt to process user events, so that the operation of the entire application flows smoothly.

22.2.2 Using Timers

Using timers to process a task is very similar to using work procedures. Timers are not called as frequently as work procedures, so Xt can wait longer for user events to be generated and processed when the application uses timers. An application can add a timer using XtAppAddTimeOut(), which takes the following form:

   XtIntervalId
   XtAppAddTimeOut(app_context, interval, proc, client_data)
       XtAppContext         app_context;
       unsigned long        interval;
       XtTimerCallbackProc  proc;
       XtPointer            client_data;
The interval parameter specifies how long Xt waits before invoking the timer specified by proc. The main difference between using a timer and a work procedure is that a timer is called once and then automatically unregistered. To have a timer called at a regular interval, an application must call XtAppAddTimeOut() again from within the timer callback. With this exception, using timers is similar to using work procedures, so we aren't going to present a separate example here. See Chapter 11, Labels and Buttons, for some examples that use timers in various contexts.

22.2.3 Processing Events

If your application needs to start a lengthy process that is difficult to break into small pieces, you probably don't want to return control to Xt. In this case, you never lose control of your own processing loop, but you need to check for X events that need to be processed every once in a while. This technique is more convenient than work procedures for certain algorithms, since the application doesn't have to break out of its processing loop unless the user terminates the operation or the task completes naturally.

Processing events is somewhat complicated, but not because of the function calls involved or the design required to support the processing. The complications involve the decisions about which events you want to process, which you want to ignore, and which you want to put off handling until later. Say you are rendering a complicated graphic directly into a DrawingArea. While you are busy processing, you need to decide what to do if you get an incoming ButtonPress, Expose, or ConfigureNotify event, among others. In many cases, what you do depends on the widget or the window that receives the event.

When an application starts a lengthy task, it should post a WorkingDialog that displays an appropriate message. The WorkingDialog can also provide a Stop button to allow the user to terminate the task. During the operation, the user should not be interacting with other windows in the application. It is a good idea to change the cursor that is used in these windows, to make it clear that the windows will not respond to user input. When the operation is finished, the application needs to remove the WorkingDialog and reset the cursor.

If you are going to process events yourself, you probably want to write a routine that checks the event queue for relevant events. This routine would process all of the important events, such as those that cause widgets to be repainted. The routine should also handle events for the Stop button in the WorkingDialog, so the user can terminate the task.

The program listed in the source code supports the requirements that we have laid out for an application that processes its own events. XtSetLanguageProc() is only available in X11R5; there is no corresponding function in X11R4. XmStringCreateLocalized() is only available in Motif 1.2; XmStringCreateSimple() is the corresponding function in Motif 1.1.

   /* busy.c -- demonstrate how to use a WorkingDialog and to process
    * only important events.  e.g., those that may interrupt the
    * task or to repaint widgets for exposure.  Set up a simple shell
    * and a widget that, when pressed, immediately goes into its own
    * loop.  Set a timeout cursor on the shell and pop up a WorkingDialog.
    * Then enter loop and sleep for one second ten times, checking between
    * each interval to see if the user clicked the Stop button or if
    * any widgets need to be refreshed.  Ignore all other events.
    *
    * main() and get_busy() are stubs that would be replaced by a real
    * application; all other functions can be used as is.
    */
   #include <Xm/MessageB.h>
   #include <Xm/PushB.h>
   #include <X11/cursorfont.h>

   Widget shell;
   void TimeoutCursors();
   Boolean CheckForInterrupt();

   main(argc, argv)
   int argc;
   char *argv[];
   {
       XtAppContext app;
       Widget button;
       XmString label;
       void get_busy();

       XtSetLanguageProc (NULL, NULL, NULL);

       shell = XtVaAppInitialize (&app, "Demos",
           NULL, 0, &argc, argv, NULL, NULL);

       label = XmStringCreateLocalized ("Press Here To Start A Long Task");
       button = XtVaCreateManagedWidget ("button",
           xmPushButtonWidgetClass, shell,
           XmNlabelString,          label,
           NULL);
       XmStringFree (label);
       XtAddCallback (button, XmNactivateCallback, get_busy, NULL);

       XtRealizeWidget (shell);
       XtAppMainLoop (app);
   }

   void
   get_busy(widget, client_data, call_data)
   Widget widget;
   XtPointer client_data;
   XtPointer call_data;
   {
       int n;

       TimeoutCursors (True, True);
       for (n = 0; n < 10; n++) {
           sleep (1);
           if (CheckForInterrupt ()) {
               puts ("Interrupt!");
               break;
           }
       }
       if (n == 10)
           puts ("Done");
       TimeoutCursors (False, False);
   }

   /* The interesting part of the program -- extract and use at will */

   static Boolean stopped;  /* True when user wants to stop task */
   static Widget dialog;    /* WorkingDialog displayed */

   /* TimeoutCursors() -- turns on the watch cursor over the application
    * to provide feedback for the user that she's going to be waiting
    * a while before she can interact with the application again.
    */
   void
   TimeoutCursors(on, interruptable)
   Boolean on, interruptable;
   {
       static int locked;
       static Cursor cursor;
       extern Widget shell;
       XSetWindowAttributes attrs;
       Display *dpy = XtDisplay (shell);
       XEvent event;
       Arg args[5];
       int n;
       XmString str;
       extern void stop();

       /* "locked" keeps track if we've already called the function.
        * This allows recursion and is necessary for most situations.
        */
       if (on)
           locked++;
       else
           locked--;
       if (locked > 1 || locked == 1 && on == 0)
           return; /* already locked and we're not unlocking */

       stopped = False;
       if (!cursor)
           cursor = XCreateFontCursor (dpy, XC_watch);

       /* if on is true, then turn on watch cursor, otherwise, return
        * the shell's cursor to normal.
        */
       attrs.cursor = on ? cursor : None;

       /* change the main application shell's cursor to be the timeout
        * cursor or to reset it to normal.  If other shells exist in
        * this application, they will have to be listed here in order
        * for them to have timeout cursors too.
        */
       XChangeWindowAttributes (dpy, XtWindow (shell), CWCursor, &attrs);

       XFlush (dpy);

       if (on) {
           /* we're timing out, put up a WorkingDialog.  If the process
            * is interruptable, allow a "Stop" button.  Otherwise, remove
            * all actions so the user can't stop the processing.
            */
           n = 0;
           str = XmStringCreateLocalized ("Busy -- Please Wait.");
           XtSetArg (args[n], XmNmessageString, str); n++;
           dialog = XmCreateWorkingDialog (shell, "busy", args, n);
           XmStringFree (str);
           XtUnmanageChild (XmMessageBoxGetChild (dialog, XmDIALOG_OK_BUTTON));
           XtUnmanageChild (XmMessageBoxGetChild (dialog, XmDIALOG_HELP_BUTTON));
           if (interruptable) {
               str = XmStringCreateLocalized ("Stop");
               XtVaSetValues (dialog, XmNcancelLabelString, str, NULL);
               XmStringFree (str);
               XtAddCallback (dialog, XmNcancelCallback, stop, NULL);
           }
           else
               XtUnmanageChild (XmMessageBoxGetChild
                   (dialog, XmDIALOG_CANCEL_BUTTON));
           XtManageChild (dialog);
       }
       else {
           /* get rid of all button and keyboard events that occured
            * during the time out.  The user shouldn't have done anything
            * during this time, so flush for button and keypress events.
            * KeyRelease events are not discarded because accelerators
            * require the corresponding release event before normal input
            * can continue.
            */
           while (XCheckMaskEvent (dpy,
                   ButtonPressMask | ButtonReleaseMask | ButtonMotionMask
                   | PointerMotionMask | KeyPressMask, &event)) {
               /* do nothing */;
           }
           XtDestroyWidget (dialog);
       }
   }

   /* stop() -- user pressed the "Stop" button in dialog. */
   void
   stop(dialog, client_data, call_data)
   Widget dialog;
   XtPointer client_data;
   XtPointer call_data;
   {
       stopped = True;
   }

   /* CheckForInterrupt() -- check events in event queue and process
    * the interesting ones.
    */
   Boolean
   CheckForInterrupt()
   {
       extern Widget shell;
       Display *dpy = XtDisplay (shell);
       Window win = XtWindow (dialog);
       XEvent event;

       /* Make sure all our requests get to the server */
       XFlush (dpy);

       /* Let Motif process all pending exposure events for us. */
       XmUpdateDisplay (shell);

       /* Check the event loop for events in the dialog ("Stop"?) */
       while (XCheckMaskEvent (dpy,
              ButtonPressMask | ButtonReleaseMask | ButtonMotionMask |
              PointerMotionMask | KeyPressMask, &event)) {
           /* got an "interesting" event. */
           if (event.xany.window == win)
               XtDispatchEvent (&event); /* it's in our dialog.. */
           else /* uninteresting event--throw it away and sound bell */
               XBell (dpy, 50);
       }
       return stopped;
   }
This program is obviously for demonstration purposes only. To keep to the subject matter, we have made the main part of the program quite unrealistic and only use it to support the functions we are about to discuss. The application displays a single button that starts a "long task." The get_busy() callback routine is trivial, but it demonstrates the use of the TimeoutCursors() and CheckForInterrupt() routines.

The TimeoutCursors() routine is used to change the cursor for the main application shell and to post the WorkingDialog. The cursor is changed to a watch shape to give the user visual feedback that the main window is not responding to input. The routine uses the static variable locked to keep track of how many times it has been called with on set to True. The function does not reset the cursor and remove the WorkingDialog until a matching number of calls has been made with on set to False. This technique makes it possible for a low-level function in an application to call TimeoutCursors() at its beginning and end, without affecting higher-level loops that also call the function.

The routine stores the XC_watch cursor in the static cursor variable. The cursor is created using XCreateFontCursor(), which is why <X11/cursorfont.h> is included. TimeoutCursors() uses XChangeWindowAttributes() to change the cursor to the watch shape and to reset it to its normal shape when on is False. The cursor is modified for the window of the shell widget, which is the main window for the application. If your application uses multiple ApplicationShells or TopLevelShells, you will need to modify the function to change the cursor shape for all of the shells.

At this point, we call XFlush() to make sure that all of our requests have been sent to the server. The TimeoutCursors() function may be called from deep within an application, so there may be a number of server requests that are waiting and we want to be sure that the server knows about them now. If the are turning off the timeout cursor, we may also need to read any resulting events.

Now we determine whether we are locking or unlocking the application. If we are locking it, we create and post a WorkingDialog. The dialog is created with a standard message. If the interruptable parameter is True, we provide a Stop button by changing the label of the Cancel button. We also add a callback routine for button, so that we can actually stop the task in progress.

We should note that an application does not necessarily have to post a WorkingDialog, as long as it changes the cursor. The watch cursor provides enough feedback to indicate that the application is in a busy state. The decision about whether or not to post a dialog really depends on the length of the task being performed. For relatively short tasks, it typically doesn't make sense to provide a WorkingDialog, as it takes some time to actually create and post the dialog.

Now the application is in a busy state. However, the user has yet to see anything; events need to be processed in order for the dialog to be mapped to the screen. At this point, the CheckForInterrupt() routine takes over. This routine handles Expose events by calling XmUpdateDisplay(). This Motif function processes all of the Expose events in the event queue by causing the server to flush these events for all of the windows on the display. This processing may cause redrawing event handlers to be called for various widgets. If you have installed your own exposure routines for any widgets, be sure that they are not too time consuming, or you may find yourself in a bind. You can check to see which windows are going to be repainted before it actually happens by using XCheckMaskEvent() to process Expose events.

After any possible repainting has occurred, we check for any button or keyboard events in the event queue. If one has been generated, we extract it from the input queue using XCheckMaskEvent(). The function takes the following form:

   Bool
   XCheckMaskEvent(display, event_mask, event_return)
       Display   *display;
       long       event_mask;
       XEvent    *event_return;
This Xlib function looks for events in the queue that match event_mask. If there is a matching event, the event_return parameter is filled in with the event and the routine returns True. Otherwise, the function returns False and we can return. The event is processed only if it occurred within the WorkingDialog window. Since the application is busy, events in other windows are not processed. If the user did something in the WorkingDialog, we process the event because she may have activated the Stop button. If the button is not provided for the dialog, it does not affect the code here.

You should be aware that XCheckMaskEvent() removes the event from the queue. If you choose not to process an event, you cannot stick it back in the queue. If you retrieve an event out of the queue and don't want to process it, you should set an application-defined variable or flag that notifies the application that it must eventually deal with the event. Another alternative is to save the event by allocating a new XEvent structure and copying the data. Then you dispatch the event later, when you are prepared to handle it.

We do not check for KeyRelease events in CheckForInterrupt() for a very important reason that concerns how the X Toolkit Intrinsics handles accelerators. Say your application has a menu item that initiates a long, complicated process. The callback function for this menu item calls both TimeoutCursors() and CheckForInterrupt(), just like the get_busy() routine. Let's say that ALT-X is the accelerator for the menu item. When the user types this key sequence, the callback routine for the menu item is activated by the KeyPress events. At this time, the KeyRelease events associated with the accelerator are still in the event queue. If we checked for KeyRelease events in CheckForInterrupt(), the ones for the accelerator would get thrown away, since they did not occur in the WorkingDialog.

Throwing away these events is a problem because Xt uses an internal state machine to determine whether or not any particular sequence of keyboard events is an accelerator or a prefix for one. Since Xt would never get the accompanying KeyRelease events, it would think that the user is still entering a keyboard accelerator. Xt would not get out of that state until the matching events were given, with the result that no other keyboard events would work in the application until the user happened to type the same accelerator sequence. This situation is not a bug in Xt; Xt is simply doing what it must to handle acclerators. However, the situation does demonstrate the intricacies of handling events in X.

Getting back to CheckForInterrupt(), if the user presses the Stop in the dialog, the event is processed and the stop() callback routine is invoked. This routine simply sets the global variable stopped to True. By the time that CheckForInterrupt() is ready to return, stopped has been set, so the function returns True. If the WorkingDialog does not have a Stop button, the callback routine is not installed, so stopped is never set to True.

After the get_busy() routine finishes processing, it calls TimeoutCursors() again to unlock the application. When on is set to False, the routine uses XCheckMaskEvent() to look in the event queue for button and keyboard events. In this case, the events are thrown away, since the input is no longer useful. The routine also destroys the WorkingDialog. In one sense, TimeoutCursors() implements a kind of modality, similar to that discussed in Section #smodaldlg. However, modality alone cannot provide the functionality necessary to handle long-running tasks.

22.2.4 Updating the Display

As discussed earlier, XmUpdateDisplay() checks the event queue for all Expose events and processes them immediately. However, there are some circumstances under which the routine does not work as you might expect. For example, let's say that your application creates and posts a dialog that contains a DrawingArea widget. You call XSync() and XmUpdateDisplay() to make sure that the dialog is on the screen and fully exposed. After you call XClearWindow() to make sure the window is clear, you begin drawing. Unfortunately, you may find that nothing is drawn.

The problem is due to the redirection of events from the window manager and the way events are processed and queued. When a dialog is posted using XtManageChild() or XtPopup(), the toolkit calls XMapRaised() to raise the window to the top of the window stack. The call to XSync() sends the MapRequest event to the server, which redirects it to the window manager (e.g., mwm). A bottleneck can occur if the window manager is swapped out, which is a side effect of multi-tasking operating systems such as UNIX.

In this case, mwm may not react immediately to the redirection and can take an indeterminate amount of time to respond. The X server doesn't take this delay into account. It thinks that the event has been delivered properly, so your application believes that the window has been mapped. As a result, XmUpdateDisplay() doesn't get the Expose event that you were expecting and drawing does no good because the window still hasn't been mapped. When mwm gets around to mapping the window to the screen, the server generates the Expose event, but by now your application is off doing something else.

One solution to this problem is to change the design of your application so that it doesn't start drawing until the server actually generates the Expose events. In this case, you should post the dialog and immediately return control to the main event-processing loop (XtAppMainLoop()). If you have installed an event handler or a translation for the Expose event, the routine is called at the appropriate time. Another advantage to this design is that the drawing procedure is called anytime an Expose event occurs, which ensures that the window is always up-to-date.

In the source code we show another solution. This solution should be used only if you need to create, pop up, or manage a dialog and then immediately draw into the window. The ForceUpdate() routine ensures that the specified widget is visible before it returns.

   /* ForceUpdate() -- a superset of XmUpdateDisplay() that ensures
    * that a window's contents are visible before returning.
    * The monitoring of window states is necessary because an attempt to
    * map a window is subject to the whim of the window manager, which can
    * introduce a significant delay before the window is actually mapped
    * and exposed.  This function is intended to be called after XtPopup(),
    * XtManageChild() or XMapRaised().  Don't use it in other situations
    * as it may sit and process other unrelated events until the widget
    * becomes visible.
    */
   void
   ForceUpdate(w)
   Widget w; /* This widget must be visible before the function returns */
   {
       Widget diashell, topshell;
       Window diawindow, topwindow;
       XtAppContext cxt = XtWidgetToApplicationContext (w);
       Display *dpy;
       XWindowAttributes xwa;
       XEvent event;

       /* Locate the shell we are interested in */
       for (diashell = w; !XtIsShell (diashell); diashell = XtParent (diashell))
           ;

       /* Locate its primary window's shell (which may be the same) */
       for (topshell = diashell; !XtIsTopLevelShell (topshell);
               topshell = XtParent (topshell))
           ;  

       /* If the dialog shell (or its primary shell window) is not realized,
        * don't bother ... nothing can possibly happen.
        */
       if (XtIsRealized (diashell) && XtIsRealized (topshell)) {
           dpy = XtDisplay (topshell);
           diawindow = XtWindow (diashell);
           topwindow = XtWindow (topshell);

           /* Wait for the dialog to be mapped.  It's guaranteed to become so */
           while (XGetWindowAttributes (dpy, diawindow, &xwa) &&
                  xwa.map_state != IsViewable) {

               /* ...if the primary is (or becomes) unviewable or unmapped,
                * it's probably iconic, and nothing will happen.
                */
               if (XGetWindowAttributes (dpy, topwindow, &xwa) &&
                   xwa.map_state != IsViewable)
                   break;

               /* we are guaranteed there will be an event of some kind. */
               XtAppNextEvent (cxt, &event);
               XtDispatchEvent (&event);
           }
       }

       /* The next XSync() will get an expose event. */
       XmUpdateDisplay (topshell);
   }
This routine makes sure that a dialog is visible by waiting for the window of the dialog to be mapped to the screen.

22.2.5 Avoiding Forks

Before we close out this section, there is one more method of of executing tasks in the background that we should discuss. Beginning programmers tend to use library functions and system calls such as system(), popen(), fork(), and exec() to invoke external commands. Although these functions are perfectly reasonable, they can backfire quite easily on virtually any error condition. Recovering from these errors is the GUI programmer's nightmare, since there are so many different possible conditions to deal with.

The purpose of using these functions, of course, is to call another UNIX program and have it run concurrently with the main application. The system() and popen() functions fork a new process using the fork() system call. They also use some form of exec() so the new child process can invoke the external UNIX program. If the new process cannot fork, if there is something wrong with the external UNIX command, if there is a communications protocol error, or any one of a dozen other possible error conditions, there is no way for the external program to display an error message as a part of the main application.

It is unlikely that the external program would display a dialog box or any sort of reasonable user-interface element. It is illegal for a new process to use any of the widgets or windows in the main application because only one connection to the server is allowed per process. If the child process wants to post a dialog, it must establish a new connection to the X server and create an entirely new widget tree, as it is a separate application. Since most system utilities do not have graphical user interface front ends, this scenario is very unlikely. It is also entirely unreasonable to have any expectations of the external process, especially since other solutions are much easier.

If a separate process is necessary in order to accomplish a particular task, setting up pipes between the child application and the parent is usually the best alternative. The popen() function uses this method superficially, but it is not the most elegant solution. The routine only handles forking the new process and setting up half of a two-way pipe. The popen() function is used in several places throughout the book; check the index for those uses.

To really handle external processes and pipes properly, an application should do the following:

If the parent calls XtAppAddInput(), Xt can see the data the child sends through the pipe and invoke the callback routine associated with the file descriptor. XtAppAddInput() takes the following form:
   XtInputId
   XtAppAddInput(app_context, source, mask, proc, client_data)
       XtAppContext        app_context;
       int                 source;
       XtPointer           mask;
       XtInputCallbackProc proc;
       XtPointer           client_data;
The source parameter should be the side of the pipe that the parent uses to read data sent by the child process. The proc function is called when there is data to read on the pipe. When the function is called, the client_data is passed to the callback. For example, you can pass the process ID returned by fork(), so you can see if the process is still alive and read the data using read().

This discussion is merely presented as an overview, since the implementation details are beyond the scope of this book. For example, UNIX signals cause problems in a number of ways. The parent process is sent signals when the child dies or its process state changes. The child is also sent signals that are delivered to the parent by the user or other outside forces. Different forms of UNIX require that process groups be set up in different ways to avoid other problems with signals.

Another problem involves file descriptors that are set up as non-blocking files. If read() returns 0 with one of these descriptors, you may not know whether there is nothing to read or the end of the file has been reached, which means that the child process has terminated. Incidentally, popen() does not deal with any of these issues correctly, so building a new solution is the best thing to do in the long run.

You should really consult the programmer's guide for your UNIX system for more information on the techniques used to spawn new processes and communicate with them appropriately. Once you have a handle on those issues, it should be relatively easy to redirect text from file descriptors using the toolkit. For more information on XtAppAddInput(), including examples of how it can be used, see Volume Four, X Toolkit Intrinsics Programming Manual.

22.3 Dynamic Message Symbols

The MessageDialog is used to display many different types of messages; the image in the dialog helps the user identify the purpose of the dialog. The pixmaps used by the standard MessageDialogs are predefined by the Motif toolkit. When you are using the standard dialogs, you typically change the dialog's type rather than its symbol, since changing its type effectively changes the symbol that it displays. However, you can change the MessageDialog's symbol to a customized image using the XmNsymbolPixmap resource.

The resource takes a pixmap value that must be created before the resource is set. When the resource is set, the pixmap is not copied by the dialog widget. If the dialog is destroyed, you should be sure to free the pixmap unless you are using it elsewhere. If you are going to destroy the dialog using XtDestroyWidget() directly, you should get the pixmap by calling XtVaGetValues(), so that you can free it. However, the dialog can also be destroyed automatically, so you should also specify an XmNdestroyCallback procedure that is called whenever the dialog is destroyed.

the source code shows an example of using a custom image in a standard MessageDialog. The program also demonstrates how the dialog should clean up after itself. XtSetLanguageProc() is only available in X11R5; there is no corresponding function in X11R4. XmStringCreateLocalized() is only available in Motif 1.2; XmStringCreateSimple() is the corresponding function in Motif 1.1. XmFONTLIST_DEFAULT_TAG replaces XmSTRING_DEFAULT_CHARSET in Motif 1.2.

   /* warn_msg.c -- display a very urgent warning message.
    * Really catch the user's attention by flashing an urgent-
    * looking pixmap every 250 milliseconds.
    * The program demonstrates how to set the XmNsymbolPixmap
    * resource, how to destroy the pixmap and how to use timers.
    */
   #include <Xm/MessageB.h>
   #include <Xm/PushB.h>

   #include "bang0.symbol"
   #include "bang1.symbol"

   #define TEXT "Alert!0he computer room is ON FIRE!0ll of your e-mail will be lost."

   /* define the data structure we need to implement flashing effect */
   typedef struct {
       XtIntervalId   id;
       int            which;
       Pixmap         pix1, pix2;
       Widget         dialog;
       XtAppContext   app;
   } TimeOutClientData;

   main(argc, argv)
   int argc;
   char *argv[];
   {
       XtAppContext app;
       Widget toplevel, button;
       XmString label;
       void warning();

       XtSetLanguageProc (NULL, NULL, NULL);

       toplevel = XtVaAppInitialize (&app, "Demos",
           NULL, 0, &argc, argv, NULL, NULL);

       label = XmStringCreateLocalized (
           "Don't Even Think About Pressing This Button");
       button = XtVaCreateManagedWidget ("button",
           xmPushButtonWidgetClass, toplevel,
           XmNlabelString,          label,
           NULL);
       XmStringFree (label);

       /* set up callback to popup warning */
       XtAddCallback (button, XmNactivateCallback, warning, NULL);

       XtRealizeWidget (toplevel);
       XtAppMainLoop (app);
   }

   /* warning() -- callback routine for the button.  Create a message
    * dialog and set the message string.  Allocate an instance of
    * the TimeOutClientData structure and set a timer to alternate
    * between the two pixmaps.  The data is passed to the timeout
    * routine and the callback for when the user presses "OK".
    */
   void
   warning(parent, client_data, call_data)
   Widget parent;
   XtPointer client_data;
   XtPointer call_data;
   {
       Widget        dialog;
       XtAppContext  app = XtWidgetToApplicationContext (parent);
       XmString      text;
       extern void   done(), destroy_it(), blink();
       Display      *dpy = XtDisplay (parent);
       Screen       *screen = XtScreen (parent);
       Pixel         fg, bg;
       Arg           args[5];
       int           n, depth;
       TimeOutClientData *data = XtNew (TimeOutClientData);

       /* Create the dialog */
       n = 0;
       XtSetArg (args[n], XmNdeleteResponse, XmDESTROY); n++;
       dialog = XmCreateMessageDialog (parent, "danger", args, n);

       XtUnmanageChild (XmMessageBoxGetChild (dialog, XmDIALOG_CANCEL_BUTTON));
       XtUnmanageChild (XmMessageBoxGetChild (dialog, XmDIALOG_HELP_BUTTON));

       XtAddCallback (dialog, XmNokCallback, done, NULL);
       XtAddCallback (dialog, XmNdestroyCallback, destroy_it, data);

       /* now that dialog has been created, it's colors are initialized */
       XtVaGetValues (dialog,
           XmNforeground, &fg,
           XmNbackground, &bg,
           XmNdepth, &depth,
           NULL);

       /* Create pixmaps that are going to be used as symbolPixmaps.
        * Use the foreground and background colors of the dialog.
        */
       data->pix1 = XCreatePixmapFromBitmapData (dpy, XtWindow (parent),
           bang0_bits, bang0_width, bang0_height, fg, bg, depth);
       data->pix2 = XCreatePixmapFromBitmapData (dpy, XtWindow (parent),
           bang1_bits, bang1_width, bang1_height, fg, bg, depth);
       /* complete the timeout client data */
       data->dialog = dialog;
       data->app = app;

       /* Add the timeout for blinking effect */
       data->id = XtAppAddTimeOut (app, 1000L, blink, data);

       /* display the help text and the appropriate pixmap */
       text = XmStringCreateLtoR (TEXT, XmFONTLIST_DEFAULT_TAG);
       XtVaSetValues (dialog,
           XmNmessageString,      text,
           XmNsymbolPixmap,       data->pix2,
           NULL);
       XmStringFree (text);

       XtManageChild (dialog);
       XtPopup (XtParent (dialog), XtGrabNone);
   }

   /* blink() -- visual blinking effect for dialog's symbol.  Displays
    * flashing ! symbol, restarts timer and saves timer id.
    */
   void
   blink(client_data, id)
   XtPointer client_data;
   XtIntervalId *id;
   {
       TimeOutClientData *data = (TimeOutClientData *) client_data;

       data->id = XtAppAddTimeOut (data->app, 250L, blink, data);
       XtVaSetValues (data->dialog,
           XmNsymbolPixmap,  (data->which = !data->which) ?
               data->pix1 : data->pix2,
           NULL);
   }

   /* done() -- called when user presses "OK" in dialog or
    * if the user picked the Close button in system menu.
    * Remove the timeout id stored in data, free pixmaps and
    * make sure the widget is destroyed (which is only when
    * the user presses the "OK" button.
    */
   void
   done(dialog, client_data, call_data)
   Widget dialog;
   XtPointer client_data;
   XtPointer call_data;
   {
       XtDestroyWidget(dialog);
   }

   /* destroy_it() -- called when dialog is destroyed.  Removes
    * timer and frees allocated data.
    */
   void
   destroy_it(dialog, client_data, call_data)
   Widget dialog;
   XtPointer client_data;
   XtPointer call_data;
   {
       TimeOutClientData *data = (TimeOutClientData *) client_data;
       Pixmap symbol;

       XtRemoveTimeOut (data->id);
       XFreePixmap (XtDisplay (data->dialog), data->pix1);
       XFreePixmap (XtDisplay (data->dialog), data->pix2);
       XtFree (data);
   }
The dialog is created in warning(), the callback routine for the PushButton in the main window. We create a simple MessageDialog that does not have a predefined symbol so we can specify a custom image. The dialog actually uses two symbols that are exchanged every 250 milliseconds by a timer callback routine. The output of this program is shown in the figure.

figs.eps/V6a.21.04.eps.png
Output of warn_msg.c

To implement the flashing symbol, we must associate certain information with the dialog. Basically, we need to keep track of the two pixmaps and the timer routine. All of the information is placed in a single data structure, so we can pass the structure around as client data. We can also use multiple structure variables to store information about multiple dialogs. The TimeOutClientData is defined as follows:

   typedef struct {
       XtIntervalId   id;
       int            which;
       Pixmap         pix1, pix2;
       Widget         dialog;
       XtAppContext   app;
   } TimeOutClientData;
The warning() routine allocates a new instance of the structure using XtNew(), since it is going to create a new dialog and it needs a unique structure for the dialog. The routine uses XmCreateMessageDialog() to create the dialog. We unmanage the Cancel and Help buttons and specify a callback for the OK button. The done() callback simply calls XtDestroyWidget() , which causes the XmNdestroyCallback to be called. We also set the XmNdeleteResponse resource for the dialog to XmDESTROY . This setting causes the Motif toolkit to destroy the dialog if the user dismisses it using the Close button on the window menu,

Since we are not reusing the dialog or its data, we must be sure to free the pixmaps, release the timer, and free the allocated data structure when the dialog is destroyed. To be sure that these tasks take place, we install a callback function for the XmNdestroyCallback resource. The destroy_it() routine handles all of the cleanup for the dialog.

Before we create the pixmaps that are used in the dialog, we retrieve the dialog's foreground and background colors using XtVaGetValues() so that the new pixmaps can use the same colors. Once the colors are known, we can create the pixmaps and finish initializing the fields in the TimeOutClientData structure. The dialog field of the structure points to the MessageDialog. We call XtAppAddTimeOut() to start the timer that controls the flashing effect and set the id field to the timer ID.

We perform a final bit of setup for the dialog by specifying the XmNsymbolPixmap and XmNmessageString resources. Once everything is set up, the function returns, Xt regains control, and normal event processing resumes. After the initial one-second interval times out, the blink() function is called. This routine adds another timeout for 250 milliseconds and switches the pixmaps displayed in the dialog. This loop continues until the user dismisses the dialog, at which time it is destroyed, the pixmaps are freed, the timer is removed, and the TimeOutClientData structure is freed.

Since we created a simple MessageDialog that does not have a predefined image, we did not have to get a handle to the XmNsymbolPixmap for the dialog and destroy it. However, if you decide to change the pixmap for one of the standard dialogs that has a predefined symbol, like the ErrorDialog, you should get its pixmap and free it. In this case, you should use XmDestroyPixmap() rather than XFreePixmap(). The Motif dialogs use XmGetPixmap() to create their images, so the pixmaps must be freed with the companion routine XmDestroyPixmap(). See Section #spixmaps in Chapter 3, Overview of the Motif Toolkit, for a discussion on XmGetPixmap().

Although changing the symbol pixmap in a dialog is quite simple, using the feature effectively requires a careful design to make sure that all of the pointers and data structures are destroyed appropriately. Being meticulous about cleaning up after destroyed widgets and other objects is sometimes a difficult task because of the many ways in which the user can destroy them. However, eliminating these possible memory leaks enables a program to run longer and more efficiently.

22.4 Summary

Developing a real application often involves a lot of work to get the details just right. Some of the most interesting problems in designing an interface cannot be solved by Motif alone. Motif provides the basic user interface, but you must make it work with your application. A solid understanding of the fundamentals of the X Window System and the X Toolkit Intrinsics makes it easier to fine-tune the interface for an application. This chapter has presented some solutions to common problems that require using both Xlib and Xt routines in conjunction with the Motif toolkit.


Contents Previous Next