Background

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.

Overview

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.

Implementation

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.

Reusable Functions

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.

Finding the Window

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);
}

Sending Events

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;
}

Other Window Operations

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

A Simple Demonstration

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.

Why no XtAppMainLoop()?

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.

Finding the Process ID

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:
  • Call the function pid when the user clicks the Pid button.
  • Create an atom with the property string WM_VEEPID. This atom will hold the HP VEE process identifier if the rest of the protocol succeeds.
  • Call XConvertSelection, specifying the atom that VEE attaches to its window. Tell the system that you want the result stored in the atom identified by the property string WM_VEEPID in integer format.
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.
  • When HP VEE calls XChangeProperty, the system responds by sending a PropertyNotify event to the window that owns the newly-changed atom.
  • Our example responds to this event by calling the function veePid. This function makes a call to XGetWindowProperty, specifying that it wants to get a value from the atom we created in the pid function.
  • At this point the atom contains the process identifier for the VEE instance that created the window.

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;
}

Library Sources, Example Files, and the Pre-built Library

Application Sources

Implementation File

Header File

Make File for HP-UX 9.x

Library Source

Implementation File

Debugging the Shared Library

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.

Using and Redistributing this 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:

References