Contents Previous Next

15 Text Widgets


This chapter explains how the Text and TextField widgets can be used to provide text-entry capabilities in an application. These widgets can be used for a variety of purposes, from a simple data-entry field to a full-fledged text editor. The chapter describes the selection mechanisms provided by the widgets and how they can be used to communicate with other applications via the clipboard. The widgets also allow the programmer to control the format of the data that is entered by the user.

Despite all that can be done with menus, buttons, and lists, there are times when the user can best interact with an application by typing at the keyboard. The Text widget is usually the best choice for providing this style of interface, as it provides full-featured text editing capabilities. The Text widget can be used anywhere the user might be expected to type free-form text, such as in a compose window in a mail application. Unlike standard text editors, the Text widget supports the point-and-click model that people expect from GUI-based applications. The TextField widget provides a single-line data entry field with the same set of editing commands as the Text widget, but it requires less overhead. Text widgets can also be used in output-only mode to display more textual information than is practical with a label or a button.

Even though the text widgets allow for complex interaction, they still provide simple mechanisms for program control. The widgets have resources that access the text, as well as control their behavior. They also provide callback routines that allow an application to intervene on actions that add text, delete text, or move the insertion cursor. The widgets support keyboard management methods that control the editing style, paging style, character positioning, and line-wrapping. There are also convenience routines that enable quick and simple access to the clipboard.

The text widgets do have their limitations. For example, they do not support multiple colors or fonts, so a single widget can only use one color and one font. There is no support for text formatting such as paragraph specifications, automatic line numbering, or indentation, so you cannot create WYSIWYG documents. WYSIWYG stands for What You See Is What You Get. This term is used to describe page formatting programs that can produce camera-ready documents that match what is displayed on the screen. The Text widget is not a terminal emulator; it cannot be used to run interactive programs. The widgets cannot display multi-media objects either, which means that it is not possible to insert graphics into the text stream.

There are some cases where a text widget is not the most appropriate user-interface element, even though you are displaying text. For example, a Text widget should not be used to display a list whose items can be individually selected; that is the job of the List widget. Text that cannot be edited, or selected should be displayed in a Label widget. Chapter 11, Labels and Buttons, and Chapter 12, The List Widget, describe the appropriate uses of these components.

If you have not used the Motif Text widget, you should familiarize yourself with one before getting too involved in this chapter. Running some of our introductory examples should provide an adequate platform for experimentation. the figure shows an application that uses several Text widgets. Two widgets are used for single-line data entry. The widget with the ScrollBars attached to it is used for editing multiple lines.

figs.eps/V6a.14.01.eps.png
An editor application with two different styles of Text widgets

The Text widget supports both single-line and multiline editing. In single-line mode, which is the default mode, newlines are ignored. However, single-line text entry is usually done with the TextField widget class. This widget class is a completely separate class, not a subclass, of Text that is lighter-weight because it only supports single-line text editing. The TextField widget was added to the toolkit in Motif 1.1; in early versions of that release the widget had a number of bugs that made it difficult to use. These bugs have been fixed in later releases. Although they are two separate widget classes, the Text and TextField widgets provide many of the same resources and convenience routines. We will point out the differences as we go, but keep in mind that there are two widget classes so you don't get confused as we discuss them throughout this chapter.

Since the TextField widget cannot handle multiline editing, you must use the Text widget for this purpose. When multiple lines are used for editing, the number of lines typically grows and shrinks dynamically as the user edits the text. The Text widget is often used in a scrollable window, so that the user can view different portions of the underlying text. The combination of a Text widget and a ScrolledWindow widget is called a ScrolledText object. This object is not a widget class, although there is a convenience routine, XmCreateScrolledText(), that allows you to create both widgets at once.

15.1 Interacting With Text Widgets

The Text and TextField widgets are highly configurable in terms of appearance and behavior. Given the level of sophistication for both the programmer and the user, the widgets should not be taken lightly or underestimated. The ease of configurability should not tempt you to enforce your personal ideas about how a text editor should work. The best thing to do with text widgets is configure them as minimally as possible to suit the needs of your program. You should let the user have control over as many details of their display and operation as possible. This laissez-faire approach ensures that your application is more compatible with other Motif programs.

15.1.1 Inserting Text

The user interface for the text widgets follows the point-and-click model of interaction. The insertion cursor indicates the position where the next character that is typed will be inserted. In Motif 1.2, the insertion position is marked by an I-beam cursor. Using the left mouse button, the user can click on a new location in the widget to move the insertion cursor there, so text may be inserted at any location in the widget.

In Motif 1.1, the text widgets used two different cursors. The I-beam was used to mark the insertion position, while a caret (^) was used as the destination cursor when it was separate from the insertion cursor. The destination cursor showed the last position that text was inserted, edited, or selected. Having two separate cursors was confusing for users and programmers, so the model has been simplified for Motif 1.2 to use only the I-beam cursor.

The text widgets have predefined action routines that allow the user to perform simple editing operations such as moving one character to the right or deleting backwards to the beginning of the line. The user can specify translations in a resource file that modify the input behavior of the widgets. The widgets are modeless, so they are always in text-insertion mode. In Motif 1.2, there is an action that puts the Text widget in overstrike mode, while in Motif 1.1, it is programmatically possible to emulate such a mode using multiple action routines.

The user can use the action routines provided by the widgets to set up the translation table to mimic an editor such as emacs. The Text widget does not insert nonprintable characters, so users typically bind control-character sequences to editing action routines. An editor like vi cannot be emulated because there is no distinction between command mode and text-entry mode.

15.1.2 Selecting Text

Users have become accustomed to the ability to cut and paste text between windows in GUI-based applications. Cut and paste is more difficult for the programmer to implement with the X Window System than a system where a single vendor controls all of the variables, because the nature of X requires a more general solution . For example, applications running on the same display may actually be executing on different systems; these systems may have different byte orders or other differences in the underlying data format. Currently, only text selections are implemented, which makes byte order irrelevant. However, the mechanism is designed to allow transparent transfer of any kind of data. In order to insulate cut and paste operations from dependencies like these, all communication between applications is implemented via the X server. Data that is cut is stored in a property on the X server. A property is simply a named piece of data associated with a window and stored on the server.

The Interclient Communications Conventions Manual Reprinted as Appendix L in Volume Zero, X Protocol Reference Manual . (ICCCM) defines a set of standard property names to be used for operations such as cut and paste and lays out rules for how applications should interact with these properties. According to the ICCCM, text that is selected is typically stored in the PRIMARY property. The SECONDARY property is defined as an alternate storage area for use by applications that wish to support more than one simultaneous selection operation or that wish to support operations requiring two selections, such as switching the contents of the two selections. The CLIPBOARD property is defined as a longer-term holding area for data that is actually cut (rather than simply copied) from the application's window. When we refer to the primary, secondary, or clipboard selection, we mean the property of the same name.

The most common implementation of the selection mechanism is provided by the X Toolkit Intrinsics. The low-level routines that are used to implement selections are described in detail in Volume Four, X Toolkit Intrinsics Programming Manual. In general, applications such as xterm and widgets such as the Motif Text widget encapsulate this functionality in action routines that are invoked by the user with mouse button or key combinations.

The user can select text in a Motif Text widget by pressing the left mouse button and dragging the pointer across the text. The selected text is displayed in reverse video. When the button is released, the text widget has ownership of the selection, but no text is copied. The selection can be extended either by pressing the SHIFT key and then dragging the pointer with the left mouse button down, or by pressing any of the arrow keys while holding down the SHIFT key. In addition to the click-and-drag technique for text selection, the Text widget also supports multiple-clicking techniques: double-clicking selects a word, triple-clicking selects the current line, and quadruple-clicking selects all of the text in the widget. An important constraint imposed by the ICCCM is that only one window may own a selection property at one time, which means that once the user makes another primary selection, the original selection is lost.

The user can copy text directly from the primary selection into the Text widget by clicking the middle mouse button at the location where the text is to be inserted. This action is sometimes called stuffing the selection into the widget. The user can stuff text at any location in the text stream, as long as the location is not inside the current selection. The text is copied only when the middle mouse button is clicked, which is defined as a quick succession of press and release actions. The operation does not take place simply because the middle mouse button is pressed, as this action is used for drag and drop operations.

In Motif 1.2, the Text and TextField widgets support the drag-and-drop model of transferring textual data. Once text has been selected in a widget, the selection can be dragged by pressing the middle mouse button over the selection and dragging the pointer. The text is transferred when the user releases the middle mouse button with the pointer over another location in the same widget or over another text widget. By default, the text is moved, which means that the original text is deleted once the transfer is complete. The user can force a copy operation by holding down the CONTROL key while dragging the pointer and releasing the mouse button. For more information on drag and drop, see Chapter 18, Drag and Drop.

The secondary selection is used by the Motif text widgets to copy text directly within a widget. The user performs this type of operation by first selecting the location where the copied text is to be placed; clicking the left mouse button places the insertion point. Then the text that is to be copied is selected by pressing and dragging the middle mouse button while the ALT key is pressed. The selected text is underlined rather than highlighted in reverse video. When the button is released, the selected text is immediately stuffed at the location of the insertion cursor. Unlike the primary selection, which may be retrieved many times, the secondary selection is immediate and can only be stuffed once.

The third location for holding text is the clipboard selection. The clipboard selection is designed to be used as a longer-term storage area for data. For example, MIT provides a client called xclipboard that asserts ownership of the CLIPBOARD property and provides a user interface to it. xclipboard not only allows a selection to survive the termination of the window where the data was originally selected, but it also allows for the storage of multiple selections. The user can view all of the selections before deciding which one to paste.

OSF's implementation of the clipboard is incompatible with xclipboard. If xclipboard is running, any Motif routines that attempt to store data on the clipboard will not succeed. The Motif routines temporarily try to lock the clipboard, and xclipboard will not give up its own lock. Motif treats the clipboard as a two-item cache. Only Motif applications that use the clipboard routines described in Chapter 17, The Clipboard, can interoperate using this selection. The advantage of the Motif implementation is that it provides functionality far beyond that provided by the standard MIT clients. With xterm and the Athena widgets, selections can really only be used for copy-and-paste operations; the selected text is unchanged. The Motif Text widget, by contrast, allows you to cut, copy, clear, or type over a selection. While there is a translation and action-based interface defined for these operations, it is typically not implemented.

As described in Chapter 2, The Motif Programming Model, Motif defines translations in terms of virtual key bindings. By default, the virtual keys osfCut, osfCopy, osfPaste , et. al., are not bound to any actual keys. If a user wants to use these keys, he must specify the bindings in a .motifbind file in his home directory. The interface for these features is usually provided by menu items associated with the Text widget, as we will demonstrate in this chapter.

When text is selected in a Text widget, it is automatically stored in the primary selection. When one of the Text widget functions, such as XmTextCut(), is used, the text is also stored in the clipboard selection. Most users will be completely unaware that there are separate holding areas for selected text. If your application gets heavily into cutting and pasting, you may find that the fusion of the primary and clipboard selections in the convenience routines is confusing. You should be careful to implement the selection operations so that the different properties are transparent to the user.

The reference pages for the Text and TextField widgets (in Volume Six B, Motif Reference Manual; Section 2, Motif and Xt Widget Classes) lists the default translations for the widgets. See Volume Four, X Toolkit Intrinsics Programming Manual , for a description of how to programmatically alter translation tables; see Volume Three, X Window System User's Guide, for a description of how a user can customize widget translations. See Chapter 17, The Clipboard, for a discussion of the lower-level Motif clipboard functions.

15.2 Text Widget Basics

In order to understand the complexities of the Text and TextField widgets, you need to know about some of the basic resources and functions that they provide. This section describes the fundamentals of working with text widgets, including how to create the widgets, how to work with the textual data, and how to control simple aspects of appearance and behavior. Applications that wish to use the Text widget need to include the file <Xm/Text.h>. TextField widgets require the file <Xm/TextF.h>. You can create a Text widget using XtVaCreateManagedWidget() as usual:

   Widget text_w;

   text_w = XtVaCreateManagedWidget("name",
       xmTextWidgetClass, parent,
       resource-value-list,
       NULL);
To create a TextField widget instead, specify the class as xmTextFieldWidgetClass.

15.2.1 The Textual Data

The XmNvalue resource of the Text and TextField widgets provides the most basic means of access to the internal text storage for the widgets. Unlike the other widgets in the Motif toolkit that use text, the text widgets do not use compound strings for their values. Instead, the value is specified as a regular C string, as shown in the source code XtSetLanguageProc() is only available in X11R5; there is no corresponding function in X11R4.

   /* simple_text.c -- Create a minimally configured Text widget */
   #include <Xm/Text.h>

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

       XtSetLanguageProc (NULL, NULL, NULL);

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

       XtVaCreateManagedWidget ("text", xmTextWidgetClass, toplevel,
           XmNvalue,     "Now is the time...",
           NULL);

       XtRealizeWidget (toplevel);
       XtAppMainLoop (app);
   }
This short program simply creates a Text widget with the initial value shown in the figure.

figs.eps/V6a.14.02.eps.png
Output of simple_text.c

In Motif 1.2, both widgets also provide the XmNvalueWcs resource for storing a wide-character representation of the text value. For more information on using the text widgets in an internationalized application, see Section #stexti18n. The initial value of the XmNvalue resource may be set either when the widget is created or by using XtVaSetValues() after the widget has been created. The value of the resource always represents the entire text of the widget. You can also use a Motif convenience routine, XmTextSetString(), to set the text value. This routine takes the following form:

   void
   XmTextSetString(text_w, value)
       Widget  text_w;
       char   *value;
This routine works for both Text and TextField widgets. The TextField widget has a corresponding routine, XmTextFieldSetString(), but it only works for TextField widgets. If you are using both types of text widgets in an application, we recommend using the Text widget routines to manipulate all of the widgets. Since these routines work with both types of widgets, you don't need to keep track of the widget types.

Although the convenience routine and XtVaSetValues() produce the same results, the convenience routine may be more efficient since it accesses the internals of the widget directly, while the XtVaSetValues() method involves going through Xt. On the other hand, if you are setting a number of resources at the same time, the XtVaSetValues() method is better because all of the resources can be set in a single function call. Whichever function you use, the text value is copied into the internals of the widget, and the displayed value is changed accordingly.

If, for whatever reason, you are making multiple changes in a short period of time to the text in a Text widget, you may have problems with visual flashing in the widget. With Motif 1.2, you can solve this problem by calling XmTextDisableRedisplay() to turn off visual updating in the widget. After the call, the appearance of the widget remains unchanged until XmTextEnableRedisplay() is called. You can access the textual data in a Text widget using XtVaGetValues() or XmTextGetString(). XmTextGetString() allocates enough space (using XtMalloc() ) for all of the text in the widget and returns a pointer to the allocated data. You can modify the returned data any way you like, and then you must free it using XtFree() when you are done. The code fragment below demonstrates the use of XmTextGetString():

   char *text;

   if (text = XmTextGetString (text_w)) {
       /* manipulate text in whatever way is necessary */
       ...
       /* free text or there will be a memory leak */
       XtFree (text);
   }
