The motivation for writing this paper came from a customer
request to provide a way for a process to programmatically push
VEE's Run button. At first, I didn't think this request
fell into the 'reasonable' category. After discussing it, though,
I came to realize that there are a few situations where having
programmatic control over some of a window's behavior is a pretty
handy capability. The result is a library of functions that allow
you to exercise some degree of control over an HP VEE window.
While the library may provide some value in and of itself, the
far greater value is in its illustration of the techniques you
can use to define your own behaviors.
The remainder of this paper presents the techniques used in building a library capable of customizing the behavior of an X window. We present the behavior-altering library along with a very simple Motif client program that exercises our library.
There are two fundamental issues to resolve when trying to
build a library that communicates with an X window:
Identifying an arbitrary window can be difficult, especially
if window characteristics, like the title bar text, tend to be
defined dynamically, as they are in the case of HP VEE. Fortunately,
HP VEE stores an asserts ownership of an atom that lets us unambiguously
identify the main window.
Sending window events to press VEE's controls consists of manufacturing
keystroke events that mimic the actions of an interactive user
pressing the keyboard accelerators associated with each control.
Our library supplies functions to manufacture keystroke events
that mimic pressing:
The library also provides functions to hide, iconify, and restore the window and a function to get the process identifier for the instance of VEE that created the window.
We'll examine our window-interaction scheme by first explaining the development of a library of functions that encapsulates the totality of our capabilities. We then use this library to help us build a simple, interactive application to exercise our system.
Our library contains functions intended for use in client programs and for use by other functions within the library. This helps to keep our library self contained. For example, if a library function wishes to push VEE's Run button on behalf of a client program, the function must be able to identify the window that is the target of an event. Our library defines a function to do just that. We could have structured things such that the client program would need to call the window-identification function, then pass the window identifier to any subsequent window-manipulation function. That approach seems a little tedious. Instead, we have chosen to let functions whose intent is to expose higher-level capabilities to a client handle the details. There is nothing to prevent a client from calling an internal function, but pointing out that there are two different categories of functions in our library will help to better organize our explanation.
When an instance of HP VEE first starts, it creates and asserts
ownership of an atom identified by the property string WM_VEEWINDOW.
An atom is an index into a table of values that the X server maintains.
Each atom is identified by a property string. Many X Window programs
communicate by requesting that another window store a particular
piece of information, in a particular format, into the atom table.
One of the properties of an atom is that it has exactly zero
or one owner. By finding out which window owns the atom identified
by the string WM_VEEWINDOW, you have accomplished all that
is required to find the VEE window which last asserted its ownership
of the atom.
One implication using this method of window identification
is that you can only identify the last window that
had asserted ownership of the atom. Let's say that you started
two VEE programs. The second instance to start would be the last
to assert ownership of the atom. If the second instance were to
exit, while the first was still running, there would be no way
to identify the earlier-created VEE window.
The following function returns a Window identifier by
finding out who owns the atom. The call to XInternAtom
will return an atom if an atom identified by the given property
string already exists. XGetSelectionOwner returns the window
identifier for the VEE process which last asserted ownership of
the atom.
Window findVeeWin(aDisplay) Display *aDisplay;{ Window selectionOwner; if(!errorHandler){ XSetErrorHandler(xErrorHandler); } wmVeeWindow=XInternAtom(aDisplay, "WM_VEEWINDOW", True); if(!wmVeeWindow){ return((Window) 0); } selectionOwner=XGetSelectionOwner(aDisplay, wmVeeWindow); if(selectionOwner == (Window)None){ return((Window)0); } return(selectionOwner); }
We simulate the interactive manipulation of VEE controls by
sending keystroke events that represent the accelerators assigned
to each control. For instance, the accelerator keystroke pair
assigned to VEE's Run button is ctrl-g, so to programmatically
push it, we send a ctrl-g keystroke event to VEE's window.
The following function helps us construct a keystroke event. One point to mention here is that the aDisplay parameter refers to an X Window Display structure pointer. This structure is where the X server gets the information it needs to determine which physical screen to send events to. Our library only sends events to VEE windows running on the same screen as the client program. The library can be easily enhanced to support sending events to screens other than those which contain the client application.
Status sendKeyEvent(aDisplay, keySym, sendControl) Display *aDisplay; KeySym keySym; unsigned char sendControl;{ Window aWindow; XEvent anEvent; Status sendEventStatus; aWindow=findVeeWin(aDisplay); if(!aWindow){ return(0); } memset(&anEvent, '\0', sizeof(XEvent)); anEvent.type=KeyPress; anEvent.xkey.type=KeyPress; anEvent.xkey.send_event=True; anEvent.xkey.display=aDisplay; anEvent.xkey.window=aWindow; anEvent.xkey.root=RootWindow(aDisplay, 0); anEvent.xkey.subwindow=aWindow; anEvent.xkey.time=CurrentTime; anEvent.xkey.x=0; anEvent.xkey.y=0; anEvent.xkey.x_root=0; anEvent.xkey.y_root=0; if(sendControl){ anEvent.xkey.state=ControlMask; } else{ anEvent.xkey.state=0; } anEvent.xkey.keycode=XKeysymToKeycode(aDisplay, keySym); anEvent.xkey.same_screen=True; sendEventStatus=XSendEvent(aDisplay, aWindow, True, KeyPressMask, &anEvent); return(sendEventStatus); }
The keySym argument is an X Window KeySym value.
These are platform-independent key definitions identified through
manifest constants in the X Window header files. Using key symbols
this way allows X Windows to render the correct screen character
independent of the different scan codes generated by keyboards
on different platforms. The call to the function XKeysymToKeycode
translates the key symbol to a key code, which X Windows uses
to render a character.
The sendControl argument is a flag that tells the function
whether to press the control key coincident with the character
code we will send.
There are a number of functions intended to allow a client program to perform particular operations on a VEE window. Specifically:
The function: | Performs the following action: |
sendGoKeyEvent | Pushes VEE's Run button. |
sendPauseKeyEvent | Pushes VEE's Pause button. |
sendStepKeyEvent | Pushes VEE's Step Into button. |
sendContKeyEvent | Pushes VEE's Run button, which happens to be the same control as the Continue button. |
sendF1KeyEvent | Presses the F1 key. This function can be expanded to send any key stroke. |
The first four of these functions look very much alike, so
we will only describe the first and refer you to the
source code for the rest. Each of these first four functions
works by:
Each action has an associated enumerated value as listed below:
enum VeeOperation{ vOperNotDefined = -1, vRun, vPause, vStepInto, vContinue };
The table of operations is an array of structures which associates the operation we want to perform with the key symbol and control key information for that operation. The table is listed below:
struct{ enum VeeOperation op; KeySym keySym; unsigned char sendControl :1; } VeeOperationStruct[]={ {vRun, XK_g, 1}, {vPause, XK_p, 1}, {vStepInto, XK_t, 1}, {vOperNotDefined, 0, 0} };
The function tableIndexForOperation returns an index
into the array of structures corresponding to the entry which
defines the key symbol and control key information for a given
operation. For instance, the sendGoKeyEvent function calls
tableIndexForOperation, passing it the enumerated value
vRun. sendGoKeyEvent uses the returned table index
to retrieve the appropriate key symbol and control key information,
which it uses in a call to sendKeyEvent. These two functions
are listed below:
int tableIndexForOperation(veeOp) enum VeeOperation veeOp;{ int returnVal = -1; int index=0; while(VeeOperationStruct[index].op != vOperNotDefined){ if(VeeOperationStruct[index].op == veeOp){ returnVal=index; break; } index++; } return returnVal; } Status sendGoKeyEvent(aDisplay) Display *aDisplay;{ int tableIndex; tableIndex=tableIndexForOperation(vRun); if(tableIndex >= 0){ return(sendKeyEvent(aDisplay, VeeOperationStruct[tableIndex].keySym, VeeOperationStruct[tableIndex].sendControl)); } return 0; }
Our library provides some other functions, again intended to expose a particular capability to a client, to control the window VEE runs in. Specifically:
The function: | Performs the following action: | By making the following X call: |
hideWindow | Removes VEE's window and its associated icon from the workspace. | XUnmapWindow |
iconizeWindow | Removes VEE's window from the workspace and displays its icon window. | XIconifyWindow |
restoreWindow | Shows VEE's window in its last known location. | XMapRaised |
Our example program, used to demonstrate
and test the capabilities in our library, is shown in the figure
at left. We create a set of buttons that, except for the Pid
button, invokes an associated function in our library.
This is a pretty mundane Motif application, until you get to
the button that gets the process identifier for the instance of
VEE that started the window we have been working with. A miniature
communications protocol (described in the next section) is necessary
for us to associate a process identifier with the window identifier.
Part of that protocol involves our example application receiving
PropertyNotify events.
To properly handle these events, we replaced the call to XtAppMainLoop
you would normally expect to see in a Motif application with the
code snippet shown below.
/* Set ourselves up to receive PropertyNotify events. This will happen when VEE puts its process id in our wmPid atom. */ XSelectInput(XtDisplay(topLevel), XtWindow(topLevel), PropertyChangeMask); /* Loop, reading and dispatching events. When we detect a PropertyNotify event, indicating that VEE has stored its process id in our atom, dispatch that event. */ for(;;){ XtAppNextEvent(app, &event); if(!XtDispatchEvent(&event) && (event.type == PropertyNotify)){ veePid(&event); } }
The call to XSelectInput informs X Windows that our application wants to be notified when a property we own gets changed. The loop containing the call to XtAppNextEvent allows us to control our response to a PropertyNotify event.
The following table shows the steps involved with getting the process identifier for the VEE instance that created a given window.
Example program: | HP VEE: |
|
|
VEE responds to the XConvertSelection call by putting its process identifier in the atom our example program created. It does this using a call to XChangeProperty. | |
|
The pid and veePid functions are listed below.
void pid(widget, clientData, cbs) Widget widget; XtPointer clientData; XmPushButtonCallbackStruct *cbs;{ Window appWindow; Window aWindow; aWindow=findVeeWin(XtDisplay(widget)); if(!aWindow){ return; } appWindow=(Window)clientData; if(!wmPid){ wmPid=XInternAtom(XtDisplay(widget), "WM_VEEPID", False); } if(wmVeeWindow){ XConvertSelection(XtDisplay(widget), wmVeeWindow, XA_INTEGER, wmPid, appWindow, XtLastTimestampProcessed(XtDisplay(widget))); } return; } void veePid(anEvent) XEvent *anEvent;{ int actualFormat; Atom actualType; unsigned long nItems; unsigned long bytesAfter; unsigned char *propValue; int getPropResult; pid_t aPid; if(anEvent->xproperty.atom != wmPid){ return; } getPropResult=XGetWindowProperty(anEvent->xproperty.display, anEvent->xproperty.window, wmPid, 0, 1, True, AnyPropertyType, &actualType, &actualFormat, &nItems, &bytesAfter, &propValue); if(getPropResult != Success){ return; } if(bytesAfter != 0){ return; } if(actualType == XA_INTEGER){ aPid = * (pid_t *) propValue; printf("%ld\n", aPid); } return; }
The make file is intended to build the shared library and the
example application with debugging information, so you can step
through and see how things work. To debug our application and
the code in a shared library, execute the command:
xdb -l libipcUtils.sl -s ipc
The "-l" instructs the debugger to attach the shared
library, making its symbols available for source-level debugging,
and the "-s" tells the debugger you want to debug a
shared library.
You may use this library in and the associated sources in any
way you see fit, as long as you adhere to two simple rules: