Making an SDDAS Compliant Application


written by Joey Mukherjee - joey@swri.org
Last Updated - April 28, 1999

Table of contents

  1. Overview
  2. Getting Started
  3. Step 1 - Include "libViewer.h"
  4. Step 2 - AppInfoCreate / AppInfoDestroy
  5. Step 3 - Miscellaneous Client Routines
  6. Step 4 - AppMessageContextOpen / AppMessageContextClose
  7. Step 5 - MsgSendStatus
  8. Step 6 - MapXGraphics
  9. Step 7 - MsgStateSend
  10. Step 8 - Viewer Calls
  11. Helper Routines
  12. Linking libViewer
  13. Adding to the Viewer Application List

Overview

Making an SDDAS compliant application means you will write code in your program which will allow our programs to "talk" to your program. When we tell your application to do something, your application will comply with the request and in some cases, return a status. The other aspect of an SDDAS compliant application is that any graphics that you draw will be drawn onto a window we will provide. If you have such an application, it can be run under the viewer (sdview) and will work seamlessly with our other applications.

Getting Started...

First, get your application working in "stand-alone" mode. It is fairly straight forward to get an application to be SDDAS compliant after the application is complete.

Next, make sure your program can do the following:

  1. Load a file from the command line.
  2. Minimize or iconify itself.
  3. Can print to an encapsulated postscript file.

After that, its just a series of steps to add your program to the list of SDDAS compliant apps!

Step One - Include "libViewer.h"

In your main program for sure, add an "#include "libViewer.h" which has all the prototypes and data structures used for viewer integration. In all the modules in which you use any of the routines listed below, make sure to add this include file.

Step Two - AppInfoCreate / AppInfoDestroy

The first thing you will need is an AppInfo structure. Since this structure must be accessed from a variety of places, I encourage you to make it global or very easily accessable.

Somewhere in the beginning of your program, create an AppInfo structure with the AppInfoCreate routine. The prototype is as follows:

     AppInfo *AppInfoCreate (int *argcp, char *argv[]);

It takes the command line arguments given to your program, and puts them into a data structure which is then accessible from other routines which will help with viewer integration. Notice that it takes pointers for its arguments. The reason is that it will remove the arguments which it recognizes and leave the rest alone for you to deal with. An example call:

    global_app_info = AppInfoCreate (&argc, argv);

You can also do an AppInfoDestroy when you quit your program. It will delete the structure created by AppInfoCreate.

One important thing gained by AppInfoCreate is the ability to know whether the program has been started in stand-alone mode or if it was started by the viewer. To check whether you are in stand-alone mode, use the macro AppAlone and send it the AppInfo structure. If the value is equal to 1, you are in stand-alone mode. If the value is zero, the viewer started you.

Example:


if (AppAlone (global_app_info)) global_stand_alone = sTrue;
    else global_stand_alone = sFalse;

Step Three - Routines that need to be done in the client program

If your program supports a GUI, for further viewer compliance, you need to add certain routines to your program to do the following things:

  1. Disable the close button on the window decoration.
  2. Add a minimize routine to minimize yourself on start up.
  3. Hide the file button.

Disabling the close button

The close button is not truely disabled, but is remapped to make your program minimize itself under the viewer. A routine is provided to help you disable the close button.

void DisableClose (Widget, XtCallbackProc, XtPointer);

The first parameter is a widget. You should send it the toplevel shell of your program. The second parameter is the name of a routine to call when the user double clicks on the program's Close button. The last parameter is something that will be passed to the second parameter's callback routine. It can be set to NULL.

For example, the following is taken from the Contour program:

This is in the main subroutine of the Contour program.

    DisableClose (contourShell, 
                  _mainDialog->MinimizeCallback, (XtPointer) _mainDialog);

This is in the main dialog window which controls the main part of the GUI.

void MainDialog::MinimizeCallback(Widget w,
                XtPointer clientData, XtPointer callData)
{
    MainDialog *obj = (MainDialog *) clientData;
    obj->Minimize ();
}

void
MainDialog::Minimize ()
{
    extern SDDAS_BOOL global_stand_alone;

    if (global_stand_alone == sTrue) {
        cout << "Exiting..." << endl;
        Quit ();
        exit (0);
    } else {
        XIconifyWindow (XtDisplay (_MainDialog), 
                        XtWindow (XtParent (_MainDialog)), 0);
    }
}

