X-Designer - The Leading X/Motif GUI Builder - Click to download a FREE evaluation |
In this chapter:
Chapter: 18
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 font1. There is no support for text formatting such as paragraph specifications, automatic line numbering, or indentation, so you cannot create WYSIWYG documents.2The 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 12, Labels and Buttons, and Chapter 13, 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. Figure 18-1 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.
Figure 18-1 An editor application with both Text and TextField widgets
The Text widget supports both single-line and multi-line 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. 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.
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.
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. 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.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 by default always in text-insertion mode. There is an action that puts the Text widget in overstrike mode if this is required.
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 non printable 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, and in this sense, the Motif Text widgets are completely modeless.
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.3In 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 Manual4(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 4, 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, 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 22, Drag and Drop, and Chapter 23, The Uniform Transfer Model.
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 21, The Clipboard, can inter-operate 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.
In Motif 2.0 and later, the Uniform Transfer Model as described in Chapter 23 hides some of the internal details of differences between the various methods of data transference from the programmer; note that the UTM does not, however, hide the differences between selection, the clipboard, or drag-and-drop from the user.
The reference pages for the Text and TextField widgets (in Volume 6B, Motif Reference Manual; Section 2, Motif and Xt Widget Classes) lists the default translations for the widgets. See Volume 4, X Toolkit Intrinsics Programming Manual, for a description of how to programmatically alter translation tables; see Volume 3, X Window System User's Guide, for a description of how a user can customize widget translations. See Chapter 21, The Clipboard, for a discussion of the lower-level Motif clipboard functions.
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 the following methods:To create a TextField widget instead, either specify the class as xmTextFieldWidgetClass in the XtVaCreateWidget() call, or use the Motif convenience routine XmCreateTextField().Widget text_w = XtVaCreateWidget ( "name", xmTextWidgetClass, parent, resource-value-list, NULL); Widget text_w = XmCreateText ( parent, "name", resource-value-array, resource-value-count);
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 Example 18-1.5
This short program simply creates a Text widget with the initial value as shown in Figure 18-2./* simple_text.c -- Create a minimally configured Text widget */ #include <Xm/Text.h> main (int argc, char *argv[]) { Widget toplevel; XtAppContext app; Widget text_w; Arg args[2]; XtSetLanguageProc (NULL, NULL, NULL); toplevel = XtVaOpenApplication (&app, "Demos", NULL, 0, &argc, argv, NULL, sessionShellWidgetClass, NULL); XtSetArg (args[0], XmNvalue, "Now is the time..."); text_w = XmCreateText (toplevel, "text", args, 1); XtManageChild (text_w); XtRealizeWidget (toplevel); XtAppMainLoop (app); }
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 the Text Widget Internationalization Section.
Specifying the Text
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: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.void XmTextSetString (Widget text_w, char *value)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. 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.
Retrieving the Text
You can access the textual data in a Text widget using XtVaGetValues() or XmTextGetString(). The function 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():XmTextGetString() works with both Text and TextField widgets, while the corresponding TextField routine, XmTextFieldGetString(), only works with TextField widgets. You can also use XmTextGetSubstring() to get a copy of a portion of the text in a Text widget.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); }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:
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.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);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.
Single and Multiple Lines
In Example 18-1, the Text widget provides a single-line text entry area that is 20 columns wide; it is shown in Figure 18-2. 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.6The 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.
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.7In 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:
This routine is not a variable-argument list function; it uses the argument-list style of setting resources with the XtSetArg() macro.Widget XmCreateScrolledText ( Widget parent, char *name, ArgList arglist, Cardinal argcount)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:
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.Widget scrolled_w, text_w; Arg args[6]; int n = 0; XtSetArg (args[n], XmNscrollingPolicy, XmAPPLICATION_DEFINED); n++; XtSetArg (args[n], XmNvisualPolicy, XmVARIABLE); n++; XtSetArg (args[n], XmNscrollBarDisplayPolicy, XmSTATIC); n++; XtSetArg (args[n], XmNshadowThickness, 0); n++; scrolled_w = XmCreateScrolledWindow (parent, "scrolled_w", args, n); n = 0; XtSetArg (args[n], XmNeditMode, XmMULTI_LINE_EDIT); n++; ... text_w = XmCreateText (scrolled_w, "text", args, n); ... XtManageChild (text); XtManageChild (scrolled_w);This creation method makes the programmer responsible for managing both of the widgets, as shown at the bottom of the fragment. 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.
Example 18-2 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.8
The output of the program is shown in Figure 18-3./* 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 (int argc, char *argv[]) { Widget top, main_w, menubar, menu, rc, text_w, file_w, label_w; XtAppContext app; XmString file, open, exit; void read_file(Widget, XtPointer, XtPointer); void file_cb(Widget, XtPointer, XtPointer); Arg args[10]; int n; XtSetLanguageProc (NULL, NULL, NULL); /* initialize toolkit and create toplevel shell */ top = XtVaOpenApplication (&app, "Demos", NULL, 0, &argc, argv, NULL, sessionShellWidgetClass, NULL); /* MainWindow for the application -- contains menubar ** and ScrolledText/Prompt/TextField as WorkWindow. */ main_w = XmCreateMainWindow (top, "main_w", NULL, 0); /* 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 = XmCreateRowColumn (main_w, "work_area", NULL, 0); n = 0; XtSetArg (args[n], XmNalignment, XmALIGNMENT_BEGINNING); n++; label_w = XmCreateLabelGadget (rc, "Filename:", args, n); XtManageChild (label_w); file_w = XmCreateTextField (rc, "text_field", NULL, 0); XtManageChild (file_w); /* 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, (XtPointer) text_w); XtManageChild (rc); /* Store the filename text widget to ScrolledText object */ XtVaSetValues (text_w, XmNuserData, file_w, NULL); /* Configure the Main Window layout */ XtVaSetValues (main_w, XmNmenuBar, menubar, XmNworkWindow, rc, NULL); XtManageChild (main_w); XtRealizeWidget (top); XtAppMainLoop (app); } void popdown_fsb (Widget fsb, XtPointer client_data, XtPointer call_data) { /* This calls the ChangeManaged() routine of the parent DialogShell ** which then internally calls XtPopdown */ XtUnmanageChild (fsb); } /* file_cb() -- "File" menu item was selected so popup a ** FileSelectionDialog. */ void file_cb (Widget widget, XtPointer client_data, XtPointer call_data) { static Widget dialog; Widget text_w; void read_file(Widget, XtPointer, XtPointer); 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, (XtPointer) text_w); XtAddCallback (dialog, XmNcancelCallback, popdown_fsb, NULL); } /* The DialogShell parent ChangeManage() calls XtPopup() internally */ XtManageChild (dialog); 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 widget, /* file selection 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; 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 */ filename = XmStringUnparse (cbs->value, XmFONTLIST_DEFAULT_TAG, XmCHARSET_TEXT, XmCHARSET_TEXT, NULL, 0, XmOUTPUT_ALL); /* 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 file\n", 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); (void) fclose (fp); return; } if (!fread (text, sizeof (char), statb.st_size + 1, fp)) fprintf (stderr, "Warning: may not have read entire file!\n"); 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); (void) fclose (fp); }
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 non editable 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.
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.
Line Wrapping and ScrollBar Placement
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.
Automatic Resizing
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.
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 Figure 18-3, 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:
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.void XmTextSetInsertionPosition (Widget text_w, XmTextPosition position)Example 18-3 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.9
In this example, the user can search for strings in a ScrolledText, as shown in Figure 18-4./* 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 (int argc, char *argv[]) { Widget toplevel, rowcol_v, rowcol_h, label_w; XtAppContext app; int i, n; void search_text(Widget, XtPointer, XtPointer); Arg args[10]; XtSetLanguageProc (NULL, NULL, NULL); toplevel = XtVaOpenApplication (&app, "Demos", NULL, 0, &argc, argv, NULL, sessionShellWidgetClass, NULL); rowcol_v = XmCreateRowColumn (toplevel, "rowcol_v", NULL, 0); XtSetArg (args[0], XmNorientation, XmHORIZONTAL); rowcol_h = XmCreateRowColumn (rowcol_v, "rowcol_h", args, 1); label_w = XmCreateLabelGadget (rowcol_h, "Search Pattern:", NULL, 0); XtManageChild (label_w); search_w = XmCreateTextField (rowcol_h, "search_text", NULL, 0); XtManageChild (search_w); XtManageChild (rowcol_h); n = 0; XtSetArg (args[n], XmNeditable, False); n++; XtSetArg (args[n], XmNcursorPositionVisible, False); n++; XtSetArg (args[n], XmNshadowThickness, 0); n++; XtSetArg (args[n], XmNhighlightThickness, 0); n++; text_output = XmCreateText (rowcol_v, "text_output", args, n); XtManageChild (text_output); 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 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- occurrence 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); }
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 Example 18-3 searches the text using various string routines. However, in Motif, there is a Text routine that provides the same functionality. XmTextFindString() searches a Text widget for a specified string. This routine takes the following form:
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(). Later in this chapter we implement a full text editor and use XmTextFindString() to handle the various search operations.Boolean XmTextFindString ( Widget text_w, XmTextPosition start, char *string, XmTextDirection direction, XmTextPosition *position)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:
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 XmTextShowPosition (Widget text_w, XmTextPosition position)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.void XmTextScroll (Widget text_w, int lines)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:
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.void XmTextReplace ( Widget text_w, XmTextPosition from_pos, XmTextPosition to_pos, char *value)To add search-and-replace functionality to the program in Example 18-3, we need to add a new TextField widget that prompts for the replacement text and provide a callback routine for the widget. Example 18-4 shows the additional code that is necessary.
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 later in this chapter.Widget text_w, search_w, replace_w, text_output; main (int argc, char *argv[]) { ... replace_w = XmCreateTextField (rowcol_h, "replace_text", NULL, 0); XtManageChild (replace_w); XtAddCallback (replace_w, XmNactivateCallback, search_and_replace, NULL); ... } void search_and_replace (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); }
Output-only Text
The Text and TextField widgets can be used in an output-only mode by setting the XmNeditable 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.
Example 18-5 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.
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>. vsprintf() is a varargs version of sprintf() that exists on most modern UNIX machines.10If your machine does not have vsprintf(), you can use _doprnt(): consult your system documentation for details if you do not have the standard C varargs package available. Whether using vsprintf() or _doprnt(), both of these functions consume all of the arguments in the list and leave the result in msgbuf.#include <stdio.h> #include <stdargs.h> /* or <varargs.h> */ /* global variable */ Widget text_output; main (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); .... } /*PRINTFLIKE1*/ void wprint (const char *fmt, ...) { char msgbuf[256]; static XmTextPosition wpr_position; va_list data; va_start (data, fmt); (void) vsprintf (msgbuf, fmt, data); 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); }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:
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.void XmTextInsert (Widget text_w, XmTextPosition position, char *string)The routines in Example 18-6 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.
Using the functions 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.extern void wprint(const char *fmt, ...); static void x_error (Display *dpy, XErrorEvent *err_event) { char buf[256]; XGetErrorText (dpy, err_event->error_code, buf, (sizeof buf)); wprint ("X Error: <%s>\n", buf); } static void xt_error (char *message) { wprint ("Xt Error: %s\n", message); } main (int argc, char *argv[]) { XtAppContext app; ... /* catch Xt errors */ XtAppSetErrorHandler (app, xt_error); XtAppSetWarningHandler (app, xt_error); /* and Xlib errors */ XSetErrorHandler (x_error); ... }
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 21, 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. Example 18-7 demonstrates these common editing actions. The application creates a MenuBar with an Edit PulldownMenu that contains actions that operate on the Text widget11
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 20, Interacting With the Window Manager. The output of the program is shown in Figure 18-5./* 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 (int argc, char *argv[]) { Widget toplevel, main_w, menubar, rowcol_v; XtAppContext app; void cut_paste(Widget, XtPointer, XtPointer); XmString label, cut, clear, copy, paste; Arg args[10]; int n; XtSetLanguageProc (NULL, NULL, NULL); toplevel = XtVaOpenApplication (&app, "Demos", NULL, 0, &argc, argv, NULL, sessionShellWidgetClass, NULL); main_w = XmCreateMainWindow (toplevel, "main_w", NULL, 0); /* 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 = XmCreateRowColumn (main_w, "rowcol_v", NULL, 0); n = 0; XtSetArg (args[n], XmNeditable, False); n++; XtSetArg (args[n], XmNcursorPositionVisible, False); n++; XtSetArg (args[n], XmNshadowThickness, False); n++; XtSetArg (args[n], XmNhighlightThickness, 0); n++; text_output = XmCreateText (rowcol_v, "text_output", args, n); XtManageChild (text_output); 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 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); /* FALLTHROUGH */ case 3 : XmTextClearSelection (text_w, when); break; } if (result == False) XmTextSetString (text_output, "There is no selection."); else XmTextSetString (text_output, NULL) }
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 PulldownMenu 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 routines to work with the clipboard: XmTextCut(), XmTextCopy(), XmTextPaste(), and XmTextClearSelection(). These routines take the following form:
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.12 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 Example 18-7. 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.Boolean XmTextCut (Widget text_w, Time time) Boolean XmTextCopy (Widget text_w, Time time) Boolean XmTextPaste (Widget text_w) void XmTextClearSelection (Widget text_w, Time time)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.
Getting the Selection
You can get the selected text from a Text widget using XmTextGetSelection(), which takes the following form: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.char *XmTextGetSelection (Widget text_w)XmTextGetSelectionPosition() provides information about the selected text in a Text widget. This routine takes the following form:
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.Boolean XmTextGetSelectionPosition ( Widget text_w, XmTextPosition *left, XmTextPosition *right)
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 Table 18-1.
These default actions can be modified using the XmNselectionArray and XmNselectionArrayCount resources. The XmNselectionArray resource specifies an array of XmTextScanType values, where XmTextScanType is an enumerated type defined as follows:
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:typedef enum { XmSELECT_POSITION, XmSELECT_WHITESPACE,13 XmSELECT_WORD, XmSELECT_LINE, XmSELECT_PARAGRAPH, XmSELECT_ALL } XmTextScanType;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_ALL };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 12, Labels and Buttons.static XmTextScanType sarray[] = { XmSELECT_POSITION, XmSELECT_WORD, XmSELECT_LINE, XmSELECT_PARAGRAPH, XmSELECT_ALL }; ... XtVaSetValues (text_w, XmNselectionArray, selectionArray, XmNselectionArrayCount, 5, NULL);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.
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 Figure 18-6; the code is shown in Example 18-8.14
/* 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 (int argc, char *argv[]) { XtAppContext app_context; Widget toplevel, main_window, menubar, form, search_panel, label_w; void file_cb(Widget, XtPointer, XtPointer); void edit_cb(Widget, XtPointer, XtPointer); void search_cb(Widget, XtPointer, XtPointer); 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 = XtVaOpenApplication (&app_context, "Demos", NULL, 0, &argc, argv, NULL,sessionShellWidgetClass, NULL); main_window = XmCreateMainWindow (toplevel, "main_window", NULL, 0); /* 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 = XmCreateForm (main_window, "form", NULL, 0); /* create horizontal RowColumn inside the form */ n = 0; XtSetArg (args[n], XmNorientation, XmHORIZONTAL); n++; XtSetArg (args[n], XmNpacking, XmPACK_TIGHT); n++; XtSetArg (args[n], XmNtopAttachment, XmATTACH_FORM); n++; XtSetArg (args[n], XmNleftAttachment, XmATTACH_FORM); n++; XtSetArg (args[n], XmNrightAttachment, XmATTACH_FORM); n++; search_panel = XmCreateRowColumn (form, "search_panel", args, n); /* Create two TextField widgets with Labels... */ label_w = XmCreateLabelGadget (search_panel, "Search Pattern:", NULL, 0); XtManageChild (label_w); search_text = XmCreateTextField (search_panel, "search_text", NULL, 0); XtManageChild (search_text); label_w = XmCreateLabelGadget (search_panel, "Replace Pattern:" NULL, 0); XtManageChild (label_w); replace_text = XmCreateTextField (search_panel, "replace_text", NULL, 0); XtManageChild (replace_text); XtManageChild (search_panel); n = 0; XtSetArg (args[n], XmNeditable, False); n++; XtSetArg (args[n], XmNcursorPositionVisible, False); n++; XtSetArg (args[n], XmNshadowThickness, 0); n++; XtSetArg (args[n], XmNleftAttachment, XmATTACH_FORM); n++; XtSetArg (args[n], XmNrightAttachment, XmATTACH_FORM); n++; XtSetArg (args[n], XmNbottomAttachment, XmATTACH_FORM); n++; text_output = XmCreateTextField (form, "text_output", args, n); XtManageChild (text_output); 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 (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; cbs = (XmFileSelectionBoxCallbackStruct *) call_data; if (!(filename = XmStringUnparse (cbs->value, XmFONTLIST_DEFAULT_TAG, XmCHARSET_TEXT, XmCHARSET_TEXT, NULL, 0, XmOUTPUT_ALL))) 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] != '\n') fputc ('\n', 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); (void) fclose (fp); XtUnmanageChild (dialog); } /* popdown_cb() -- callback routine for "Cancel" button. */ void popdown_cb (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 (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, (XtPointer) 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 (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 widget, XtPointer client_data, XtPointer call_data) { Boolean result = True; int reason = (int) client_data; XEvent *event; Time when; event = ((XmPushButtonCallbackStruct *) call_data)->event; 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); /* FALLTHROUGH */ case EDIT_CLEAR : XmTextClearSelection (text_edit, when); break; } if (result == False) XmTextSetString (text_output, "There is no selection."); }
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.
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 XmNactivateCallback receives the common XmAnyCallbackStruct as the call_data parameter to the function. The callback reason is always XmCR_ACTIVATE. Example 18-9 shows a callback function for some TextField widgets.15
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 Figure 18-7./* 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 (int argc, char *argv[]) { Widget toplevel, text_w, form, rowcol, label_w; XtAppContext app; int i; void print_result(Widget, XtPointer, XtPointer); Arg args[8]; int n; XtSetLanguageProc (NULL, NULL, NULL); toplevel = XtVaAppInitialize (&app, "Demos", NULL, 0, &argc, argv, NULL, sessionShellWidgetClass, NULL); rowcol = XmCreateRowColumn (toplevel, "rowcol", NULL, 0); for (i = 0; i < XtNumber (labels); i++) { n = 0; XtSetArg (args[n], XmNfractionBase, 10); n++; XtSetArg (args[n], XmNnavigationType, XmNONE); n++; form = XmCreateForm (rowcol, "form", args, n); n = 0; XtSetArg (args[n], XmNtopAttachment, XmATTACH_FORM); n++; XtSetArg (args[n], XmNbottomAttachment, XmATTACH_FORM); n++; XtSetArg (args[n], XmNleftAttachment, XmATTACH_FORM); n++; XtSetArg (args[n], XmNrightAttachment, XmATTACH_POSITION); n++; XtSetArg (args[n], XmNrightPosition, 3); n++; XtSetArg (args[n], XmNalignment, XmALIGNMENT_END); n++; XtSetArg (args[n], XmNnavigationType, XmNONE); n++; label_w = XmCreateLabelGadget (form, labels[i], args, n); XtManageChild (label_w); n = 0; XtSetArg (args[n], XmNtraversalOn, True); n++; XtSetArg (args[n], XmNleftAttachment, XmATTACH_POSITION); n++; XtSetArg (args[n], XmNleftPosition, 4); n++; XtSetArg (args[n], XmNrightAttachment, XmATTACH_FORM); n++; XtSetArg (args[n], XmNnavigationType, XmTAB_GROUP); n++; text_w = XmCreateTextField (form, "text_w", args, n); XtManageChild (text_w); /* When user hits return, print the label+value of text_w */ XtAddCallback (text_w, XmNactivateCallback, print_result, (XtPointer) labels[i]); XtManageChild (form); } XtManageChild (rowcol); XtRealizeWidget (toplevel); XtAppMainLoop (app); } /* print_result() -- callback for when the user hits return in the ** TextField widget. */ void print_result (Widget text_w, XtPointer client_data, XtPointer call_data) { char *value = XmTextFieldGetString (text_w); char *label = (char *) client_data; printf ("%s %s\n", label, value); XtFree (value); XmProcessTraversal (text_w, XmTRAVERSE_NEXT_TAB_GROUP); }
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 the Keyboard Traversal Section of Chapter 8 for more information on tab groups.
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.
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 Example 18-10, we demonstrate using this callback to convert text to uppercase.16
The program creates a RowColumn widget that contains a Label and a Text widget, as shown in Figure 18-8./* 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(Widget, XtPointer, XtPointer); main (int argc, char *argv[]) { Widget toplevel, text_w, rowcol, label_w; XtAppContext app; Arg args[2]; XtSetLanguageProc (NULL, NULL, NULL); toplevel = XtVaOpenApplication (&app, "Demos", NULL, 0, &argc, argv, NULL, sessionShellWidgetClass, NULL); XtSetArg (args[0], XmNorientation, XmHORIZONTAL); rowcol = XmCreateRowColumn (toplevel, "rowcol", args, 1); label_w = XmCreateLabelGadget (rowcol, "Enter Text:", NULL, 0); XtManageChild (label_w); text_w = XmCreateText (rowcol, "text_w", NULL, 0); XtManageChild (text_w); XtAddCallback (text_w, XmNmodifyVerifyCallback, allcaps, NULL); XtManageChild (rowcol); XtRealizeWidget (toplevel); XtAppMainLoop (app); } /* allcaps() -- convert inserted text to capital letters. */ void allcaps (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 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:
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 programmatic, for example, if the text is changed through a convenience function17. 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.typedef struct { int reason; XEvent *event; Boolean doit; XmTextPosition currInsert, newInsert; XmTextPosition startPos, endPos; XmTextBlock text; } XmTextVerifyCallbackStruct;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 up coming 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:
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.typedef struct { char *ptr; int length; XmTextFormat format; } XmTextBlockRec, *XmTextBlock;Let's review the simple case of adding new text, as demonstrated in Example 18-10. 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:
The islower() and toupper() macros are found in the <ctype.h> header file.for (len = 0; len < cbs->text->length; len++) if (islower (cbs->text->ptr[len])) cbs->text->ptr[len] = toupper (cbs->text->ptr[len]);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.
Preventing Text Modification
Example 18-10 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.
Example 18-11 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.
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./* check_zip() -- limit the user to entering a ZIP code. */ void check_zip (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; }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.
Handling Text Deletion
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. Example 18-12 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.18
As you can see in Figure 18-9, the Text widget only displays asterisks, no matter what the user has typed./* 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/TextF.h> #include <Xm/LabelG.h> #include <Xm/RowColumn.h> #include <ctype.h> void check_passwd(Widget, XtPointer, XtPointer); char *passwd = (char *) 0; /* store user-typed passwd here. */ main (int argc, char *argv[]) { Widget toplevel, text_w, label_w, rowcol; XtAppContext app; Arg args[2]; XtSetLanguageProc (NULL, NULL, NULL); toplevel = XtVaOpenApplication (&app, "Demos", NULL, 0, &argc, argv, NULL, sessionShellWidgetClass, NULL); XtSetArg (args[0], XmNorientation, XmHORIZONTAL); rowcol = XmCreateRowColumn (toplevel, "rowcol", args, 1); label_w = XmCreateLabelGadget (rowcol, "Password:", NULL, 0); XtManageChild (label_w); text_w = XmCreateTextField (rowcol, "text_w", NULL, 0); XtManageChild (text_w); 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 (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: %s\n", 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] = '*'; }
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.
Extending Text Modification
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: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. Table 18-2 shows how the interaction should work.123-456-7890
Table 18-2 Phone Number Input Interaction User Types
Text Widget Displays
4
4
1
41
5
415-
4
415-4
BACKSPACE
415-
BACKSPACE
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. Example 18-13 shows the program that implements this functionality.19
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./* 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(Widget, XtPointer, XtPointer); main (int argc, char *argv[]) { Widget toplevel, text_w, label_w, rowcol; XtAppContext app; Arg args[2]; XtSetLanguageProc (NULL, NULL, NULL); toplevel = XtVaOpenApplication (&app, "Demos", NULL, 0, &argc, argv, NULL, sessionShellWidgetClass, NULL); XtSetArg (args[0], XmNorientation, XmHORIZONTAL); rowcol = XmCreateRowColumn (toplevel, "rowcol", args, 1); label_w = XmCreateLabelGadget (rowcol, "Phone Number:", NULL, 0); XtManageChild (label_w); text_w = XmCreateText (rowcol, "text_w", NULL, 0); XtManageChild (text_w); XtAddCallback (text_w, XmNmodifyVerifyCallback, check_phone, NULL); XtManageChild (rowcol); XtRealizeWidget (toplevel); XtAppMainLoop (app); } /* check_phone() -- handle phone number input. */ void check_phone (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] = '-'; } }As a result, the only way to handle the situation is to actually add the dashes in the XmNmodifyVerifyCallback 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. 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.
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. Example 18-14 shows a new version of the check_phone() routine that prevents cursor movement.
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.main (int argc, char *argv[]) { Widget text_w; ... XtAddCallback (text_w, XmNmotionVerifyCallback, check_phone, NULL); ... } /* check_phone() -- handle phone number input. */ void check_phone (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 or typing in the middle of string */ if (cbs->currInsert < len) { cbs->doit = False; return; } ... }
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:The XmNfocusCallback is passed a callback structure of type XmAnyCallbackStruct with the callback reason set to XmCR_FOCUS.{ Widget text_w; void gain_focus(Widget, XtPointer, XtPointer); ... text_w = XmCreateScrolledText(...); XtAddCallback (text_w, XmNfocusCallback, gain_focus, NULL); ... } void gain_focus (Widget text_w, XtPointer client_data, XtPointer call_data) { XmTextShowCursorPosition (text_w, XmTextGetCursorPosition (text_w)); }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.
Text Widget Internationalization
In Motif, the Text and TextField widgets support internationalized input and output. The internationalization capabilities of the widgets are layered on top of the functionality originally 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 Chapter 2, The Motif Programming Model. See Volume 4, X Toolkit Intrinsics Programming Manual for more information on the localization of an Xt-based application.
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 non uniform 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 X 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 1, Xlib Programming Manual.
The Motif Text and TextField widgets provide two resources for specifying their textual data: XmNvalue andXmNvalueWcs. 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. XmNvalueWcs 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:
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 6B, Motif Reference Manual.Boolean XmTextFindStringWcs ( Widget widget, XmTextPosition start, wchar_t *wc, XmTextDirection dir, XmTextPosition *pos) wchar_t *XmTextGetSelectionWcs (Widget widget) wchar_t *XmTextGetStringWcs (Widget widget) int XmTextGetSubstringWcs ( Widget widget, XmTextPosition start, int num_chars, int buf_size, wchar_t *buffer) void XmTextInsertWcs ( Widget widget, XmTextPosition position, wchar_t *wc) void XmTextReplaceWcs ( Widget widget, XmTextPosition from, XmTextPosition to, wchar_t *wc) void XmTextSetStringWcs (Widget widget, wchar_t *wc)The widgets also provide a wide-character version of the text modification callback, XmNmodifyVerifyCallbackWcs. 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:
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 { int reason; XEvent *event; Boolean doit; XmTextPosition currInsert, newInsert; XmTextPosition startPos, endPos; XmTextBlockWcs text; } XmTextVerifyCallbackStructWcs;If callback routines are registered for both the XmNmodifyVerifyCallback and the XmNmodifyVerifyCallbackWcs, the routines for the XmNmodifyVerifyCallback are invoked first. The resulting data, which may have been modified, is passed to the XmNmodifyVerifyCallbackWcs routines.typedef struct { wchar_t *wcsptr; int length; } XmTextBlockRecWcs, *XmTextBlockWcs;
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 1, Xlib Programming Manual, for more information on internationalized text output.Each of the widgets has a XmNrenderTable resource for specifying the font that it uses.20 Since the widgets do not use compound strings, they cannot use font list tags to display text using different fonts as described in Chapter 25, Compound Strings. However, the render table can specify a rendition which contains 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 render table for a rendition that has the tag XmFONTLIST_DEFAULT_TAG. If the search finds such a rendition that contains a font set, it is used. Otherwise, the widgets use the first font set specified in the font list. If the render table does not contain a font set, the first font is used. If you specify a rendition entry with the tag XmFONTLIST_DEFAULT_TAG, make sure that it is appropriate for the encoding of the current locale.21
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 lower case and upper case 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 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:
The status area is an output-only window that displays information about the state of the input method interaction.
The pre-edit area displays the intermediate text while the user is composing a character.
The auxiliary area is used to display any dialog boxes or popup menus that are needed by the input method. 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:
The root-window style, where the input method displays the pre-edit data in a window that is a child of the root window.
The off-the-spot style, in which the input method displays the data at a fixed location in the application window, often at the bottom of the window.
The over-the-spot style, where the input method displays the data in a window of its own that is placed over the current insertion point.
The on-the-spot style, in which the input method directs the application to display the pre-edit data, so the application can display the data however it wants. 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 1, 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 X11. X11R5 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 standardized the input method implementation, allowing for dynamic input method server connectivity. The details of this are beyond the scope of this manual; you are referred to the Programmer's Supplement for Release 6 of Volumes 1, 2, 4, and 5 for a complete discussion of the topic.
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, XmNpreeditType, and XmNinputPolicy resources for specifying the input method, the interaction style, and the input context creation policy respectively22. 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. The XmNinputPolicy resource specifies whether an input context is to be created on a per-shell basis (XmPER_SHELL) or for each widget which requests connection to an input method (XmPER_WIDGET).
In Motif 1.2, only the over-the-spot, off-the-spot, and root-window interaction styles are supported. Motif 2.0 also supports the on-the-spot style. 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 public routines:23
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. A description of each is found in Volume 6B, Motif Reference Manual.void XmImCloseXIM (Widget widget) void XmImFreeXIC (Widget widget, XIC xic) XIC XmImGetXIC (Widget widget, XmInputPolicy policy, ArgList argv, Cardinal argc) XIM XmImGetXIM (Widget widget) int XmImMbLookupString (Widget widget, XKeyPressedEvent *event, char *buffer, int num_bytes, KeySym *keysym, int *status) void XmImMbResetIC (Widget widget, char **mb_text) void XmImRegister (Widget widget, unsigned int reserved) void XmImSetFocusValues (Widget widget, ArgList argv, Cardinal argc) void XmImSetValues (Widget widget, ArgList argv, Cardinal argc) XIC XmImSetXIC (Widget widget, XIC xic) void XmImUnregister (Widget widget) void XmImUnsetFocus (Widget widget) void XmImVaSetFocusValues (Widget widget, resource-value-list, NULL) void XmImVaSetValues (Widget widget, resource-value-list, NULL)
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.
Exercises
The following exercises are designed to expand on the ideas described in this chapter and introduce some new directions for using Text widgets.
- Using the XmNmodifyVerifyCallback, you can add more data to a Text widget than what is typed by the user. This technique is useful for supporting advanced editing features such as file or word completion. The user should be able to enter the leading part of a word and then type a special character that completes the word automatically, based on a predefined list of words in /usr/dict/words. Write an XmNmodifyVerifyCallback routine that checks each character that is typed and, upon receipt of the special character, looks backwards in the text until it finds whitespace and checks this word against the words in the list. If there is a match, modify the text to complete the word.
- The function XmTextSetHighlight() can be used to highlight text in the same fashion as if the user had selected it. This routine is useful for emphasizing different pieces of text. Based upon the previous exercise, write a simple spell-checker program. Use a PushButton or a menu item to get all of the text from a Text widget and check the words against /usr/dict/words. Highlight all of the words that are not found in the dictionary so that the user can find them quickly.
- Modify the allcaps.c program to use the XmNgainFocusCallback and XmNlosingFocusCallback callback routines. When the widget loses the focus, all of the characters should be converted to lower case, and when the input focus is gained, the characters should revert to upper case.
- The XmNsource resource specifies an XmTextSource, which is an internal object that contains all of the information about the text in a Text widget. You can set or get the value for this resource using XtVaSetValues() and XtVaGetValues(). Since the data type is opaque to the programmer, you cannot create your own source, but you can get one from an existing Text widget. By getting the XmNsource from one Text widget and setting it in another, you can have two Text widgets that edit the same text. Write a program that does just that.
1 The abortive compound string Text widget, CSText, was introduced in Motif 2.0. This did support multi-font capability, but it had serious performance problems; it was excised from Motif 2.1.2 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.
3 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.
4 Reprinted as Appendix L in Volume Zero, X Protocol Reference Manual.
5 XtVaAppInitialize() is considered deprecated in X11R6.
6 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.
7 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.
8 XtVaAppInitialize() is considered deprecated in X11R6. XmStringGetLtoR() and XmMainWindowSetAreas () are considered deprecated in Motif 2.0 and later.
9 XtVaAppInitialize() is considered deprecated in X11R6.
10 System V has vsprintf(), as does SunOS, but older Ultrix and BSD machines may use _doprnt().
11 XtVaAppInitialize() is considered deprecated in X11R6.
12 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 20, Interacting With the Window Manager, for more information on window manager protocols.
13 XmSELECT_WHITESPACE works in the same way as XmSELECT_WORD.
14 XtVaAppInitialize() is considered deprecated in X11R6. XmStringGetLtoR() is deprecated in Motif 2.0 and later. XmRepTypeInstallTearOffModelConverter() is deprecated in Motif 2.0: the converter is installed internally.
15 XtVaAppInitialize() is considered deprecated in X11R6.
16 XtVaAppInitialize() is considered deprecated in X11R6.
17 There is a persistent bug in the toolkit such that if the user pastes characters into a Text widget using the mouse, the event field is also NULL. It is therefore not possible to differentiate between a programmatic and mouse change of the Text contents by inspecting only the information contained within the callback data.
18 XtVaAppInitialize() is considered deprecated in X11R6.
19 XtVaAppInitialize() is considered deprecated in X11R6. Note that this program does not work using a TextField: due to a bug, deleting the dash character paints the text contents blank.
20 The XmNfontList resource is deprecated as of Motif 2.0.
21 This is different to the way in which compound strings are rendered: if a font or tag is absent when rendering a compound string, callbacks may be invoked. See Chapter 24, Render Tables, for more details.
22 XmNinputPolicy is only available from Motif 2.0 onwards.
23 XmImCloseXIM(), XmImFreeXIC(), XmImGetXIC(), XmImSetXIC() are only available from Motif 2.0 onwards.
|