XmTextGetString() works with both Text and TextField widgets, while the corresponding TextField routine, XmTextFieldGetString() , only works with TextField widgets. In Motif 1.2, you can also use XmTextGetSubstring() to get a copy of a portion of the text in a Text widget.

The alternative to XmTextGetString() is the Xt function XtVaGetValues(). The Text widget responds to XtVaGetValues() by allocating memory and returning a copy of the text. As a result, this data must be freed after use. This use of the GetValues() method is different from most other resources. For most resources, XtVaGetValues() returns a pointer to internal data that should be treated as read-only data. In order to avoid memory leaks, you need to be sure to free the memory that is allocated by XtVaGetValues() for the XmNvalue resource, as shown in the following code fragment:

   char *text;

   XtVaGetValues (text_w, XmNvalue, &text, NULL);
   /* manipulate text in whatever way is necessary */
   ...
   /* free text or there will be a memory leak */
   XtFree (text);

Getting the value of a Text widget can be an expensive operation if the widget contains a large amount of text. In all situations, whenever text is retrieved from the Text widget with any function, the length of time the data is valid is only guaranteed until the next Xt call into the same Text widget; what any particular call might do to the internal text stream is undefined, and that information will not be reflected in the current character pointer handle you may have.

A Text widget may contain an arbitrarily large amount of text, assuming that there is enough memory on the computer running the client application. The text for a widget is not stored on the X server; only the client computer stores widget-specific information. The server displays a bitmap rendition of what the Text widget chooses to show. The XmNmaxLength resource specifies the upper limit on the number of characters the user can type into a Text widget. The default value of this resource is the largest integer for the particular system, so it is likely that the user's computer will run out of memory before the Text widget's maximum capacity is reached. You can lower the value of the resource to limit the number of characters that the user can input to a particular Text widget.

The Text widget does not use a temporary file to store its data. All of the data resides in memory on the machine, so you cannot use a Text widget to browse or edit a file directly. Instead, you load the contents of a file into a Text widget and allow the user to edit the internal buffer. The application controls when to rewrite files with updated data. An application can also provide an interface that allows the user to control this action. Applications that use Text widgets to edit vital information should make provisions for data recovery if the system fails or the application terminates unexpectedly. The Text widget does not support this type of recovery.

15.2.2 Single and Multiple Lines

In the source code the Text widget provides a single-line text entry area that is 20 columns wide; it is shown in the figure. Both the single-line editing style and the width are default values. The width of each column is based on the font that is used for the text. Since the widget uses the single-line editing style, nothing happens when the user presses RETURN in the widget. If the user types more text than the widget can display, the text scrolls to the left. Since newlines are not interpreted when they are typed by the user, textual data is always a single line. It is possible to set XmNvalue to a string that contains newline characters in a single-line Text widget, but the interaction with the user is undefined, and the widget produces confusing behavior. The user can resize the widget to make it appear large enough to display multiple lines, but this action does not affect the operation of the widget or the way it handles input.

Multiline editing allows the user to enter newlines into a Text widget and provides the capability to edit a large amount of text. The switch from single-line to multiline causes a number of changes in the behavior of the widget. For example, now widget geometry must be considered in order to determine the amount of text that is visible at one time. The Text widget may need to be placed in a ScrolledWindow, so that the user can view all of the text.

Single or multiline editing is controlled through the XmNeditMode resource. The value of the resource can be either XmSINGLE_LINE_EDIT or XmMULTI_LINE_EDIT. While the two editing modes are quite different in concept, it should be quite intuitive when to use the different modes. Single-line text entry areas are commonly used to prompt for file and directory names, short phrases, or single words. They are also useful for command-line entry in applications that were originally based on a tty-style interface. Multiline editing is used for editing files or other large quantities of text.

15.2.3 Scrollable Text

The layout of a multiline Text widget can be difficult to manage, especially if the text is editable by the user. An application needs to decide how many lines of text are displayed, how to handle the layout when the user adds new text, and how to deal with resizing the Text widget. The easiest way to manage an editable multiline Text widget is to create it as part of a ScrolledText compound object. The ScrolledText object is not a widget class in and of itself, but rather a compound object that is composed of a Text widget and a ScrolledWindow widget.

When you create a ScrolledText object, the ScrolledWindow automatically handles scrolling the text in the Text widget. Basically, the two widget classes have hooks and procedures that allow them to cooperate intelligently with each other. As of Motif 1.2, the performance of the ScrolledText object has improved considerably. One unfortunate side-effect of the performance improvement is that subclasses of the Text widget may not work under Motif 1.2, due to the addition of a new data structure. In previous releases, scrolling operations could be quite slow when the Text widget contained a large amount of text.

You can create a ScrolledText object using the Motif convenience routine XmCreateScrolledText(), which takes the following form:

   Widget
   XmCreateScrolledText(parent, name, arglist, argcount)
       Widget   parent;
       char    *name;
       ArgList  arglist;
       Cardinal argcount;
This routine is not a variable-argument list function; it uses the argument-list style of setting resources with the XtSetArg() macro.

XmCreateScrolledText() creates a ScrolledWindow widget and a Text widget as its child. The routine returns a handle to the Text widget; you can get a handle to the ScrolledWindow using the function XtParent(). When you are laying out an application that uses ScrolledText objects, you should be sure to use XtParent() to get the ScrolledWindow widget, since that is the widget that you need to position.

For purposes of specifying resources, the ScrolledWindow takes the name of the Text widget with the suffix SW . For example, if the name of the Text widget is name, its ScrolledWindow parent widget has the name nameSW.

If you specify an argument list in a call to XmCreateScrolledText(), the resources are set for the Text widget or the ScrolledWindow as appropriate. The routine also sets some resources for the ScrolledWindow so that scrolling is handled automatically. You should be sure to set the XmNeditMode resource to XmMULTI_LINE_EDIT, since it doesn't make sense to have a single-line Text widget in a ScrolledWindow. If you don't set the resource, the Text widget defaults to single-line editing mode. The behavior of a single-line Text widget (or a TextField widget) in a ScrolledWindow is undefined.

XmCreateScrolledText() is adequate for most situations, but you can also create the two widgets separately, as shown in the following code fragment:

   Widget scrolled_w, text_w;

   scrolled_w = XtVaCreateManagedWidget ("scrolled_w",
       xmScrolledWindowWidgetClass, parent,
       XmNscrollingPolicy,          XmAPPLICATION_DEFINED,
       XmNvisualPolicy,             XmVARIABLE,
       XmNscrollBarDisplayPolicy,   XmSTATIC,
       XmNshadowThickness,          0,
       NULL);

   text_w = XtVaCreateManagedWidget ("text",
       xmTextWidgetClass, scrolled_w,
       XmNeditMode,                 XmMULTI_LINE_EDIT,
       ...
       NULL);
We create the ScrolledWindow widget with the same resource setting that the Motif function uses. Since we are creating the ScrolledWindow ourselves, we can give it our own name. The Text widget itself is created as a child of the ScrolledWindow. In this situation, it is clear that the parent of the ScrolledWindow controls the position of both of the widgets.

This creation method makes the programmer responsible for managing both of the widgets. You may also need to handle the case in which the widgets are destroyed. When you call XmCreateScrolledText(), the routine installs an XmNdestroyCallback on the Text widget that destroys the ScrolledWindow parent. When you create the widgets yourself, you also need to be sure that they are destroyed together, either by destroying them explicitly or installing a callback routine on the Text widget. Unless you are creating and destroying ScrolledText objects dynamically, this issue should not be a concern.