The first subroutine merely calls the second. It is needed for mixing C++ with Motif. The second subroutine is the main part of the routine. If I am in stand alone mode (i.e. outside the viewer), I call my Quit routine and then exit. If not, I just minimize myself using XIconifyWindow.

Add a minimize routine to minimize yourself on start up

The minimize routine above is also useful for minimizing yourself on startup, but only if the viewer tells you. The viewer will ask you to minimize yourself on startup if the user is opening a layout. If the user drops a box and then adds an application, the program will come up normally. This typically is what the user wants and will make the application do the right thing.

There is a macro called AppRunState which you can check to know whether or not to minimize yourself. Send it the AppInfo structure you created with AppInfoCreate.

Example code which might be directly applicable if you used the above routine:

if (AppRunState (global_app_info)) _mainDialog->Minimize ();

Hide the file button

The user should not be able to load a file, save a file, or quit while inside the viewer. This would put the viewer in a very unknown state. To prevent this, at the very least, unmanange the "File" button while in viewer mode.

Step Four - AppMessageContextOpen / AppMessageContextClose

This routine will enable the messaging mechanism between the viewer and your application. In theory, you can add an AppMessageContextClose to your program, but it will never be called since the viewer does not send a quit message; it just "kills" the program. Remember, only call the remaining routines if you are within the viewer.

The prototype for the AppMessageContextOpen is:

    void AppMessageContextOpen (XtAppContext app, void *data);

The XtAppContext is available when you first start your program. It is returned when you first call XtVaAppInitialize. The second parameter can be anything; however, what you send here will be passed to all routines which are messaged to you (i.e. Save, Restore, Print...).

An example call:

    AppMessageContextOpen (app, (void *) _mainDialog);

Step Five - MsgSendStatus

MsgSendStatus is used to tell the viewer that you are ready to receive more messages. The first message you send after doing the AppMessageContextOpen is MsgSendStatus (READY). When the viewer has received all READY messages from the applications it has started, it will switch the watch cursor to an arrow pointer.

The only thing you can send is a READY status, so do this immediately after the AppMessageContextOpen.

Example:

    AppMessageContextOpen (app, (void *) _mainDialog);
    MsgSendStatus (READY);

Step Six - MapXGraphics

If you are in the viewer (and you use our graphics package), you need to call MapXGraphics instead of init_graph. Send MapXGraphics the AppInfo you created when the program first started up with AppInfoCreate.

Example:

// If we are in the viewer, call MapXGraphics

    extern AppInfo *global_app_info;

    if (AppBoxWin (global_app_info) != 0) {
        MapXGraphics (global_app_info);
    } else

// Initialize the graphics package

        init_graph (global_layout->GetOutputDevice (), -1);

Step Seven - MsgStateSend

When a user changes a configuration item in the application, the viewer needs to know this so the user has the opportunity to save the changes. The way this is done is through the MsgStateSend procedure. Currently, there is only one state and that is "INCONSISTENT". Send "INCONSISTENT" as the parameter to MsgStateSend. This ought to be called everytime a user clicks "OK" in a dialog or somehow changes information in your program which ought to be saved.

Example:

    MsgStateSend (INCONSISTENT);

Step Eight - Viewer Calls

The viewer will make some calls to your program and these "callbacks" need to be handled. A callback is simply a procedure which you must write that follows our specifications. For convenience, a skeleton callback file is available which you can link into your program for quick viewer compliance.

The callbacks are as follows:

  1. void MenuAction (void *target);
  2. void CurrentPageAction (void *target);
  3. void NextPageAction (void *target);
  4. void SaveAction (void *target, char *filename, char *comments);
  5. void ResizeAction (void *target, int height, int width);
  6. void PrintAction (void *target, int dev_num, char *ext);

All callbacks take a "void *" as the first argument. This void * can be anything and you specify what it is in the AppMessageContextOpen routine.

MenuAction - must bring up your main menu.
CurrentPageAction - must draw the current page. Current page is defined as the standard draw command for your program.
NextPageAction - draws the next page for your application if your application supports paging.
SaveAction - saves the state of your application into the given filename. You must return to the viewer the data source(s) and the time you are using.
ResizeAction - if your application cares when its being resized, this callback will let you know the height and width of your window.
PrintAction - your application is being told to print on the given device number with the given extension (PC, EPS, GIF). You need to tell your application to print the contents of your window into a postscript file (or GIF image) and then issue a:

     MsgPrintSend (<gif/postscript output file>, getpid ()).

For printing, we generally like it to be done in the background. For this to happen, you must fork twice and make the grandchild do the printing. For example, after the first fork, save your layout into a temporary file and re-run your program in batch mode (your program can handle a batch mode, right?) with its output set to a graphics file (either encapsulated postscript or GIF).

Routines which can make your programming easier

We have some helper routines which can make your job a little easier. Make sure to include "libViewer.h" in your code if you use any of these routines.

When you honor a "Save" message from the viewer, You need to put the filename and the comments field (currently unused, but it may come back) into the AppInfo structure created by AppInfoCreate. To do this, call SaveActionHelper sending it the filename and the comments which are sent to you by the viewer in SaveAction.

Example:

    SaveActionHelper (global_app_info, filename, comments);
Another thing the application must do to honor a save message is return back to the viewer the source(s) and the time used by the application. The viewer will write this information into the layout file for use by SDBrowse. If you have only one source and one time, you may use the SaveViewerMessage routine. Send it pointers to the IdfsSource and the IdfsTimeStruct.

Example:

    IdfsSource *source;
    IdfsTimeStruct *time;             // these two variables are in your program
                                      // don't copy these directly!

    SaveViewerMessage (source, time);

Putting this all together, we offer this example pulled from the Contour program which has only one source and one time.

void SaveAction (void *target, char *filename, char *comments)
{
    extern LAYFile *global_layout;
    extern AppInfo *global_app_info;

    IdfsSource *src = &(global_layout->GetSource ()->GetIDFSSrc ())->virt;
    IdfsTimeStruct *time = global_layout->GetTime ()->GetIDFSTime ();

// Save the contents to our filename

    global_layout->Save (filename);

// Send the information back to the viewer

    SaveActionHelper (global_app_info, filename, comments);
    SaveViewerMessage (src, time);
}

If you have more than one source or do not have either of these structures accessible, you must build the message yourself. We have a helper routine for that as well: AddStringToString.

The prototype for AddStringToString is :

void AddStringToString (char *msg_string, char *tag, char *value, int int_value);

The msg_string is a string which you must allocate in your program. The tag is the identifier to use for the string. A tag is something which the browser program can look for and for now we only support the following tags:

  1. PROJECT
  2. SATELLITE
  3. EXPERIMENT
  4. INSTRUMENT
  5. VIRTUAL_INSTRUMENT
  6. START_TIME
  7. STOP_TIME

The value and int_value can be anything which is valid for those tags. If you do not need an int_value, use a "-1". It will not be included in the message.

After you have built a message, use MsgInfoSend to send the message.

The following is an example pulled from SpectroScalar which plots multiple sources along a single time :

void SaveAction (void *target, char *filename, char *comments)
{
    extern SSFile *global_ssfile;
    extern AppInfo *global_app_info;
    extern BaseObj *SS_base;

    SaveActionHelper (global_app_info, filename, comments);

// Stuff from old SS

     char message[10000];
     LinkList IdList;
     LinkList BoxList;
     LinkList PlotList;
     IdObj *Id;
     BoxObj *Box;                                                           
     PlotObj *Plot;

     memset(message, 0, sizeof (message));

     IdList = BaseIds(SS_base);

     while (IdList != NULL)
     {
        Id = (IdObj *) IdList->data;

        BoxList = IdBoxes(Id);

        while (BoxList != NULL)
        {
           Box = (BoxObj *) BoxList->data;
           PlotList = BoxPlots(Box);

           while (PlotList != NULL)
           {
              Plot = (PlotObj *) PlotList->data;
              IdfsSource *src = PlotSource (Plot);

              AddStringToString (message, "PROJECT",
                                 IdfsSourceProjectName (src), IdfsSourceProject (src));
              AddStringToString (message, "SATELLITE",
                                 IdfsSourceSatName (src), IdfsSourceSatellite (src));
              AddStringToString (message, "EXPERIMENT",
                                 IdfsSourceExpName (src), IdfsSourceExperiment (src));
              AddStringToString (message, "INSTRUMENT",
                                 IdfsSourceInstName (src), IdfsSourceInstrument (src));
              AddStringToString (message, "VIRTUAL_INSTRUMENT",
                                 IdfsSourceVirtInstName (src), IdfsSourceVirtInst (src));


              PlotList = PlotList->next;
           }
           BoxList = BoxList->next;
        }
        AddStringToString (message, "START_TIME",
                           IdfsStartTimeStr (Id), -1);
        AddStringToString (message, "STOP_TIME",
                           IdfsStopTimeStr (Id), -1);
        IdList = IdList->next;
     }
     MsgInfoSend (message);
}

Another routine we have a helper for is the print message. Printing is normally done in the background and if your program can print from a save file from the command line, the PrintActionHelper routine should work for you. Ideally, a program should spawn itself twice to properly have its children reaped by the parent process and not leave zombies. You, as the code author, are responsible for one of the forks. This routine will do the last one for you.

If you choose not to use PrintActionHelper, you must generate an output filename of <extension>.m<PID number>. This will be used by the viewer and renamed to something more appropriate.

PrintActionHelper takes any number of arguments, but they must be in a certain order. The first argument is the extension which you wish to have the program generate and the next argument is the number of arguments which follow. Every subsequent argument will be used to call your program in batch mode.

Example - this is pulled from the Contour program which takes on the command line the name of the file to print, the words "VIEWER_PRINT", and the device number, and the window id (its a weird Contour thing and probably not a standard need). The point is any arguments can be sent!.


PrintActionHelper (ext, 5, "Contour", new_fname, "VIEWER_PRINT", device, windowId);

Putting all this together, this last example is pulled from the Contour program which shows the fork and the call to PrintActionHelper.

void PrintAction (void *target, int dev_num, char *ext)
{
    extern LAYFile *global_layout;
    extern AppInfo *global_app_info;

// Make sure children don't turn into zombies

    no_zombie ();

// Fork off a new process

    pid_t pid = fork ();
    if (pid == -1) {
        cerr << "Error forking print process!" << endl;
        perror ("PrintAction");
        MsgPrintSend ("Nofile", getpid ());
        return;
    } else

// Parent process needs to return

        if (pid > 0) return;

// CHILD process
// Grandchild process which will do the printing

    char new_fname [100];

// Save temp file

    sprintf (new_fname, "/tmp/contour_%ld.CO", getpid ());
    global_layout->Save (new_fname);

    char device [4];
    sprintf (device, "%d", dev_num);

    char windowId [10];
    sprintf (windowId, "%ld", AppBoxWin (global_app_info));

    PrintActionHelper (ext, 5, "Contour", new_fname, "VIEWER_PRINT", device, windowId);
}

On a Resize action, you need to put the height and the width returned to you into the AppInfo. If you use our graphics package, you must also reset the graphics package. A caveat to this however. If you call init_graph before every draw, you do not need to do anything. To put the height and width into the AppInfo structure, use the macros AppBoxH and AppBoxW for the height and width respectively. To reset our graphics package, you may call SetupGraphics in the graphics package. We have a helper routine to do all this for you as well. The routine is ResizeActionHelper and you send it a pointer to the AppInfo structure as well as the height and width sent to you by the viewer. See the example code below.

Example 1 - A standard SDDAS graphics program will reset the graphics package and put the height and width into the AppInfo structure.

void ResizeAction (void *target, int height, int width)
{
    extern AppInfo *global_app_info;

    ResizeActionHelper (global_app_info, height, width);
}

Example 2 - Does not reset the graphics package, but does put the height and width into the AppInfo structure.

void ResizeAction (void *target, int height, int width)
{
    extern AppInfo *global_app_info;

    AppBoxH (global_app_info) = height;
    AppBoxW (global_app_info) = width;
}

Linking with libViewer

You must include libViewer into your link twice! You must add it before *AND after the routine/library of yours which has the routines needed for the callback.

Adding application to the Viewer Application List

The final step in viewer integration is to get your application to appear in the viewer application window when the user does the "Add Application" button from the right mouse menu.

Within the SDDAS_HOME/config/app directory are command files which the viewer uses to add items to the "Add Application" menu. The file must end in a .app extension and have the following items defined:

  1. MENU - the name which will appear on the "Add Application" menu. Must be a single word.
  2. COMMAND - the command to run. At the end of this line must be the reserved word $ARGS. The viewer must send certain X arguments to the application and this is done at the $ARGS place holder. You may add environment variables here with the $ notation (i.e. $SDFONT).
  3. SAVE - must be "message"
  4. PRINT - must be "message"
  5. EXTENSION - the extension that every saved file of this application will have.
  6. VIEWER - this can be a YES or a NO. If the value is YES, or this item is omitted, it will display in the viewer application window. If the value is NO, it will still be used by the sdbrowse program.

Example:


MENU Contour 
COMMAND Contour.MESA $ARGS
SAVE message
PRINT message
EXTENSION CO
VIEWER YES