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.
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.
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.
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.
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.
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.
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:
The four methods fall into two basic categories:
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.
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.
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.
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.
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.
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.
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:
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.
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.
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.
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.