the source code shows a simple file browser that displays the contents of a file using a ScrolledText object. The user can specify a file by typing a filename in the TextField widget below the Filename: prompt. The user can also select a file from the FileSelectionDialog that is popped up by the Open entry on the File menu. The specified file is displayed immediately in the Text widget. 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.

   /* file_browser.c -- use a ScrolledText object to view the
    * contents of arbitrary files chosen by the user from a
    * FileSelectionDialog or from a single-line text widget.
    */
   #include <X11/Xos.h>
   #include <Xm/Text.h>
   #include <Xm/TextF.h>
   #include <Xm/FileSB.h>
   #include <Xm/MainW.h>
   #include <Xm/RowColumn.h>
   #include <Xm/LabelG.h>
   #include <sys/types.h>
   #include <sys/stat.h>
   #include <stdio.h>

   main(argc, argv)
   int argc;
   char *argv[];
   {
       Widget        top, main_w, menubar, menu, rc, text_w, file_w;
       XtAppContext  app;
       XmString      file, open, exit;
       extern void   read_file(), file_cb();
       Arg           args[10];
       int           n;

       XtSetLanguageProc (NULL, NULL, NULL);

       /* initialize toolkit and create toplevel shell */
       top = XtVaAppInitialize (&app, "Demos",
           NULL, 0, &argc, argv, NULL, NULL);

       /* MainWindow for the application -- contains menubar
        * and ScrolledText/Prompt/TextField as WorkWindow.
        */
       main_w = XtVaCreateManagedWidget ("main_w",
           xmMainWindowWidgetClass, top, NULL);

       /* Create a simple MenuBar that contains one menu */
       file = XmStringCreateLocalized ("File");
       menubar = XmVaCreateSimpleMenuBar (main_w, "menubar",
           XmVaCASCADEBUTTON, file, 'F',
           NULL);
       XmStringFree (file);

       /* Menu is "File" -- callback is file_cb() */
       open = XmStringCreateLocalized ("Open...");
       exit = XmStringCreateLocalized ("Exit");
       menu = XmVaCreateSimplePulldownMenu (menubar, "file_menu", 0, file_cb,
           XmVaPUSHBUTTON, open, 'O', NULL, NULL,
           XmVaSEPARATOR,
           XmVaPUSHBUTTON, exit, 'x', NULL, NULL,
           NULL);
       XmStringFree (open);
       XmStringFree (exit);

       /* Menubar is done -- manage it */
       XtManageChild (menubar);

       rc = XtVaCreateWidget ("work_area", xmRowColumnWidgetClass, main_w, NULL);
       XtVaCreateManagedWidget ("Filename:", xmLabelGadgetClass, rc,
           XmNalignment, XmALIGNMENT_BEGINNING,
           NULL);
       file_w = XtVaCreateManagedWidget ("text_field",
           xmTextFieldWidgetClass, rc, NULL);

       /* Create ScrolledText -- this is work area for the MainWindow */
       n = 0;
       XtSetArg(args[n], XmNrows,      12); n++;
       XtSetArg(args[n], XmNcolumns,   70); n++;
       XtSetArg(args[n], XmNeditable,  False); n++;
       XtSetArg(args[n], XmNeditMode,  XmMULTI_LINE_EDIT); n++;
       XtSetArg(args[n], XmNcursorPositionVisible,  False); n++;
       text_w = XmCreateScrolledText (rc, "text_w", args, n);
       XtManageChild (text_w);

       /* store text_w as user data in "File" menu for file_cb() callback */
       XtVaSetValues (menu, XmNuserData, text_w, NULL);
       /* add callback for TextField widget passing "text_w" as client data */
       XtAddCallback (file_w, XmNactivateCallback, read_file, text_w);

       XtManageChild (rc);

       /* Store the filename text widget to ScrolledText object */
       XtVaSetValues (text_w, XmNuserData, file_w, NULL);

       XmMainWindowSetAreas (main_w, menubar, NULL, NULL, NULL, rc);
       XtRealizeWidget (top);
       XtAppMainLoop (app);
   }

   /* file_cb() -- "File" menu item was selected so popup a
    * FileSelectionDialog.
    */
   void
   file_cb(widget, client_data, call_data)
   Widget widget;
   XtPointer client_data;
   XtPointer call_data;
   {
       static Widget dialog;
       Widget text_w;
       extern void read_file();
       int item_no = (int) client_data;

       if (item_no == 1)
           exit (0);  /* user chose Exit */

       if (!dialog) {
           Widget menu = XtParent (widget);
           dialog = XmCreateFileSelectionDialog (menu, "file_sb", NULL, 0);

           /* Get the text widget handle stored as "user data" in File menu */
           XtVaGetValues (menu, XmNuserData, &text_w, NULL);
           XtAddCallback (dialog, XmNokCallback, read_file, text_w);
           XtAddCallback (dialog, XmNcancelCallback, XtUnmanageChild, NULL);
       }
       XtManageChild (dialog);

       XtPopup (XtParent (dialog), XtGrabNone);
       XMapRaised (XtDisplay (dialog), XtWindow (XtParent (dialog)));
   }

   /* read_file() -- callback routine when the user selects OK in the
    * FileSelection Dialog or presses Return in the single-line text widget.
    * The specified file must be a regular file and readable.
    * If so, it's contents are displayed in the text_w provided as the
    * client_data to this function.
    */
   void
   read_file(widget, client_data, call_data)
   Widget widget;  /* file selection box or text field widget */
   XtPointer client_data;
   XtPointer call_data;
   {
       char *filename, *text;
       struct stat statb;
       FILE *fp;
       Widget file_w;
       Widget text_w = (Widget) client_data;
       XmFileSelectionBoxCallbackStruct *cbs =
           (XmFileSelectionBoxCallbackStruct *) call_data;

       if (XtIsSubclass (widget, xmTextFieldWidgetClass)) {
           filename = XmTextFieldGetString (widget);
           file_w = widget; /* this *is* the file_w */
       }
       else {
           /* file was selected from FileSelectionDialog */
           XmStringGetLtoR (cbs->value, XmFONTLIST_DEFAULT_TAG, &filename);
           /* the user data stored the file_w widget in the text_w */
           XtVaGetValues (text_w, XmNuserData, &file_w, NULL);
       }

       if (!filename || !*filename) { /* nothing typed? */
           if (filename)
               XtFree (filename);
           return;
       }

       /* make sure the file is a regular text file and open it */
       if (stat (filename, &statb) == -1 ||
               (statb.st_mode & S_IFMT) != S_IFREG ||
               !(fp = fopen(filename, "r"))) {
           if ((statb.st_mode & S_IFMT) == S_IFREG)
               perror (filename); /* send to stderr why we can't read it */
           else
               fprintf (stderr, "%s: not a regular file0, filename);
           XtFree (filename);
           return;
       }

       /* put the contents of the file in the Text widget by allocating
        * enough space for the entire file, reading the file into the
        * allocated space, and using XmTextFieldSetString() to show the file.
        */
       if (!(text = XtMalloc ((unsigned)(statb.st_size + 1)))) {
           fprintf (stderr, "Can't alloc enough space for %s", filename);
           XtFree (filename);
           fclose (fp);
           return;
       }

       if (!fread (text, sizeof (char), statb.st_size + 1, fp))
           fprintf (stderr, "Warning: may not have read entire file!0);

       text[statb.st_size] = 0; /* be sure to NULL-terminate */

       /* insert file contents in Text widget */
       XmTextSetString (text_w, text);

       /* make sure text field is up to date */
       if (file_w != widget) {
           /* only necessary if activated from FileSelectionDialog */
           XmTextFieldSetString (file_w, filename);
           XmTextFieldSetCursorPosition (file_w, strlen(filename));
       }

       /* free all allocated space and */
       XtFree (text);
       XtFree (filename);
       fclose (fp);
   }
The output of the program is shown in the figure.

figs.eps/V6a.14.03.eps.png
Output of file_browser.c

We use the convenience routine XmCreateScrolledText() to create a ScrolledText area. We specify that the Text widget displays 12 lines by 70 columns of text by setting the XmNrows and XmNcolumns resources. These settings are used only at initialization. Once the application is up and running, the user can resize the window and effectively change those dimensions.

The XmNeditable resource is set to False to prevent the user from editing the contents of the Text widget. Since we do not provide a way to write changes back to the file, we don't want to mislead the user into thinking that the file is editable. Since a noneditable Text widget should not display an insertion cursor, we remove it by setting XmNcursorPositionVisible to False.

The FileSelectionDialog is created and managed when the user selects the Open button from the File menu. The user can exit the program by selecting the Exit button from this menu. The read_file() routine is activated when the user presses the OK button in the FileSelectionDialog or enters RETURN in the TextField widget. This function gets the specified file and checks its type. If the file chosen is not a regular file (e.g., if it is a directory, device, tty, etc.) or if it cannot be opened, an error is reported and the function simply returns. If you are unfamiliar with the use of the stat() system call, or any other aspect of UNIX programming used in examples in this book, a good source of information is the Nutshell Handbook Using C on the UNIX System , by Dave Curry (O'Reilly & Associates, 1988).

Assuming that the file checks out, its contents are placed in the Text widget. Rather than loading the file by reading each line using a function like fgets(), we allocate enough space to contain the entire file and read it all in with one call to fread(). The text is then loaded into the Text widget using XmTextSetString(). The ScrollBars are updated automatically and the text is positioned so that the beginning of the file is displayed. In file_browser.c, the ScrolledText object has two ScrollBars that are installed automatically. The vertical ScrollBar is needed in case the text exceeds 12 lines; the horizontal ScrollBar is needed in case any of those lines are wider than 70 columns. Most users are accustomed to having Text windows be a fixed width (typically 80 columns), especially if they have ever used an ASCII terminal. However, it can be annoying to have text that is scrollable in the horizontal direction, since you need to see the entire line to read smoothly through a page of text.

The XmNscrollHorizontal resource controls whether or not a horizontal ScrollBar is displayed. If the resource is set to False, the ScrollBar is not displayed, but that does not stop text from being displayed beyond the visible area. In order to have text wrap appropriately, the XmNwordWrap resource must be set to True. When this resource is set, the Text widget breaks lines at spaces, tabs, and newlines. While line breaking is fine for previewing files and other output-only Text widgets, you should not enforce such a policy for Text widgets that are used for text editing, as the user may want to edit wide files.

The XmNscrollVertical resource controls whether or not a vertical ScrollBar is displayed. This resource defaults to True when a Text widget is created as a child of a ScrolledWindow. The XmNscrollLeftSide and XmNscrollTopSide resources take Boolean values that control the location of the ScrollBars within the ScrolledWindow. By default, XmNscrollTopSide is set to False, which causes the ScrollBar to be placed below the ScrolledWindow. The default value of XmNscrollLeftSide depends upon the value of XmNstringDirection. These two resources should not be set by the application, but left to users to specify themselves. The XmNresizeWidth and XmNresizeHeight resources control whether or not a Text widget should resize itself vertically or horizontally in order to display the entire text stream. Both of the resources default to False. If XmNresizeWidth is set to True and new text is added such that the number of columns needs to grow, the width of the widget grows to contain the new text. Similarly, if XmNresizeHeight is set to True and the number of lines increases, the height of the widget increases so that it can display all of the lines. These resources have no effect in a ScrolledText object, since the ScrollBars are managing the widget's size. Also, if line breaking is active, XmNresizeWidth has no effect.

In most cases, it is not appropriate to set these resources, as it is regarded as poor user-interface design to have a Text widget that dynamically resizes as the text is being edited. It is also impolite for a window to resize itself except as the result of an explicit user action. One example of an acceptable use of these resources involves using a Text widget to display text for a help dialog. In this situation, the Text widget can resize itself silently before it is mapped to the screen, so that by the time it is visible, its size is constant.

15.2.4 Text Positions

A position in a Text widget specifies the number of characters from the beginning of the text in the widget, where the first character position is defined as zero (0). All whitespace and newline characters are considered part of the text and are counted as single characters. For example, in the figure, the insertion cursor in the TextField widget is at position 14. When the user types in a Text widget, the new text is always added at the position of the insertion cursor and the insertion cursor is advanced. If the user does not move the cursor, it is always positioned at the end of the text in the widget.

You can set the position of the insertion cursor explicitly using XmTextSetInsertionPosition(), which takes the following form:

   void
   XmTextSetInsertionPosition(text_w, position)
       Widget         text_w;
       XmTextPosition position;
This function is identical to XmTextSetCursorPosition(). The XmTextPosition type is a long value, so it can represent all of the positions in a Text widget. You can get the current cursor position using XmTextGetInsertionPosition() or XmTextGetCursorPosition(). As with most of the Text widget functions, there are corresponding TextField functions for setting and getting the position of the insertion cursor. The TextField routines only work with TextField widgets, while the Text routines work with both Text and TextField widgets.

the source code shows an application that uses these routines as part of a search operation. The program searches the Text widget for a specified pattern and then positions the insertion cursor so that the pattern is displayed. XtSetLanguageProc() is only available in X11R5; there is no corresponding function in X11R4.

   /* search_text.c -- demonstrate how to position a cursor at a
    * particular location.  The position is determined by a pattern
    * match search.
    */
   #include <Xm/Text.h>
   #include <Xm/TextF.h>
   #include <Xm/LabelG.h>
   #include <Xm/RowColumn.h>
   #include <X11/Xos.h>   /* for the index() function */

   Widget text_w, search_w, text_output;

   main(argc, argv)
   int argc;
   char *argv[];
   {
       Widget        toplevel, rowcol_v, rowcol_h;
       XtAppContext  app;
       int           i, n;
       void          search_text();
       Arg           args[10];

       XtSetLanguageProc (NULL, NULL, NULL);

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

       rowcol_v = XtVaCreateWidget ("rowcol_v",
           xmRowColumnWidgetClass, toplevel, NULL);

       rowcol_h = XtVaCreateWidget ("rowcol_h",
           xmRowColumnWidgetClass, rowcol_v,
           XmNorientation,  XmHORIZONTAL,
           NULL);
       XtVaCreateManagedWidget ("Search Pattern:",
           xmLabelGadgetClass, rowcol_h, NULL);
       search_w = XtVaCreateManagedWidget ("search_text",
           xmTextFieldWidgetClass, rowcol_h, NULL);
       XtManageChild (rowcol_h);

       text_output = XtVaCreateManagedWidget ("text_output",
           xmTextWidgetClass, rowcol_v,
           XmNeditable,              False,
           XmNcursorPositionVisible, False,
           XmNshadowThickness,       0,
           XmNhighlightThickness,    0,
           NULL);

       n = 0;
       XtSetArg (args[n], XmNrows,      10); n++;
       XtSetArg (args[n], XmNcolumns,   80); n++;
       XtSetArg (args[n], XmNeditMode,  XmMULTI_LINE_EDIT); n++;
       XtSetArg (args[n], XmNscrollHorizontal,  False); n++;
       XtSetArg (args[n], XmNwordWrap,  True); n++;
       text_w = XmCreateScrolledText (rowcol_v, "text_w", args, n);
       XtManageChild (text_w);

       XtAddCallback (search_w, XmNactivateCallback, search_text, NULL);

       XtManageChild (rowcol_v);

       XtRealizeWidget (toplevel);
       XtAppMainLoop (app);
   }

   /* search_text() -- called when the user activates the TextField. */
   void
   search_text(widget, client_data, call_data)
   Widget widget;
   XtPointer client_data;
   XtPointer call_data;
   {
       char *search_pat, *p, *string, buf[32];
       XmTextPosition pos;
       int len;
       Boolean found = False;

       /* get the text that is about to be searched */
       if (!(string = XmTextGetString (text_w)) || !*string) {
           XmTextSetString (text_output, "No text to search.");
           XtFree (string); /* may have been ""; free it */
           return;
       }
       /* get the pattern we're going to search for in the text. */
       if (!(search_pat = XmTextGetString (search_w)) || !*search_pat) {
           XmTextSetString (text_output, "Specify a search pattern.");
           XtFree (string); /* this we know is a string; free it */
           XtFree (search_pat); /* this may be "", XtFree() checks.. */
           return;
       }
       len = strlen (search_pat);

       /* start searching at current cursor position + 1 to find
        * the -next- occurrance of string.  we may be sitting on it.
        */
       pos = XmTextGetCursorPosition (text_w);
       for (p = &string[pos+1]; p = index (p, *search_pat); p++)
           if (!strncmp (p, search_pat, len)) {
               found = True;
               break;
           }
       if (!found) { /* didn't find pattern? */
           /* search from beginning till we've passed "pos" */
           for (p = string;
                   (p = index (p, *search_pat)) && p - string <= pos; p++)
               if (!strncmp (p, search_pat, len)) {
                   found = True;
                   break;
               }
       }
       if (!found)
           XmTextSetString (text_output, "Pattern not found.");
       else {
           pos = (XmTextPosition)(p - string);
           sprintf (buf, "Pattern found at position %ld.", pos);
           XmTextSetString (text_output, buf);
           XmTextSetInsertionPosition (text_w, pos);
       }
       XtFree (string);
       XtFree (search_pat);
   }
In this example, the user can search for strings in a ScrolledText, as shown in the figure.

figs.eps/V6a.14.04.eps.png
Output of search_text.c

This program doesn't provide a way to load a file, so if you want to experiment, you need to type or paste some text into the widget. Once there is some text in the widget, type a string pattern in the Search Pattern TextField widget and press RETURN to activate the search. The text is searched starting at the position immediately following the current cursor position. If the search routine reaches the end of the text before it finds the pattern, it resumes searching from the beginning of the text and continues until it finds the pattern or reaches the cursor position. If the routine finds the pattern, it moves the insertion point to that location using XmTextSetInsertionPosition(). Otherwise, the routine prints an error message and does not move the cursor.

The search_text() routine shown in the source code searches the text using various string routines. In Motif 1.2, there is a new Text routine that provides the same functionality. XmTextFindString() searches a Text widget for a specified string. This routine takes the following form:

   Boolean
   XmTextFindString(text_w, start, string, direction, position)
       Widget          text_w;
       XmTextPosition  start;
       char           *string;
       XmTextDirection direction;
       XmTextPosition *position;
The start argument specifies the starting position for the search, while direction indicates whether the routine searches forward or backward in the text. This parameter can have the value XmTEXT_FORWARD or XmTEXT_BACKWARD. The routine returns True if it finds the string, and in this case, the position parameter returns the position where the string starts in the text. If the string is not found, the routine returns False, and the value of position is undefined. It is easy to rewrite search_text() to take advantage of XmTextFindString(). In Section #stexteditor, we implement a full text editor and use XmTextFindString() to handle the various search operations.

The text_output widget in search_text.c is also a Text widget, even though it looks more like a Label widget. By setting XmNshadowThickness to 0 and XmNeditable to False, we create the Text widget that doesn't look like a normal Text widget, and the user cannot edit the text. We demonstrate this technique not to advocate such usage, but to point out the versatility of this widget class.

If you paste a large amount of text into the main Text widget and search repeatedly for a common pattern, you should notice that the Text widget may scroll automatically to make the specified text visible. This action is controlled by the XmNautoShowCursorPosition resource. This resource has a default value of True, which means that the Text widget adjusts the visible text to make sure that the cursor is always visible. When the resource is set to False, the widget does not scroll to compensate for the cursor's invisibility. This resource also works in single-line Text widgets and TextField widgets; these widgets may scroll their displays horizontally to display the insertion cursor.

It is easy to scroll a Text widget to a particular position in the text stream by setting the cursor position and then calling XmTextShowPosition(). This routine takes the following form:

   void
   XmTextShowPosition(text_w, position)
       Widget         text_w;
       XmTextPosition position;
To scroll to the end of the text, you need to scroll to the last position, which can be retrieved using XmTextGetLastPosition() . It is also possible to perform relative scrolling using the function XmTextScroll(), which takes the following form:
   void
   XmTextScroll(text_w, lines)
       Widget text_w;
       int    lines;
A positive value for lines causes a Text widget to scroll upward by that many lines, while a negative value causes downward scrolling. The Text widget does not have to be a child of ScrolledWindow for this routine to work; the widget simply adjusts the viewable text.

Now that we have a routine that searches for text, the next logical step is to implement a function that performs a search-and-replace operation. Motif makes this task fairly easy by providing the XmTextReplace() routine, which takes the following form:

   void
   XmTextReplace(text_w, from_pos, to_pos, value)
       Widget          text_w;
       XmTextPosition  from_pos;
       XmTextPosition  to_pos;
       char           *value;
This function identifies the text to be replaced in the Text widget starting at the position from_pos and ending at, but not including, the position to_pos. This text is replaced by the text in value. If value is NULL or an empty string, the text between the two positions is simply deleted. If you want to remove all of the text from the widget, call XmTextSetString() with a NULL string as the text value.

To add search-and-replace functionality to the program in the source code we need to add a new TextField widget that prompts for the replacement text and provide a callback routine for the widget. the source code shows the additional code that is necessary.

   Widget text_w, search_w, replace_w, text_output;

   main(argc, argv)
   int argc;
   char *argv[];
   {
       ...
       replace_w = XtVaCreateManagedWidget ("replace_text",
           xmTextFieldWidgetClass, rowcol_h, NULL);

       XtAddCallback (replace_w, XmNactivateCallback, search_and_replace, NULL);
       ...
   }

   void
   search_and_replace(widget, client_data, call_data)
   Widget widget;
   XtPointer client_data;
   XtPointer call_data;
   {
       char *search_pat, *p, *string, *new_pat, buf[32];
       XmTextPosition pos;
       int search_len, pattern_len;
       int nfound = 0;

       string = XmTextGetString (text_w);
       if (!*string) {
           XmTextSetString (text_output, "No text to search.");
           XtFree (string);
           return;
       }

       search_pat = XmTextGetString (search_w);
       if (!*search_pat) {
           XmTextSetString (text_output, "Specify a search pattern.");
           XtFree (string);
           XtFree (search_pat);
           return;
       }

       new_pat = XmTextGetString (replace_w);
       search_len = strlen (search_pat);
       pattern_len = strlen (new_pat);
       /* start at beginning and search entire Text widget */
       for (p = string; p = index (p, *search_pat); p++)
           if (!strncmp (p, search_pat, search_len)) {
               nfound++;
               /* get the position where pattern was found */
               pos = (XmTextPosition)(p-string);
               /* replace the text from our position + strlen (new_pat) */
               XmTextReplace (text_w, pos, pos + search_len, new_pat);
               /* "string" has changed -- we must get the new version */
         XtFree (string); /* free the one we had first... */
         string = XmTextGetString (text_w);
               /* continue search for next pattern -after- replacement */
               p = &string[pos + pattern_len];
           }
       if (!nfound)
           strcpy (buf, "Pattern not found.");
       else
           sprintf (buf, "Made %d replacements.", nfound);
       XmTextSetString (text_output, buf);
       XtFree (string);
       XtFree (search_pat);
       XtFree (new_pat);
   }
In this routine, the pattern search starts at the beginning of the text and searches all of the text in the widget. We are not interested in the cursor position and do not attempt to move it. The main loop of the function only needs to find the specified pattern and replace each occurrence with the new text. After each call to XmTextReplace() , we reread the text, since the old value is no longer valid. As with the search_text() routine, we could easily use XmTextFindString() to search for the pattern, as we do in the text editor in Section #stexteditor.

15.2.5 Output-only Text

The Text and TextField widgets can be used in an output-only mode by setting the XmN­editable resource to False. If the user tries to edit the text in a read-only widget, the widget beeps and does not allow the modification. We used an output-only Text widget in our file browsing application.

Our next example addresses a common need for many developers: a method for displaying text messages while an application is running. These messages may include status messages about application actions, as well as error messages from Xlib, Xt, and functions internal to the application. The message area is an important part of the main window of many applications, as discussed in Chapter 4, The Main Window. While a message area can be ­implemented using a Label widget, an output-only ScrolledText object is better suited for use as a message area because the user can scroll back to previous messages.

the source code shows the wprint() function that we wrote to handle displaying messages. The function acts like printf() in that it takes variable arguments and understands the standard string formatting characters. The output goes to a ScrolledText widget so the user can review previous messages. All new text is appended to the end of the output, so it is immediately visible and the user does not have to manually scroll to the end of the display.

   #include <stdio.h>
   #include <varargs.h> /* or <stdarg.h> */

   /* global variable */
   Widget text_output;

   main(argc, argv)
   int argc;
   char *argv[];
   {
       Arg          args[10];
       int          n;

       ...

       /* Create output_text as a ScrolledText window */
       n = 0;
       XtSetArg(args[n], XmNrows,             6); n++;
       XtSetArg(args[n], XmNcolumns,          80); n++;
       XtSetArg(args[n], XmNeditable,         False); n++;
       XtSetArg(args[n], XmNeditMode,         XmMULTI_LINE_EDIT); n++;
       XtSetArg(args[n], XmNwordWrap,         True); n++;
       XtSetArg(args[n], XmNscrollHorizontal, False); n++;
       XtSetArg(args[n], XmNcursorPositionVisible, False); n++;
       text_output = XmCreateScrolledText(rowcol, "text_output", args, n);
       XtManageChild (text_output);

       ...
   }

   /*VARARGS*/
   void
   wprint(va_alist)
   va_dcl
   {
       char msgbuf[256];
       char *fmt;
       static XmTextPosition wpr_position;
       va_list args;

       va_start (args);
       fmt = va_arg (args, char *);
   #ifndef NO_VPRINTF
       (void) vsprintf (msgbuf, fmt, args);
   #else /* !NO_VPRINTF */
       {
           FILE foo;
           foo._cnt = 256;
           foo._base = foo._ptr = msgbuf; /* (unsigned char *) ?? */
           foo._flag = _IOWRT+_IOSTRG;
           (void) _doprnt (fmt, args, &foo);
           *foo._ptr = ' '; /* plant terminating null character */
       }
   #endif /* NO_VPRINTF */
       va_end (args);

       XmTextInsert (text_output, wpr_position, msgbuf);
       wpr_position = wpr_position + strlen (msgbuf);
       XtVaSetValues (text_output, XmNcursorPosition, wpr_position, NULL);
       XmTextShowPosition (text_output, wpr_position);
   }
Since the wprint() function acts like printf(), it takes a variable-length argument list, which requires the inclusion of either <varargs.h> or <stdarg.h>. If you have access to the source code for X, you could include <X11/VarargsI.h> instead. This file is used by the X Toolkit whenever variable-length argument lists are used; it includes the appropriate file for the current operating system. The function wprint() takes va_alist as its only parameter. This argument is a pointer to the first of a list of arguments passed to the function; it is declared as va_dcl in accordance with the standards for functions that take variable-length argument lists.

The va_start() and va_arg() macros are used to extract the first parameter from the argument list. Since wprint() is supposed to act like printf(), we know that the first parameter is going to be a char pointer. The call to va_arg() causes fmt to point to the format string, which may or may not contain % formatting characters that expand to other strings depending on the other arguments to the function.

The rest of the arguments are read and parsed by either vsprintf() or _doprnt(), depending on the C library that you are using. vsprintf() is a varargs version of sprintf() that exists on most modern UNIX machines. System V has vsprintf(), as does SunOS, but Ultrix and older BSD machines typically use _doprnt(). If your machine does not have vsprintf(), you can use _doprnt() as shown in the source code Both of these functions consume all of the arguments in the list and leave the result in msgbuf.

Now that we have the complete string in msgbuf , we can append it to the existing text in the Text widget. We keep track of the end of text_output with wpr_position. Each time msgbuf is concatenated to the end of the text, the value of wpr_position is incremented appropriately. The new text is added using the convenience routine XmTextInsert(), which takes the following form:

   void
   XmTextInsert(text_w, position, string)
       Widget          text_w;
       XmTextPosition  position;
       char           *string;
The function simply inserts the given text at the specified position. Finally, we call XmTextShowPosition() to make the end position visible within the Text widget. This routine may cause the Text widget to adjust its text so that the new text is visible, as a convenience to the user so that he does not have to scroll the window to view new messages.

The routines in the source code show how wprint() can be used to reset the error handling functions for Xlib and Xt so that the messages are printed in a Text widget rather than to stderr.

   extern void wprint();

   static void
   x_error(dpy, err_event)
   Display      *dpy;
   XErrorEvent  *err_event;
   {
       char                buf[256];

       XGetErrorText (dpy, err_event->error_code, buf, (sizeof buf));

       wprint("X Error: <%s>0, buf);
   }

   static void
   xt_error(message)
   char *message;
   {
       wprint ("Xt Error: %s0, message);
   }

   main(argc, argv)
   int argc;
   char *argv[];
   {
       XtAppContext app;

       ...

       /* catch Xt errors */
       XtAppSetErrorHandler (app, xt_error);
       XtAppSetWarningHandler (app, xt_error);

       /* and Xlib errors */
       XSetErrorHandler (x_error);

       ...
   }
Using XtAppSetErrorHandler(), XtAppSetWarningHandler() , and XSetErrorHandler(), we send all X-related error messages to a Text widget through wprint(). You can also use wprint() to send any application-specific messages to the ScrolledText area.

15.3 Text Clipboard Functions

Both the Text widget and the TextField widget have convenience routines that support communication with the clipboard. Using these functions, you can implement the standard cut, copy, and paste functionality, as well as support communication with other windows or applications on the desktop. If you are not familiar with the clipboard and how it works, see Chapter 17, The Clipboard. Briefly, the clipboard is one of three transient locations where arbitrary data such as text can be stored so that other windows or applications can copy the data. For the Text widget, we are only interested in copying textual data and providing visual feedback within the widget. The Text widget can send and receive data from all three of the locations, depending on the interface style that you are using.

As described earlier in this chapter, the user typically selects text by pressing the first mouse button and dragging the pointer across the text. When text is selected, it is rendered in reverse video and automatically copied into the primary selection. Now the user can paste text from the primary selection into any Text widget on the desktop by pressing the middle mouse button. The insertion cursor is moved to the location of the button press, and the data is automatically copied into the Text widget at this position. This functionality works by default within the Text widget. However, the actions operate on the primary selection, not the clipboard selection. Furthermore, the actions only allow you to copy data to and from the selection, not cut it or clear it.

To provide these features, most applications provide other user-interface controls, such as a PulldownMenu and appropriate menu items, that call Text widget clipboard routines. These routines store text on the clipboard. They also allow the user to move text between the clipboard and the primary selection, as well as between windows that are interested only in the clipboard selection. Typical menu entries include Cut, Copy, Paste, and Clear. the source code demonstrates these common editing actions. The application creates a MenuBar with an Edit PulldownMenu that contains actions that operate on the Text widget. 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.

   /* cut_paste.c -- demonstrate the text functions that handle
    * clipboard operations.  These functions are convenience routines
    * that relieve the programmer of the need to use clipboard functions.
    * The functionality of these routines already exists in the Text
    * widget, yet it is common to place such features in the interface
    * via the MenuBar's "Edit" pulldown menu.
    */
   #include <Xm/Text.h>
   #include <Xm/LabelG.h>
   #include <Xm/PushBG.h>
   #include <Xm/RowColumn.h>
   #include <Xm/MainW.h>

   Widget text_w, text_output;

   main(argc, argv)
   int argc;
   char *argv[];
   {
       Widget        toplevel, main_w, menubar, rowcol_v;
       XtAppContext  app;
       void          cut_paste();
       XmString      label, cut, clear, copy, paste;
       Arg           args[10];
       int           n;

       XtSetLanguageProc (NULL, NULL, NULL);

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

       main_w = XtVaCreateWidget ("main_w",
           xmMainWindowWidgetClass, toplevel, NULL);

       /* Create a simple MenuBar that contains a single menu */
       label = XmStringCreateLocalized ("Edit");
       menubar = XmVaCreateSimpleMenuBar (main_w, "menubar",
           XmVaCASCADEBUTTON, label, 'E',
           NULL);
       XmStringFree (label);

       cut = XmStringCreateLocalized ("Cut");      /* create a simple    */
       copy = XmStringCreateLocalized ("Copy");    /* pulldown menu that */
       clear = XmStringCreateLocalized ("Clear");  /* has these menu     */
       paste = XmStringCreateLocalized ("Paste");  /* items in it.       */
       XmVaCreateSimplePulldownMenu (menubar, "edit_menu", 0, cut_paste,
           XmVaPUSHBUTTON, cut, 't', NULL, NULL,
           XmVaPUSHBUTTON, copy, 'C', NULL, NULL,
           XmVaPUSHBUTTON, paste, 'P', NULL, NULL,
           XmVaSEPARATOR,
           XmVaPUSHBUTTON, clear, 'l', NULL, NULL,
           NULL);
       XmStringFree (cut);
       XmStringFree (clear);
       XmStringFree (copy);
       XmStringFree (paste);

       XtManageChild (menubar);

       /* create a standard vertical RowColumn... */
       rowcol_v = XtVaCreateWidget ("rowcol_v",
           xmRowColumnWidgetClass, main_w, NULL);

       text_output = XtVaCreateManagedWidget ("text_output",
           xmTextWidgetClass, rowcol_v,
           XmNeditable,              False,
           XmNcursorPositionVisible, False,
           XmNshadowThickness,       0,
           XmNhighlightThickness,    0,
           NULL);

       n = 0;
       XtSetArg (args[n], XmNrows,      10); n++;
       XtSetArg (args[n], XmNcolumns,   80); n++;
       XtSetArg (args[n], XmNeditMode,  XmMULTI_LINE_EDIT); n++;
       XtSetArg (args[n], XmNscrollHorizontal,  False); n++;
       XtSetArg (args[n], XmNwordWrap,  True); n++;
       text_w = XmCreateScrolledText (rowcol_v, "text_w", args, n);
       XtManageChild (text_w);

       XtManageChild (rowcol_v);
       XtManageChild (main_w);

       XtRealizeWidget (toplevel);
       XtAppMainLoop (app);
   }

   /* cut_paste() -- the callback routine for the items in the edit menu */
   void
   cut_paste(widget, client_data, call_data)
   Widget widget;
   XtPointer client_data;
   XtPointer call_data;
   {
       Boolean result = True;
       int reason = (int) client_data;
       XEvent *event = ((XmPushButtonCallbackStruct *) call_data)->event;
       Time when;

       XmTextSetString (text_output, NULL);   /* clear message area */

       if (event != NULL) {
           switch (event->type) {
               case ButtonRelease :
                   when = event->xbutton.time;
                   break;
               case KeyRelease :
                   when = event->xkey.time;
                   break;
               default:
                   when = CurrentTime;
                   break;
           }
       }

       switch (reason) {
           case 0 :
               result = XmTextCut (text_w, when);
               break;
           case 1 :
               result = XmTextCopy (text_w, when);
               break;
           case 2 :
               result = XmTextPaste (text_w);
           case 3 :
               XmTextClearSelection (text_w, when);
               break;
       }
       if (result == False)
           XmTextSetString (text_output, "There is no selection.");
       else
           XmTextSetString (text_output, NULL);
   }

The application creates a MainWindow widget, so that it can contain the MenuBar. The MenuBar and the PulldownMenu are created using their respective convenience routines, as described in Chapter 4, The Main Window, and Chapter 15, Menus. The output of the program is shown in the figure.

figs.eps/V6a.14.05.eps.png
Output of cut_paste.c

Again, you need to enter some text or paste it from another window if you want to experiment with this application. The main window contains the same Text widgets used in previous examples. The Edit PulldownMmenu allows the user to interact with the clipboard. The cut_paste() routine is the callback function for all of the menu items in the Edit menu. This function uses four Text convenience routines to work with the clipboard: XmTextCut(), XmTextCopy(), XmTextPaste(), and XmTextClearSelection(). These routines take the following form:

   Boolean
   XmTextCut(text_w, time)
       Widget  text_w;
       Time    time;
   Boolean
   XmTextCopy(text_w, time)
       Widget text_w;
       Time   time;
   Boolean
   XmTextPaste(text_w)
       Widget text_w;
   void
   XmTextClearSelection(text_w, time)
       Widget text_w;
       Time   time;

XmTextCopy() copies the text that is selected in the Text widget and places it on the clipboard. XmTextCut() is similar to XmTextCopy(), except that the Text widget that owns the selection is instructed to delete the text once it has been copied to the clipboard. The deletion is handled by sending a DELETE protocol request to the window holding the selection. This protocol is not the same as the WM_DELETE protocol, which indicates that a window is being deleted. See Chapter 16, Interacting With the Window Manager, for more information on window manager protocols. The time parameters should not be set to CurrentTime to avoid race conditions with other clipboard operations that may be occurring at the same time. Since the clipboard routines are called by menu item callback routines, you can use the time field of the XEvent that is passed in the callback structure, as we do in the source code Both XmTextCopy() and XmTextCut() return True if the operation succeeds. False may be returned if there is no selected text or an error occurs in attempting to communicate with the clipboard.

XmTextPaste() gets the current selection from the clipboard and inserts it at the location of the insertion cursor. If there is some selected text in the Text widget, that text is replaced by the selection from the clipboard. XmTextPaste() returns True if there is a selection on the clipboard that can be retrieved.

XmTextClearSelection() deselects the text selection in the Text widget. If there is no selected text, nothing happens. The routine does not provide any feedback or return any value. Any text that is held on the clipboard or in a selection property remains.

One additional convenience routine that operates on the selection is XmTextRemove(). This function is like XmTextCut(), in that it removes the selected text from a Text widget, but it does not place the text on the clipboard.

15.3.1 Getting the Selection

You can get the selected text from a Text widget using XmTextGetSelection(), which takes the following form:

   char *
   XmTextGetSelection(text_w)
       Widget text_w;
This routine returns allocated data that contains the selected text. This text must be freed using XtFree() when you are through using it. The routine returns NULL if there is no text selected in the Text widget.

XmTextGetSelectionPosition() provides information about the selected text in a Text widget. This routine takes the following form:

   Boolean
   XmTextGetSelectionPosition(text_w, left, right)
       Widget          text_w;
       XmTextPosition *left;
       XmTextPosition *right;
If XmTextGetSelectionPosition() returns True, the values for left and right specify the boundaries of the selected text. If the routine returns False, the widget does not contain any selected text, and the values for left and right are undefined.

15.3.2 Modifying the Selection Mechanisms

The Text widget supports multi-clicking techniques for selecting increasingly large chunks of text. The default multi-clicking actions in the Text widget are shown in tab(@), linesize(2); l | l l | l. User Action@Text Widget Action
_
Single click@Resets insertion cursor to position Double click@Selects a word (bounded by whitespace) Triple click@Selects a line (bounded by newlines) Quadruple click@Selects all of the text
_ These default actions can be modified using the XmNselectionArray and XmN­selectionArrayCount resources. The XmNselectionArray resource specifies an array of XmTextScanType values, where XmTextScanType is an enumerated type defined as follows:

   typedef enum {
       XmSELECT_POSITION,
       XmSELECT_WHITESPACE,
       XmSELECT_WORD,
       XmSELECT_LINE,
       XmSELECT_PARAGRAPH
       XmSELECT_ALL,
   } XmTextScanType;
XmSELECT_WHITESPACE works in the same way as XmSELECT_WORD . Each successive button click in a Text widget selects the text according to the corresponding item in the array. The default array is defined as follows:
   static XmTextScanType sarray[] = {
       XmSELECT_POSITION, XmSELECT_WORD, XmSELECT_LINE, XmSELECT_ALL
   };
You should keep the items in the array in ascending order, so as not to confuse the user. The following code fragment shows an acceptable change to the array:
   static XmTextScanType sarray[] = {
       XmSELECT_POSITION, XmSELECT_WORD, XmSELECT_LINE, XmSELECT_PARAGRAPH,
       XmSELECT_ALL
   };
   ...
   XtVaSetValues (text_w,
       XmNselectionArray,      selectionArray,
       XmNselectionArrayCount, 5,
       NULL);

The maximum time interval between button clicks in a multi-click action is specified by the multiClickTime resource. This resource is maintained by the X server and set for all applications; it is not a Motif resource. The value of the resource can be retrieved using XtGetMultiClickTime() and changed with XtSetMultiClickTime(). For more discussion on this value, see Chapter 11, Labels and Buttons.

The XmNselectThreshold resource can be used to modify the behavior of click-and-drag actions. This resource specifies the number of pixels that the user must move the pointer before a character can be selected. The default value is 5, which means that the user must move the mouse at least 5 pixels before the Text widget decides whether or not to select a character. This threshold is used throughout a selection operation to determine when characters are added or deleted from the selection. If you are using an extremely large font, you may want to increase the value of this resource to cut down on the number of calculations that are necessary to determine if a character should be added or deleted from the selection.

15.4 A Text Editor


Before we describe the Text widget callback routines, we are going to present an example that combines all the information covered so far. The example is a full-featured text editor built from the examples presented so far in this chapter. You should recognize most of the code in the example; the code that you don't recognize should be understandable from the context in which it is used. The output of the program is shown in the figure; the code is shown in the source code 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. XmTextFindString() is only available in Motif 1.2; there is no corresponding function in Motif 1.1, so you have to implement your own search capabilities.

figs.eps/V6a.14.06.eps.png
Output of editor.c


   /* editor.c -- create a full-blown Motif editor application complete
    * with a menubar, facilities to read and write files, text search
    * and replace, clipboard support and so forth.
    */
   #include <Xm/Text.h>
   #include <Xm/TextF.h>
   #include <Xm/LabelG.h>
   #include <Xm/PushBG.h>
   #include <Xm/RowColumn.h>
   #include <Xm/MainW.h>
   #include <Xm/Form.h>
   #include <Xm/FileSB.h>
   #include <X11/Xos.h>
   #include <stdio.h>
   #include <sys/types.h>
   #include <sys/stat.h>

   Widget text_edit, search_text, replace_text, text_output;

   #define FILE_OPEN 0
   #define FILE_SAVE 1
   #define FILE_EXIT 2

   #define EDIT_CUT 0
   #define EDIT_COPY 1
   #define EDIT_PASTE 2
   #define EDIT_CLEAR 3

   #define SEARCH_FIND_NEXT 0
   #define SEARCH_SHOW_ALL 1
   #define SEARCH_REPLACE 2
   #define SEARCH_CLEAR 3

   main(argc, argv)
   int argc;
   char *argv[];
   {
       XtAppContext  app_context;
       Widget        toplevel, main_window, menubar, form, search_panel;
       void          file_cb(), edit_cb(), search_cb();
       Arg           args[10];
       int           n = 0;
       XmString      open, save, exit, exit_acc, file, edit, cut,
                     clear, copy, paste, search, next, find, replace;

       XtSetLanguageProc (NULL, NULL, NULL);

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

       XmRepTypeInstallTearOffModelConverter ();

       main_window = XtVaCreateWidget ("main_window",
           xmMainWindowWidgetClass, toplevel, NULL);

       /* Create a simple MenuBar that contains three menus */
       file = XmStringCreateLocalized ("File");
       edit = XmStringCreateLocalized ("Edit");
       search = XmStringCreateLocalized ("Search");
       menubar = XmVaCreateSimpleMenuBar (main_window, "menubar",
           XmVaCASCADEBUTTON, file, 'F',
           XmVaCASCADEBUTTON, edit, 'E',
           XmVaCASCADEBUTTON, search, 'S',
           NULL);
       XmStringFree (file);
       XmStringFree (edit);
       XmStringFree (search);

       /* First menu is the File menu -- callback is file_cb() */
       open = XmStringCreateLocalized ("Open...");
       save = XmStringCreateLocalized ("Save...");
       exit = XmStringCreateLocalized ("Exit");
       exit_acc = XmStringCreateLocalized ("Ctrl+C");
       XmVaCreateSimplePulldownMenu (menubar, "file_menu", 0, file_cb,
           XmVaPUSHBUTTON, open, 'O', NULL, NULL,
           XmVaPUSHBUTTON, save, 'S', NULL, NULL,
           XmVaSEPARATOR,
           XmVaPUSHBUTTON, exit, 'x', "Ctrl<Key>c", exit_acc,
           NULL);
       XmStringFree (open);
       XmStringFree (save);
       XmStringFree (exit);
       XmStringFree (exit_acc);

       /* ...create the "Edit" menu --  callback is edit_cb() */
       cut = XmStringCreateLocalized ("Cut");
       copy = XmStringCreateLocalized ("Copy");
       clear = XmStringCreateLocalized ("Clear");
       paste = XmStringCreateLocalized ("Paste");
       XmVaCreateSimplePulldownMenu (menubar, "edit_menu", 1, edit_cb,
           XmVaPUSHBUTTON, cut, 't', NULL, NULL,
           XmVaPUSHBUTTON, copy, 'C', NULL, NULL,
           XmVaPUSHBUTTON, paste, 'P', NULL, NULL,
           XmVaSEPARATOR,
           XmVaPUSHBUTTON, clear, 'l', NULL, NULL,
           NULL);
       XmStringFree (cut);
       XmStringFree (copy);
       XmStringFree (paste);

       /* create the "Search" menu -- callback is search_cb() */
       next = XmStringCreateLocalized ("Find Next");
       find = XmStringCreateLocalized ("Show All");
       replace = XmStringCreateLocalized ("Replace Text");
       XmVaCreateSimplePulldownMenu (menubar, "search_menu", 2, search_cb,
           XmVaPUSHBUTTON, next, 'N', NULL, NULL,
           XmVaPUSHBUTTON, find, 'A', NULL, NULL,
           XmVaPUSHBUTTON, replace, 'R', NULL, NULL,
           XmVaSEPARATOR,
           XmVaPUSHBUTTON, clear, 'C', NULL, NULL,
           NULL);
       XmStringFree (next);
       XmStringFree (find);
       XmStringFree (replace);
       XmStringFree (clear);

       XtManageChild (menubar);

       /* create a form work are */
       form = XtVaCreateWidget ("form",
           xmFormWidgetClass, main_window, NULL);

       /* create horizontal RowColumn inside the form */
       search_panel = XtVaCreateWidget ("search_panel",
           xmRowColumnWidgetClass, form,
           XmNorientation,     XmHORIZONTAL,
           XmNpacking,         XmPACK_TIGHT,
           XmNtopAttachment,   XmATTACH_FORM,
           XmNleftAttachment,  XmATTACH_FORM,
           XmNrightAttachment, XmATTACH_FORM,
           NULL);
       /* Create two TextField widgets with Labels... */
       XtVaCreateManagedWidget ("Search Pattern:",
           xmLabelGadgetClass, search_panel, NULL);
       search_text = XtVaCreateManagedWidget ("search_text",
           xmTextFieldWidgetClass, search_panel, NULL);
       XtVaCreateManagedWidget ("     Replace Pattern:",
           xmLabelGadgetClass, search_panel, NULL);
       replace_text = XtVaCreateManagedWidget ("replace_text",
           xmTextFieldWidgetClass, search_panel, NULL);
       XtManageChild (search_panel);

       text_output = XtVaCreateManagedWidget ("text_output",
           xmTextFieldWidgetClass, form,
           XmNeditable,              False,
           XmNcursorPositionVisible, False,
           XmNshadowThickness,       0,
           XmNleftAttachment,        XmATTACH_FORM,
           XmNrightAttachment,       XmATTACH_FORM,
           XmNbottomAttachment,      XmATTACH_FORM,
           NULL);

       n = 0;
       XtSetArg (args[n], XmNrows,             10); n++;
       XtSetArg (args[n], XmNcolumns,          80); n++;
       XtSetArg (args[n], XmNeditMode,         XmMULTI_LINE_EDIT); n++;
       XtSetArg (args[n], XmNtopAttachment,    XmATTACH_WIDGET); n++;
       XtSetArg (args[n], XmNtopWidget,        search_panel); n++;
       XtSetArg (args[n], XmNleftAttachment,   XmATTACH_FORM); n++;
       XtSetArg (args[n], XmNrightAttachment,  XmATTACH_FORM); n++;
       XtSetArg (args[n], XmNbottomAttachment, XmATTACH_WIDGET); n++;
       XtSetArg (args[n], XmNbottomWidget,     text_output); n++;
       text_edit = XmCreateScrolledText (form, "text_edit", args, n);
       XtManageChild (text_edit);

       XtManageChild (form);
       XtManageChild (main_window);

       XtRealizeWidget (toplevel);
       XtAppMainLoop (app_context);
   }

   /* file_select_cb() -- callback routine for "OK" button in
    * FileSelectionDialogs.
    */
   void
   file_select_cb(dialog, client_data, call_data)
   Widget dialog;
   XtPointer client_data;
   XtPointer call_data;
   {
       char buf[256], *filename, *text;
       struct stat statb;
       long len;
       FILE *fp;
       int reason = (int) client_data;
       XmFileSelectionBoxCallbackStruct *cbs =
           (XmFileSelectionBoxCallbackStruct *) call_data;

       if (!XmStringGetLtoR (cbs->value, XmFONTLIST_DEFAULT_TAG, &filename))
           return; /* must have been an internal error */

       if (*filename == NULL) {
           XtFree (filename);
           XBell (XtDisplay (text_edit), 50);
           XmTextSetString (text_output, "Choose a file.");
           return; /* nothing typed */
       }

       if (reason == FILE_SAVE) {
           if (!(fp = fopen (filename, "w"))) {
               perror (filename);
               sprintf (buf, "Can't save to %s.", filename);
               XmTextSetString (text_output, buf);
               XtFree (filename);
               return;
           }
           /* saving -- get text from Text widget... */
           text = XmTextGetString (text_edit);
           len = XmTextGetLastPosition (text_edit);
           /* write it to file (check for error) */
           if (fwrite (text, sizeof (char), len, fp) != len)
               strcpy (buf, "Warning: did not write entire file!");
           else {
               /* make sure a newline terminates file */
               if (text[len-1] != '0)
                   fputc ('0, fp);
               sprintf (buf, "Saved %ld bytes to %s.", len, filename);
           }
       }
       else { /* reason == FILE_OPEN */
           /* make sure the file is a regular text file and open it */
           if (stat (filename, &statb) == -1 ||
                   (statb.st_mode & S_IFMT) != S_IFREG ||
                   !(fp = fopen (filename, "r"))) {
               perror (filename);
               sprintf (buf, "Can't read %s.", filename);
               XmTextSetString (text_output, buf);
               XtFree (filename);
               return;
           }
           /* put the contents of the file in the Text widget by
            * allocating enough space for the entire file, reading the
            * file into the space, and using XmTextSetString() to show
            * the file.
            */
           len = statb.st_size;
           if (!(text = XtMalloc ((unsigned)(len+1)))) /* +1 for NULL */
               sprintf (buf, "%s: XtMalloc(%ld) failed", len, filename);
           else {
               if (fread (text, sizeof (char), len, fp) != len)
                   sprintf (buf, "Warning: did not read entire file!");
               else
                   sprintf (buf, "Loaded %ld bytes from %s.", len, filename);
               text[len] = 0; /* NULL-terminate */
               XmTextSetString (text_edit, text);
           }
       }
       XmTextSetString (text_output, buf); /* purge output message */

       /* free all allocated space. */
       XtFree (text);
       XtFree (filename);
       fclose (fp);
       XtUnmanageChild (dialog);
   }

   /* popdown_cb() -- callback routine for "Cancel" button. */
   void
   popdown_cb (w, client_data, call_data)
   Widget w;
   XtPointer client_data;
   XtPointer call_data;
   {
       XtUnmanageChild (w);
   }

   /* file_cb() -- a menu item from the "File" pulldown menu was selected */
   void
   file_cb(w, client_data, call_data)
   Widget w;
   XtPointer client_data;
   XtPointer call_data;
   {
       static Widget open_dialog, save_dialog;
       Widget dialog = NULL;
       XmString button, title;
       int reason = (int) client_data;

       if (reason == FILE_EXIT)
           exit (0);

       XmTextSetString (text_output, NULL);   /* clear message area */

       if (reason == FILE_OPEN && open_dialog)
           dialog = open_dialog;
       else if (reason == FILE_SAVE && save_dialog)
           dialog = save_dialog;

       if (dialog) {
           XtManageChild (dialog);
           /* make sure that dialog is raised to top of window stack */
           XMapRaised (XtDisplay (dialog), XtWindow (XtParent (dialog)));
           return;
       }

       dialog = XmCreateFileSelectionDialog (text_edit, "Files", NULL, 0);
       XtAddCallback (dialog, XmNcancelCallback, popdown_cb, NULL);
       XtAddCallback (dialog, XmNokCallback, file_select_cb, reason);
       if (reason == FILE_OPEN) {
           button = XmStringCreateLocalized ("Open");
           title = XmStringCreateLocalized ("Open File");
           open_dialog = dialog;
       }
       else { /* reason == FILE_SAVE */
           button = XmStringCreateLocalized ("Save");
           title = XmStringCreateLocalized ("Save File");
           save_dialog = dialog;
       }
       XtVaSetValues (dialog,
           XmNokLabelString, button,
           XmNdialogTitle,   title,
           NULL);
       XmStringFree (button);
       XmStringFree (title);
       XtManageChild (dialog);
   }

   /* search_cb() -- a menu item from the "Search" pulldown menu selected */
   void
   search_cb(w, client_data, call_data)
   Widget w;
   XtPointer client_data;
   XtPointer call_data;
   {
       char *search_pat, *p, *string, *new_pat, buf[256];
       XmTextPosition pos = 0;
       int len, nfound = 0;
       int search_len, pattern_len;
       int reason = (int) client_data;
       Boolean found = False;

       XmTextSetString (text_output, NULL);   /* clear message area */

       if (reason == SEARCH_CLEAR) {
           pos = XmTextGetLastPosition (text_edit);
           XmTextSetHighlight (text_edit, 0, pos, XmHIGHLIGHT_NORMAL);
           return;
       }

       if (!(string = XmTextGetString (text_edit)) || !*string) {
           XmTextSetString (text_output, "No text to search.");
           return;
       }
       if (!(search_pat = XmTextGetString (search_text)) || !*search_pat) {
           XmTextSetString (text_output, "Specify a search pattern.");
           XtFree (string);
           return;
       }

       new_pat = XmTextGetString (replace_text);
       search_len = strlen (search_pat);
       pattern_len = strlen (new_pat);

       if (reason == SEARCH_FIND_NEXT) {
           pos = XmTextGetCursorPosition (text_edit) + 1;
           found = XmTextFindString (text_edit, pos, search_pat,
               XmTEXT_FORWARD, &pos);
           if (!found)
               found = XmTextFindString (text_edit, 0, search_pat,
                   XmTEXT_FORWARD, &pos);
           if (found)
               nfound++;
       }
       else { /* reason == SEARCH_SHOW_ALL || reason == SEARCH_REPLACE */
           do {
               found = XmTextFindString (text_edit, pos, search_pat,
                   XmTEXT_FORWARD, &pos);
               if (found) {
                   nfound++;
                   if (reason == SEARCH_SHOW_ALL)
                       XmTextSetHighlight (text_edit, pos, pos + search_len,
                           XmHIGHLIGHT_SELECTED);
                   else
                       XmTextReplace (text_edit, pos, pos + search_len, new_pat);
                   pos++;
         }
     }
           while (found);
       }      

       if (nfound == 0)
           XmTextSetString (text_output, "Pattern not found.");
       else {
           switch (reason) {
               case SEARCH_FIND_NEXT :
                   sprintf (buf, "Pattern found at position %ld.", pos);
                   XmTextSetInsertionPosition (text_edit, pos);
                   break;
               case SEARCH_SHOW_ALL :
                   sprintf (buf, "Found %d occurrences.", nfound);
                   break;
               case SEARCH_REPLACE :
                   sprintf (buf, "Made %d replacements.", nfound);
           }
           XmTextSetString (text_output, buf);
       }
       XtFree (string);
       XtFree (search_pat);
       XtFree (new_pat);
   }

   /* edit_cb() -- the callback routine for the items in the edit menu */
   void
   edit_cb(widget, client_data, call_data)
   Widget widget;
   XtPointer client_data;
   XtPointer call_data;
   {
       Boolean result = True;
       int reason = (int) client_data;
       XEvent *event = ((XmPushButtonCallbackStruct *) call_data)->event;
       Time when;

       XmTextSetString (text_output, NULL);   /* clear message area */

       if (event != NULL &&
           reason == EDIT_CUT || reason == EDIT_COPY || reason == EDIT_CLEAR) {
           switch (event->type) {
               case ButtonRelease :
                   when = event->xbutton.time;
                   break;
               case KeyRelease :
                   when = event->xkey.time;
                   break;
               default:
                   when = CurrentTime;
                   break;
           }
       }

       switch (reason) {
           case EDIT_CUT :
               result = XmTextCut (text_edit, when);
               break;
           case EDIT_COPY :
               result = XmTextCopy (text_edit, when);
               break;
           case EDIT_PASTE :
               result = XmTextPaste (text_edit);
           case EDIT_CLEAR :
               XmTextClearSelection (text_edit, when);
               break;
       }
       if (result == False)
           XmTextSetString (text_output, "There is no selection.");
   }

15.5 Text Callbacks

The Text and TextField widgets use callback routines in the same way as other Motif widgets. The widgets provide callbacks for a number of different purposes, such as text modification, activation, and selection ownership. Some of the routines, such as those that monitor keyboard input, may be invoked rather frequently. In the next few sections, we introduce several of the callback routines for the widgets.

15.5.1 The Activation Callback

We begin by exploring the callback routine that is most commonly used for single-line Text widgets and TextField widgets. This callback is the XmNactivateCallback, which is invoked when the user presses RETURN in a TextField widget or a single-line Text widget. The callback is not called for multiline Text widgets. The callback routine for an XmN­activateCallback receives the common XmAnyCallbackStruct as the call_data parameter to the function. The callback reason is always XmCR_ACTIVATE. the source code shows a callback function for some TextField widgets. XtSetLanguageProc() is only available in X11R5; there is no corresponding function in X11R4.

   /* text_box.c -- demonstrate simple use of XmNactivateCallback
    * for TextField widgets.  Create a rowcolumn that has rows of Form
    * widgets, each containing a Label and a Text widget.  When
    * the user presses Return, print the value of the text widget
    * and move the focus to the next text widget.
    */
   #include <Xm/TextF.h>
   #include <Xm/LabelG.h>
   #include <Xm/Form.h>
   #include <Xm/RowColumn.h>

   char *labels[] = { "Name:", "Address:", "City:", "State:", "Zip:" };

   main(argc, argv)
   int argc;
   char *argv[];
   {
       Widget        toplevel, text_w, form, rowcol;
       XtAppContext  app;
       int           i;
       void          print_result();

       XtSetLanguageProc (NULL, NULL, NULL);

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

       rowcol = XtVaCreateWidget ("rowcol",
           xmRowColumnWidgetClass, toplevel, NULL);

       for (i = 0; i < XtNumber (labels); i++) {
           form = XtVaCreateWidget ("form", xmFormWidgetClass, rowcol,
               XmNfractionBase,  10,
               NULL);
           XtVaCreateManagedWidget (labels[i],
               xmLabelGadgetClass, form,
               XmNtopAttachment,    XmATTACH_FORM,
               XmNbottomAttachment, XmATTACH_FORM,
               XmNleftAttachment,   XmATTACH_FORM,
               XmNrightAttachment,  XmATTACH_POSITION,
               XmNrightPosition,    3,
               XmNalignment,        XmALIGNMENT_END,
               NULL);
           text_w = XtVaCreateManagedWidget ("text_w",
               xmTextFieldWidgetClass, form,
               XmNtraversalOn,      True,
               XmNrightAttachment,  XmATTACH_FORM,
               XmNleftAttachment,   XmATTACH_POSITION,
               XmNleftPosition,     4,
               NULL);

           /* When user hits return, print the label+value of text_w */
           XtAddCallback (text_w, XmNactivateCallback,
               print_result, labels[i]);

           XtManageChild( form);
       }
       XtManageChild (rowcol);

       XtRealizeWidget (toplevel);
       XtAppMainLoop (app);
   }

   /* preint_result() -- callback for when the user hits return in the
    * TextField widget.
    */
   void
   print_result(text_w, client_data, call_data)
   Widget text_w;
   XtPointer client_data;
   XtPointer call_data;
   {
       char *value = XmTextFieldGetString (text_w);
       char *label = (char *) client_data;

       printf ("%s %s0, label, value);
       XtFree (value);

       XmProcessTraversal (text_w, XmTRAVERSE_NEXT_TAB_GROUP);
   }
The program displays a data form using a RowColumn widget that manages several rows of Form widgets. Each Form contains a Label and a TextField widget, as shown in the figure.

When the user enters a value for a field and presses RETURN, the print_result() callback routine is invoked. The routine prints the value of the field and advances the keyboard focus to the next widget using XmProcessTraversal(). This function takes a widget and a traversal direction as its two parameters. We use the XmTRAVERSE_NEXT_TAB_ GROUP direction because each TextField widget is a tab group in and of itself, so we need to move to the next tab group, rather than to the next item in the same tab group. See Section #skeybtrav, for more information on tab groups.

figs.eps/V6a.14.07.eps.png
Output of text_box.c

When a single-line Text widget or a TextField widget is used as part of a predefined Motif dialog, the XmNactivateCallback for the widget is automatically hooked up to the OK button in the dialog. As a result, the same callback is called when the user presses RETURN in the widget or when the user selects the OK button. This convenience can confuse an unsuspecting programmer who may find that his callback is being invoked twice. It is also possible to overestimate what the Motif toolkit is going to do and expect a callback to be invoked when it isn't. The point is to be sure to verify that these callbacks are getting called at the appropriate times. See Chapter 6, Selection Dialogs, for examples of this feature in SelectionDialogs, PromptDialogs, and CommandDialogs.

15.5.2 Text Modification Callbacks

In this section, we discuss the callback routines that can be used to monitor and control text modification. Monitoring occurs both when the user types into a Text widget and when the text is changed using a convenience routine such as XmTextInsert(). These callbacks work for both single-line and multiline Text widgets, as well as TextField widgets. Since the text in a widget is modified by each keystroke, the modification callbacks are invoked quite frequently.

There are two callbacks for text modification: XmNmodifyVerifyCallback is called before the text is modified, and XmNvalueChangedCallback is called after the text has been changed. Depending on the needs of an application, either or both callbacks may be used on the same widget. You should never call XtVaSetValues() in one of these callbacks on the widget that is being modified because the state of the widget is unstable during these callbacks. Avoid adding or deleting callbacks or changing resources, especially the XmNvalue resource, in a callback routine. If a recursive loop occurs, you may get very unpredictable results.

Installing an XmNmodifyVerifyCallback function is useful when you need to monitor or change the user's input before it actually gets inserted into a Text widget. In the source code we demonstrate using this callback to convert text to uppercase. XtSetLanguageProc() is only available in X11R5; there is no corresponding function in X11R4.

   /* allcaps.c -- demonstrate the XmNmodifyVerifyCallback for
    * Text widgets by using one to convert all typed input to
    * capital letters.
    */
   #include <Xm/Text.h>
   #include <Xm/LabelG.h>
   #include <Xm/RowColumn.h>
   #include <ctype.h>

   void allcaps();

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

       XtSetLanguageProc (NULL, NULL, NULL);

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

       rowcol = XtVaCreateWidget ("rowcol",
           xmRowColumnWidgetClass, toplevel,
           XmNorientation, XmHORIZONTAL,
           NULL);

       XtVaCreateManagedWidget ("Enter Text:",
           xmLabelGadgetClass, rowcol, NULL);
       text_w = XtVaCreateManagedWidget ("text_w",
           xmTextWidgetClass, rowcol, NULL);

       XtAddCallback (text_w, XmNmodifyVerifyCallback, allcaps, NULL);

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

   /* allcaps() -- convert inserted text to capital letters. */
   void
   allcaps(text_w, client_data, call_data)
   Widget      text_w;
   XtPointer   client_data;
   XtPointer   call_data;
   {
       int len;
       XmTextVerifyCallbackStruct *cbs =
           (XmTextVerifyCallbackStruct *) call_data;

       if (cbs->text->ptr == NULL)
           return;

       /* convert all input to upper-case if necessary */
       for (len = 0; len < cbs->text->length; len++)
           if (islower (cbs->text->ptr[len]))
               cbs->text->ptr[len] = toupper (cbs->text->ptr[len]);
   }

The program creates a RowColumn widget that contains a Label and a Text widget, as shown in the figure.

figs.eps/V6a.14.08.eps.png
Output of allcaps.c

The Text widget uses the allcaps() routine as its XmNmodifyVerifyCallback function. The routine is actually quite simple, but there are a lot of details to examine. The call_data parameter to the function is of type XmTextVerifyCallbackStruct. This data structure provides information about the modification that may be done to the text. The data structure is defined as follows:

   typedef struct {
       int             reason;
       XEvent         *event;
       Boolean         doit;
       XmTextPosition  currInsert, newInsert;
       XmTextPosition  startPos, endPos;
       XmTextBlock     text;
   } XmTextVerifyCallbackStruct;
With an XmNmodifyVerifyCallback, the reason field has the value XmCR_MODIFYING_TEXT_VALUE. The event field contains the XEvent that caused the callback to be invoked; this field is NULL if the modification is being done by a convenience routine that modifies the text. The values for currInsert and newInsert are always the same for a modification callback. These fields specify the location of the insertion cursor, so they are only different for the XmNmotionVerifyCallback when the user moves the insertion point.

The values for startPos and endPos indicate the range of text that is affected by the modification. For insertion, these values are always the same. However, for text deletion or replacement, the values specify the beginning and end of the text about to be deleted. For example, if the user selects some text and presses the BACKSPACE key, the startPos and endPos values indicate the boundaries of the text about to be deleted. We discuss text deletion in detail in an upcoming section.

The text field points to a data structure that describes the text about to be added to the widget. The field is a pointer of type XmTextBlock, which is defined as follows:

   typedef struct {
       char        *ptr;
       int          length;
       XmTextFormat format;
   } XmTextBlockRec, *XmTextBlock;

The text being added is accessible through ptr ; it is dynamically allocated using XtMalloc() for each callback invocation. The ptr field is not NULL -terminated, so you should not use strlen() or strcpy() to copy the data. The length is stored in the length field, so if you want to copy the text, you should use strncpy(). If the user is deleting text, length is 0. While ptr should also be NULL in this case, the field isn't always set this way, so you shouldn't rely on it. The format field specifies the width of the text characters and can have the value FMT8BIT or FMT16BIT.

Let's review the simple case of adding new text, as demonstrated in the source code When new text is inserted into the Text widget, the values for currInsert, newInsert, startPos, and endPos all have the same value, which is the position in the widget where the new text will be added. Since the new text has not yet been added to the value of the widget, the application can change the value of ptr in the text block. In the allcaps() routine, we modify the input to be all capital letters by looping through the valid bytes in the ptr field of the text block that is going to be added, as shown in the following fragment:

   for (len = 0; len < cbs->text->length; len++)
       if (islower (cbs->text->ptr[len]))
           cbs->text->ptr[len] = toupper (cbs->text->ptr[len]);
The islower() and toupper() macros are found in the < ctype.h> header file.

Since allcaps() is called each time new text is added to the widget, you might wonder how length can ever be more than one. If the user pastes a block of text into the widget, the entire block is added at once, so ptr points to that text, and length specifies the amount of text. Our loop handles both single-character typing and text-block paste operations. the source code demonstrates how an application can modify the text that is entered by a user before it is displayed. An application may also want to filter the new text and prevent certain characters from being inserted. The easiest way to prevent a text modification is to set the doit field in the XmTextVerifyCallbackStruct to False. When the modification callback routine returns, the Text widget checks this field. If it has been set to False, the widget discards the new text, and the widget is left unmodified.

When a text modification is vetoed, the Text widget can sound the console bell to provide audio feedback informing the user that the input has been rejected. This action is dependent on the value of the XmNverifyBell resource. The default value is based on the value of the XmNaudibleWarning resource of the VendorShell, so it is set to True by default. You should allow a user to set this resource in a resource file, so he can turn off error notification if he doesn't want it. If you hard-code the resource value, users cannot control this feature. You should provide documentation with your application that explains how to set this resource or provide a way to set the value from the application.

the source code demonstrates a modification callback routine that filters input and prevents certain characters from being entered. The check_zip() routine would be used as the XmNmodifyVerifyCallback for a Text widget that prompts for a ZIP code. We want the user to type only digits; all other input should be ignored. We also want to keep the user from typing a string that is longer than five digits.

   /* check_zip() -- limit the user to entering a ZIP code. */
   void
   check_zip(text_w, client_data, call_data)
   Widget text_w;
   XtPointer client_data;
   XtPointer call_data;
   {
       XmTextVerifyCallbackStruct *cbs =
           (XmTextVerifyCallbackStruct *) call_data;
       int len = XmTextGetLastPosition (text_w);

       if (cbs->startPos < cbs->currInsert) /* backspace */
           return;

       if (len == 5) {
           cbs->doit = False;
           return;
       }
       /* check that the new additions won't put us over 5 */
       if (len + cbs->text->length > 5) {
           cbs->text->ptr[5 - len] = 0;
           cbs->text->length = strlen (cbs->text->ptr);
       }
       for (len = 0; len < cbs->text->length; len++) {
           /* make sure all additions are digits. */
           if (!isdigit (cbs->text->ptr[len])) {
               /* not a digit-- move all chars down one and
                * decrement cbs->text->length.
                */
               int i;
               for (i = len; (i+1) < cbs->text->length; i++)
                   cbs->text->ptr[i] = cbs->text->ptr[i+1];
               cbs->text->length--;
               len--;
           }
       }
       if (cbs->text->length == 0)
           cbs->doit = False;
   }
The first thing we do in check_zip() is to see if the user is backspacing, in which case we simply return. If text is not being deleted, then new text is definitely being added. Since the length of the current text is not available in the callback structure, we call XmTextGetLastPosition() to determine it. If the string is already five digits long, we don't want to add more digits, so we set doit to False and return.

Otherwise, we loop through the length of the new text and check for characters that are not digits. If any exist, we remove them by shifting all of the characters that follow down one place, overwriting the undesirable character. If we loop through all of the characters and find that none of them are digits, the length ends up being zero, so we set doit to False. A modification callback can determine if the user is backspacing or deleting a large block of text by checking to see if startPos is less than currInsert. Alternatively, the routine could check to see if text->length is 0. For backspacing, the values differ by one. If the user selects a large block of text and deletes the selection, the XmNmodifyVerifyCallback is invoked once to delete the text and may be invoked a second time if the user has typed new text to replace the selected text.

Our next example program demonstrates how to process character deletions in a text modification callback. the source code creates a single-line Text widget that prompts the user for a password. We don't provide any encryption for the password; we simply mask what the user is typing by displaying an asterisk (*) for each character. The actual text is stored in a separate internal variable. The challenge for this application is to capture the input text, store it internally, and modify the output, even for backspacing. XtSetLanguageProc() is only available in X11R5; there is no corresponding function in X11R4.

   /* password.c -- prompt for a password. All input looks like
    * a series of *'s.  Store the actual data typed by the user in
    * an internal variable.  Don't allow paste operations.  Handle
    * backspacing by deleting all text from insertion point to the
    * end of text.
    */
   #include <Xm/Text.h>
   #include <Xm/LabelG.h>
   #include <Xm/RowColumn.h>
   #include <ctype.h>

   void check_passwd();
   char *passwd; /* store user-typed passwd here. */

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

       XtSetLanguageProc (NULL, NULL, NULL);

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

       rowcol = XtVaCreateWidget ("rowcol",
           xmRowColumnWidgetClass, toplevel,
           XmNorientation, XmHORIZONTAL,
           NULL);

       XtVaCreateManagedWidget ("Password:",
           xmLabelGadgetClass, rowcol, NULL);
       text_w = XtVaCreateManagedWidget ("text_w",
           xmTextWidgetClass, rowcol, NULL);

       XtAddCallback(text_w, XmNmodifyVerifyCallback, check_passwd, NULL);
       XtAddCallback(text_w, XmNactivateCallback, check_passwd, NULL);

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

   /* check_passwd() -- handle the input of a password. */
   void
   check_passwd(text_w, client_data, call_data)
   Widget        text_w;
   XtPointer     client_data;
   XtPointer     call_data;
   {
       char *new;
       int len;
       XmTextVerifyCallbackStruct *cbs =
           (XmTextVerifyCallbackStruct *) call_data;

       if (cbs->reason == XmCR_ACTIVATE) {
           printf ("Password: %s0, passwd);
           return;
       }

       if (cbs->startPos < cbs->currInsert) {   /* backspace */
           cbs->endPos = strlen (passwd);       /* delete from here to end */
           passwd[cbs->startPos] = 0;           /* backspace--terminate */
           return;
       }

       if (cbs->text->length > 1) {
           cbs->doit = False;  /* don't allow "paste" operations */
           return;             /* make the user *type* the password! */
       }

       new = XtMalloc (cbs->endPos + 2); /* new char + NULL terminator */
       if (passwd) {
           strcpy (new, passwd);
           XtFree (passwd);
       } else
           new[0] = NULL;
       passwd = new;
       strncat (passwd, cbs->text->ptr, cbs->text->length);
       passwd[cbs->endPos + cbs->text->length] = 0;

       for (len = 0; len < cbs->text->length; len++)
           cbs->text->ptr[len] = '*';
   }
As you can see in the figure, the Text widget only displays asterisks, no matter what the user has typed.

figs.eps/V6a.14.09.eps.png
Output of password.c

We use the check_passwd() function for both the XmNactivateCallback and the XmNmodifyVerifyCallback callbacks. When the user presses RETURN, the routine prints what has been typed to stdout. If the user is not backspacing through the text, we know we can add the new text to passwd, which is the internal variable we use to store the text. Once the new text has been copied, we convert it into asterisks, so that the user cannot see what has been typed.

We need to handle two different cases for deletion. If the insertion cursor is at the end of the typed string and the user backspaces, we simply allow the action. If the user clicks somewhere in the middle of the string and then backspaces, we delete all of the characters from that point in the string to the end, since the user cannot see the characters that he is deleting.

To handle the different forms of text deletion, we test to see if startPos is less than currInsert. Since startPos and endPos specify the range of text that is being deleted, we can change these values and effectively delete more text than the user originally intended. By setting endPos to the string length of the internal variable passwd , we handle both of the cases that we just described. If we had wanted to, we could also have set startPos to 0 and deleted all of the text. We can expand on the ZIP code example that we used for filtering non-digits from typed input by providing an input field for an area code and phone number. The format for a US phone number is as follows:

   123-456-7890
We want to filter out all non-digits for a phone number, but we also want to add the dash character (-) automatically as it is needed. For example, after the user enters three digits, the Text widget should automatically insert a dash, so that the next character expected from the user is still a digit. Similarly, when the user backspaces and deletes a dash character, the widget should delete the preceding digit as well. shows how the interaction should work.

tab(@), linesize(2); l | l n | n. User Types@Text Widget Displays
_
4@4 1@41 5@415- 4@415-4 T{ BACKSPACE T}@415- T{ BACKSPACE T}@41
_ We can continue to use the same type of algorithm that we used in check_zip() to filter digits, and we can use some of the code from check_passwd() to handle backspacing. The only remaining problem is adding the necessary dash characters. Since we are using US phone numbers, we know that the dashes should occur after the third and seventh digits. Therefore, when currInsert is either 2 or 6, the new digit should be added first, followed by the dash. the source code shows the program that implements this functionality. XtSetLanguageProc() is only available in X11R5; there is no corresponding function in X11R4.

   /* prompt_phone.c -- a complex problem for XmNmodifyVerifyCallback.
    * Prompt for a phone number by filtering digits only from input.
    * Don't allow paste operations and handle backspacing.
    */
   #include <Xm/Text.h>
   #include <Xm/LabelG.h>
   #include <Xm/RowColumn.h>
   #include <ctype.h>

   void check_phone();

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

       XtSetLanguageProc (NULL, NULL, NULL);

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

       rowcol = XtVaCreateWidget ("rowcol",
           xmRowColumnWidgetClass, toplevel,
           XmNorientation, XmHORIZONTAL,
           NULL);

       XtVaCreateManagedWidget ("Phone Number:",
           xmLabelGadgetClass, rowcol, NULL);
       text_w = XtVaCreateManagedWidget ("text_w",
           xmTextWidgetClass, rowcol, NULL);

       XtAddCallback (text_w, XmNmodifyVerifyCallback, check_phone, NULL);

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

   /* check_phone() -- handle phone number input. */
   void
   check_phone(text_w, client_data, call_data)
   Widget     text_w;
   XtPointer  client_data;
   XtPointer  call_data;
   {
       char c;
       int len = XmTextGetLastPosition(text_w);
       XmTextVerifyCallbackStruct *cbs =
           (XmTextVerifyCallbackStruct *) call_data;

       /* no backspacing or typing in the middle of string */
       if (cbs->currInsert < len) {
           cbs->doit = False;
           return;
       }

       if (cbs->text->length == 0) {  /* backspace */
           if (cbs->startPos == 3 || cbs->startPos == 7)
               cbs->startPos--;       /* delete the hyphen too */
           return;
       }

       if (cbs->text->length > 1) { /* don't allow clipboard copies */
           cbs->doit = False;
           return;
       }

       /* don't allow non-digits or let the input exceed 12 chars */
       if (!isdigit (c = cbs->text->ptr[0]) || len >= 12)
           cbs->doit = False;
       else if (len == 2 || len == 6) {
           cbs->text->ptr = XtRealloc (cbs->text->ptr, 2);
           cbs->text->length = 2;
           cbs->text->ptr[0] = c;
           cbs->text->ptr[1] = '-';
       }
   }
There are a couple of ways that you could think to add the dashes. One way would be to use the XmNvalueChangedCallback to keep track of the phone number after it has been entered and then use XmTextInsert() to add the dashes when appropriate. The problem with this approach is that XmTextInsert() activates the XmNmodifyVerifyCallback function again, so the dash would be subject to the input filtering.

As a result, the only way to handle the situation is to actually add the dashes in the XmN­modifyVerifyCallback routine at the same time the digits are added. This approach involves modifying the ptr and length fields of the XmTextBlock structure in the XmTextVerifyCallbackStruct. The check_phone() routine checks the current length of the phone number. If it is either two or six characters long, the routine reallocates ptr to hold two characters, adds the dash, and increments length to account for the dash.

When the Text widget adds the digit and the dash, it positions the insertion cursor at the end of the new text. Prior to Motif 1.2, the position of the insertion cursor was not affected by the amount of text that was added. The workaround to this problem was to use the XmNvalueChangedCallback and call XmTextSetInsertionPosition(). Although we haven't demonstrated its use, the XmNvalueChangedCallback is useful when you need to keep track of the changes in a Text widget, but you don't need to monitor or change the input before it is displayed. This callback is invoked after the text has been modified in any way, which means that it is called for each insertion and deletion. The call_data parameter to the routine is of type XmAnyCallbackStruct; the reason field is always XmCR_VALUE_CHANGED.

The check_phone() routine is fairly simple, in that it only allows text insertions and deletions that occur when the insertion cursor is at the end of the text. While it is possible to handle modifications in the middle of the text, the code quickly becomes a large bowl of spaghetti. We do not allow clipboard copies of more than one character at a time for the same reason. Our routine is sufficient for demonstration purposes, but for a real application, you should handle these cases.

15.5.3 The Cursor Movement Callback

The XmNmotionVerifyCallback can be used to monitor the position of the insertion cursor. This callback is invoked when the user moves the location cursor using the mouse or the arrow keys, when the user drags the mouse or multi-clicks to extend the text selection, or when the application calls a Text widget function that moves the cursor or adds, deletes, or replaces text. However, if the cursor does not move as a result of a function being called, the callback is not invoked. The XmNmotionVerifyCallback allows an application to intercept and prevent cursor movement.

The XmNmotionVerifyCallback uses the XmTextVerifyCallbackStruct as its callback structure, just like the XmNmodifyVerifyCallback. However, for motion callbacks, the reason is XmCR_MOVING_INSERT_CURSOR and the startPos , endPos, and text fields are invalid. The doit field can be set to False to reject requests to reposition the insertion cursor.

If the cursor motion occurs as a result of a user action, the event field should point to an XEvent structure describing the action that caused the cursor position to be modified, When the cursor moves as a result of an application action, the field should be set to NULL. However, the event field is currently set to NULL regardless of what caused the cursor motion. This bug makes it impossible to tell the difference between a cursor motion performed by the user and one caused by the application.

We can use the XmNmotionVerifyCallback to tie up a loose end in prompt_phone.c. To make the text verification simpler, we don't want to allow the user to move the insertion cursor except by entering digits or backspacing. the source code shows a new version of the check_phone() routine that prevents cursor movement.

   main(argc, argv)
   int argc;
   char *argv[];
   {
       Widget text_w;

       ... 
       XtAddCallback (text_w, XmNmodifyVerifyCallback, check_phone, NULL);
       ...
   }

   /* check_phone() -- handle phone number input. */
   void
   check_phone(text_w, client_data, call_data)
   Widget     text_w;
   XtPointer  client_data;
   XtPointer  call_data;
   {
       char c;
       int len = XmTextGetLastPosition (text_w);
       XmTextVerifyCallbackStruct *cbs =
           (XmTextVerifyCallbackStruct *) call_data;

       if (cbs->reason == XmCR_MOVING_INSERT_CURSOR) {
           if (cbs->newInsert != len)
         cbs->doit = False;
           return;
       }

       /* no backspacing, typing or stuffing in middle of string */
       if (cbs->currInsert < len) {
           cbs->doit = False;
           return;
       }

       if (cbs->text->length == 0) { /* backspace */
           if (cbs->startPos == 3 || cbs->startPos == 7)
               cbs->startPos--;      /* delete the hyphen too */
           return;
       }

       if (cbs->text->length > 1) { /* don't allow clipboard copies */
           cbs->doit = False;
           return;
       }

       /* don't allow non-digits or let the input exceed 12 chars */
       if (!isdigit (c = cbs->text->ptr[0]) || len >= 12)
           cbs->doit = False;
       else if (len == 2 || len == 6) {
           cbs->text->ptr = XtRealloc (cbs->text->ptr, 2);
           cbs->text->length = 2;
           cbs->text->ptr[0] = c;
           cbs->text->ptr[1] = '-';
       }
   }
We check the value of newInsert against the length of the current string to determine whether or not the intended cursor position is at the end of the text string. If it is not, we set doit to False to prevent the cursor movement. The XmNmotionVerifyCallback function can also be used to monitor pointer dragging for text selections.

15.5.4 Focus Callbacks

The XmNfocusCallback and XmNlosingFocusCallback callback routines can be used to monitor when a Text widget gains and loses the keyboard focus. A Text widget can receive the input focus if the user intentionally shifts the focus to the widget or if the application moves the focus using XmProcessTraversal(). When a widget gains the input focus and the insertion cursor is not visible, we can make it visible and cause the widget to automatically scroll to the current cursor location by installing an XmNfocusCallback routine that calls XmTextShowCursorPosition(), as shown in the following code fragment:

   {
       Widget text_w;
       extern void gain_focus();

       ...
       text_w = XmCreateScrolledText(...);
       XtAddCallback(text_w, XmNfocusCallback, gain_focus, NULL);
       ...
   }

   void
   gain_focus(text_w, client_data, call_data)
   Widget    text_w;
   XtPointer client_data;
   XtPointer call_data;
   {
       XmTextShowCursorPosition (text_w, XmTextGetCursorPosition (text_w));
   }
The XmNfocusCallback is passed a callback structure of type XmAnyCallbackStruct with the callback reason set to XmCR_FOCUS .

The XmNlosingFocusCallback callback can be used to monitor when the Text widget loses its focus. The callback structure passed to the callback function is an XmTextVerifyCallbackStruct. All of the fields except the text field are valid, and the reason field is set to XmCR_LOSING_FOCUS.

15.6 Text Widget Internationalization

In Motif 1.2, the Text and TextField widgets have been modified to support internationalized input and output. The internationalization capabilities of the widgets are layered on top of the functionality provided in X11R5, which is based on the ANSI-C locale model. An internationalized application uses a library that reads a locale database at runtime to get information about the user's language environment. An application that uses the X Toolkit establishes its language environment (or locale) by registering a language procedure using XtSetLanguageProc(), as explained in Section #slangproc. See Volume Four, X Toolkit Intrinsics Programming Manual, for more information on the localization of an Xt-based application.

15.6.1 Text Representation

One of the important characteristics of a locale is the encoding used to represent the character set for the locale. A character set is simply a set of characters, while an encoding is a numeric representation of these characters. A charset (not the same as a character set) is an encoding in which all of the characters use the same number of bits. The Latin-1 charset (ISO8859-1) defines an encoding for all of the characters used in Western languages. However, not all languages can be represented by a single charset. Japanese text commonly contains words written using the Latin alphabet, as well as phonetic characters from the katakana and hirigana alphabets, and ideographic kanji characters. Each of these character sets has its own charset. The phonetic and Latin charsets are 8-bits wide, while the ideographic charset is 16-bits wide. Since the charsets must be combined into a single encoding for Japanese text, the encoding uses shift sequences to specify the character set for each character in a string.

When an encoding contains shift sequences and characters of nonuniform width, strings can still be stored in a standard NULL-terminated array of characters; this representation is known as a multibyte string. Strings can also be stored using a wide-character type (wchar_t in ANSI-C) in which each character has a fixed size and occupies one array element. ANSI-C provides functions that convert between multibyte and wide-character strings and the text output routines in X11R5 support both types of strings. Multibyte strings are usually more compact than wide-character strings, but wide-character strings are easier to work with. If an internationalized application performs any text manipulation, it must take care to handle all strings properly. Fortunately, many applications can do internationalized text input and output without performing any manipulations on the text.

Multibyte strings are NULL-terminated, while there is no single convention for the termination of wide-character strings. The following C string-handling routines are safe to use with multibyte strings: strcat(), strcmp() , strcpy(), strlen(), and strncmp(). The string comparison routines are only useful to check for byte-for-byte equality; use strcoll() to compare strings for sorting. None of the C string-handling routines work with wide-character strings.

Multibyte strings can be written to a file or an output stream. If the terminal is operating in the current locale, printing a multibyte string to stdout or stderr causes the correct text to be displayed. Multibyte strings can also be read from a file or the stdin input stream. If the file is encoded in the current locale, or the terminal is operating in the current locale, the strings that are read are meaningful. For a more complete description of working with multibyte and wide-character strings, see Volume One, Xlib Programming Manual.

The Motif 1.2 Text and TextField widgets provide two resources for specifying their textual data: XmNvalue and XmNvalueWcs. The XmNvalue resource specifies the text string as a char * value, so it can be used to set the value of the widget to a multibyte string. XmN­valueWcs specifies the string as a wchar_t * value, so it is used to set the value to a wide-character string. This resource cannot be specified in a resource file. If XmNvalue and XmNvalueWcs are both defined, the value of XmNvalueWcs takes precedence.

Regardless of which resource you set, the widgets store the text internally as a multibyte string. The widgets take care of converting between multibyte strings and wide-character strings when necessary. As a result, you can set the text string using the XmNvalue resource and retrieve it with XtVaGetValues() using the XmNvalueWcs resource.

The Text widget provides the following convenience routines for manipulating the text value as a wide-character string:

   XmTextFindStringWcs()
   XmTextGetSelectionWcs()
   XmTextGetStringWcs()
   XmTextGetSubstringWcs()
   XmTextInsertWcs()
   XmTextReplaceWcs()
   XmTextSetStringWcs()
These routines work for both Text and TextField widgets. The TextField also provides corresponding functions that only work with TextField widgets. All of these routines function identically to their regular character string counterparts, except that they take or return wide-character string values. If you have specified the text string using XmNvalue, you can still use the wide-character string routines because they handle any necessary string conversions. For more information on the different wide-character routines, see Volume Six B, Motif Reference Manual.

The widgets also provide a wide-character version of the text modification callback, XmN­modifyVerifyCallbackWcs. This callback is invoked before the value of the widget is modified, so an application can use it to monitor changes in the widget. The callback is ­passed a callback structure of type XmTextVerifyCallbackStructWcs, which is defined as follows:

   typedef struct {
       int             reason;
       XEvent         *event;
       Boolean         doit;
       XmTextPosition  currInsert, newInsert;
       XmTextPosition  startPos, endPos;
       XmTextBlockWcs  text;
   } XmTextVerifyCallbackStructWcs;

With this structure the reason field has the value XmCR_MODIFYING_TEXT_VALUE. All of the fields have the same meaning as the fields in the regular XmTextVerifyCallbackStruct, except that the text field is a pointer of type XmTextBlockWcs. This structure is defined as follows:

   typedef struct {
       wchar_t     *wcsptr;
       int          length;
   } XmTextBlockRecWcs, *XmTextBlockWcs;
If callback routines are registered for both the XmN­modifyVerifyCallback and the ­XmNmodifyVerifyCallbackWcs , the routines for the XmNmodifyVerifyCallback are invoked first. The resulting data, which may have been modified, is passed to the XmN­modifyVerifyCallbackWcs routines.

15.6.2 Text Output

The Text and TextField widgets do not use compound strings, so their text output functionality is based directly on Xlib's internationalized text output capabilities. To support languages that use multiple charsets, X11R5 introduced the XFontSet abstraction for its text output routines. An XFontSet contains all of the fonts necessary to display text in the current locale. The new text output routines work with font sets, so they can render text for locales that require multiple charsets. See Volume One, Xlib Programming Manual, for more information on internationalized text output.

Each of the widgets has a XmNfontList resource for specifying the font that it uses. Since the widgets do not use compound strings, they cannot use font list tags to display text using different fonts as decribed in Section #sfonttag. However, the font list can specify a font set, so the widgets can display text using multiple character sets in a locale that requires them. The widgets pick a font by searching the font list for a font set that has the tag XmFONTLIST_DEFAULT_TAG. If the search finds such a font set, it is used. Otherwise, the widgets use the first font set specified in the font list. If the font list does not contain a font set, the first font is used. If you specify a font list entry with the tag XmFONTLIST_DEFAULT_TAG, make sure that it is appropriate for the encoding of the current locale.

15.6.3 Text Input

Converting user keystrokes into text in the encoding of the current locale is the most difficult task of internationalization. An internationalized program cannot assume any particular mapping between keystrokes and input characters, since it must run in any locale on a single workstation, using a single keyboard. The mapping between keystrokes and Japanese characters is very different and much more complex than the mapping between keystrokes and Latin characters, for example. When there are more characters in the codeset of a locale than there are keys on a keyboard, some sort of input method is required for mapping between multiple keystrokes and input characters.

All of the characters for English can be entered using the standard keyboard; the SHIFT key makes it possible to enter both lowercase and uppercase letters as well as the number and punctuation characters. For many European languages, the most common accented characters may appear directly on a keyboard, but there are still a number of other characters that cannot be entered with any single shifted or unshifted keystroke. In these cases, the input method is typically implemented in the keyboard hardware using a special key that puts the keyboard in "compose" mode in which one or more of the following keystrokes are combined into a single character.

The Asian ideographic languages are what make internationalized text input complicated. Japanese and Korean both have phonetic alphabets that are small enough to be mapped onto a keyboard. While it is sometimes adequate to leave text in this representation, the user usually wants the final text to be in the full ideographic language. Input methods for these languages often have the user type the phonetic symbols for a particular word or words and then signal that the composition or pre-editing is complete. At this point, the input method can look up the string of phonetic characters in a dictionary and convert it to the equivalent character or characters in the ideographic language. Multiple characters can have the same phonetic representation, so the user may still have to select the desired character.

Since input methods can be large and complex and they vary from locale to locale, it does not make sense to link every application with a generic input method that is localized at runtime. The X Input Method (XIM) abstraction in X11R5 supports the model of an input manager that is run as a separate process and that communicates with the X server and with the application. An application that needs to use an input method calls XOpenIM() to establish a connection to the input method that is appropriate for the current locale.

An input method needs to provide feedback to the user, so X defines three areas for interaction:

An application generally provides the status and pre-edit areas to the input method, which is responsible for their contents. The auxiliary area is managed entirely by the input method. The location of the pre-edit area depends on the interaction style used between the input method and the application. X defines the following four interaction styles:
An application must choose an interaction style that is supported by the input method and it must provide the pre-edit and status areas as required by that style.

Just as the X server can display multiple windows for a single client, an input method can maintain multiple input contexts for an application. A text editor that supports multiple editing windows within a single top-level window could create an input context for each window or share a single context among all of the windows. The function XCreateIC() creates an X Input Context ( XIC) that keeps track of information about the input context, such as the interaction style, the windows used for the pre-edit and status areas, and the font set for the text.

When an application gets a KeyPress event, it needs to use that event in a call to XmbLookupString() or XwcLookupString() to get the multibyte or wide-character string encoded in the current locale. These routines are analogous to XLookupString(), but this routine can only return Latin-1 strings, so it is not appropriate for internationalized input.

The support for input methods in Xlib is designed to be incorporated within toolkits and widgets. Accordingly, the internationalized text input capabilities of the Motif Text and TextField widgets are layered on top of the input method mechanism. Fortunately, the widgets encapsulate most of the lower-level functionality, so you don't need to understand the details of the Xlib implementation. For a more complete description of the Xlib functionality, see Volume One, Xlib Programming Manual.

Motif leaves it to the hardware vendors to supply input methods, so the toolkit does not provide any itself. If you need to provide internationalized text input, consult the documentation for your system for information about the input methods that it supports. Alternately, you can build one of the contributed input methods provided as part of X11R5. R5 as shipped from MIT contains two separate implementations of the input method facilities. The Xsi implementation is the default on all but Sony machines, which use the Ximp implementation. Each implementation defines its own protocol for communication between Xlib and input methods. Ximp and Xsi each come with contributed input methods that are not compatible with each other. For X11R6, the X Consortium is planning to standardize the input method implementation, so you may want to enquire about the status of that effort before putting any significant effort into a product that uses one of these implementations.

When you create an editable Text or TextField widget, it automatically provides a connection to the input method for the current locale. The VendorShell widget plays a role in internationalization as it defines the XmNinputMethod and XmNpreeditType resources for specifying the input method and the interaction style, respectively. A Text or TextField widget is always created as an ancestor of a VendorShell, so the widget can access these resources to set up the connection to the input method. The resources are defined by the VendorShell because it handles the geometry management of the pre-edit and status areas for the input method.

The XmNinputMethod resource specifies the input method portion of the locale modifier that is set before an input method is opened. The format of the value for this resource is vendor-defined. The XmNpreeditType resource sets the interaction style used by the input method. The syntax, possible values, and default value of this resource are also vendor-dependent.

Motif only supports the over-the-spot, off-the-spot, and root-window interaction styles. Under the off-the-spot style, the VendorShell positions the pre-edit and status areas below the application's main window but inside the shell. The VendorShell handles the geometry management for the areas and places a separator between the main window and the input method area. If the application sets or gets the XmNheight of the shell using XtVaSetValues() or XtVaGetValues(), the height includes the height of the input method area. With the over-the-spot style, the VendorShell still displays the status area at the bottom of the application's top-level window, but the pre-edit area is positioned over the insertion cursor in the Text widget. The Text widget passes the insertion position to the input method, so that the pre-edit area moves as with the insertion cursor.

The Motif toolkit implements its internationalized text input functionality using the following undocumented public routines:

   XmImRegister()
   XmImUnregister()
   XmImSetFocusValues()
   XmImSetValues()
   XmImUnsetFocus()
   XmImGetXIM()
   XmImMbLookupString()
   XmImVaSetFocusValues()
   XmImVaSetValues()
These routines simplify the interaction with the lower-level XIM and XIC constructs provided by Xlib. If you need to provide text input in another widget, such as a DrawingArea, you have to handle opening an input method, creating an input context, and obtaining input from the input method yourself. If you have access to the source code, you may want to investigate these routines. The only danger is that because the routines are undocumented, they may change in the next release of Motif.

15.7 Summary

The Motif Text and TextField widgets can be used to provide an application with sophisticated text entry capabilities. The widgets come with a full set of convenience routines that make it easy to perform a number of standard text editing tasks. However, these widgets work best when they are left alone to do their jobs. While they are highly configurable, the little bits of fine tuning you add may cause your code to grow twice as much to accommodate the new features and the necessary error checking.

15.8 Exercises

The following exercises are designed to expand on the ideas described in this chapter and introduce some new directions for using Text widgets.


Contents Previous Next