Programming Tools: Running Doom

There is one essential item that should be in any NeXT programmer's armoury, and that is a working knowledge of how to fire up a Unix command to do some arbitrary processing. This is easy to do, but requires Unix programming techniques, and is easily missed by someone with a strict AppKit background.

To illustrate this, I'll walk through a simple, but fully functional and very useful, example application: a Doom runner. There are some example of this on the archives, but the one I have seen is much simpler, and doesn't come with source.

On the way to making this fully functional, there are a couple of extra techniques I'll have to show. One of these is how to activate and deactivate UI elements according to the state of other elements; and the other is how to program with PopUpLists. If you have tried to deal with PopUpLists before, you will know that they are the object from Hell; working with PopUps makes me think of the word "baroque", used mostly as in "baroque-n".

I also use a common combination of UI elements, a file well, text field and button, which I always incorporate in one form or another when a file name is required to be user selectable.

Setting the state of UI elements based on the setting of various elements (disabling menu cells, popups, etc) is a technique that makes your UI much more impressive, f used appropriately. Not enough applications make good use of it.

Finally, I associate a certain file type (in the case, Doom wad files) with my launcher app, and launch my app (and Doom) from a double-click on the data file.

Unix Commands

There are a number of ways of running Unix commands from a program, but some are much more desirable than others.

The commonest command found by people who need to just run a Unix command is system():

system("/bin/rm /tmp/O_*");

The return value is the shell return code. However, system() is also probably the least attractive of all these. Note that you should always use a full pathname for any command, as a basic security precaution, and make sure that all parameters are properly quoted against misinterpretation.

Sometimes you have to deal with data returned from a command, in which case a different command is appropriate.

FILE *output;
char c;

if (output = popen("/bin/ls -l", "r") != NULL) {
 while ((c = fgetc(output)) != EOF)
  putc(c, stdout);
 pclose(output);
} else {
 perror("Failed popen:");
}

This method has a disadvantage; your application blocks (generates a spinning wheel cursor) while the command is processing.

The more general technique, and the one most recommended, is to use the exec class of functions. The exec commands don't force the command into /bin/sh, and use an array of parameters, and so don't require quoting. This is what I use:

char *command[25]; // arbitrary size
int i = 0;

command[i++] = NXCopyStringBuffer([doomText stringValue]);
command[i] = 0;
if (!execv([doomText stringValue], command)) 
 perror("Failed execv");

doomText is a TextField object that contains the full path to the command I want to run, and command is an array of parameters, starting with the program name, and ending with a zero value entry.

This still has the disadvantage of blocking while the command runs. To allow my program to continue while Doom launches, we have to create a separate process to run Doom. This is easily done, once again by resorting to command Unix functions:

char *command[25]; // arbitrary size
int i = 0;
int pid;

command[i++] = NXCopyStringBuffer([doomText stringValue]);
command[i] = 0;
pid = fork();
if (pid == 0) {
 if (!execv([doomText stringValue], command)) 
  perror("Failed execv");
 exit(1);  // never gets here
}

fork() creates a new process. To execute some commands in that new process, we have to work out what is the original program, and what is the new process. The return code of fork() is zero if we are in the new process.

PopUp Lists

Popup lists are the NeXT programmers nightmare. It is inordinately complex to access the relevant components of a popup; indeed, NeXT didn't allow you to create them from Interface Builder until 2.0.

To use a Popup, you have to understand that it is a composite of several objects: a Button, a PopupList, and a Matrix of MenuCells. According to what you need to accomplish, you will probably have to reference several of these objects at the same time.

When building a Popup in Interface Builder, I always allocate tags to the various cells, and will attempt to use the tag in preference to the titles. Again, this allows changes in the nib file to be made without forcing you to make changes in the code.

Outlets set from Interface Builder will be either to the coover Button, or to individual MenuCells. Connecting to MenuCells isn't very helpful, as once again it adds complexity to code, and makes modifying the nib file more complex.

To find the Popup given the Button, useButton's target method. And to locate the Matrix of MenuCells, ask for the PopupList's itemList.

To check if the popup has been clicked on at all (and so if it is worth checking for further settings), check the state of the Button:

if ([popupButton state]) {
 ...

And to select programatically a particular cell, you need to refer to the Matrix:

[[[playerPopupButton target] itemList] selectCellWithTag:1];

Referencing Files

My favourite technique is to group together a TextField to hold the file name, a file well for the icon, and a button to trigger an Open Panel to select the file. The biggest problem is finding space to lay it out properly. Here is an example:

This is very easy to setup (unlike the two previous topics), and should be used where ever a file is required. Using the file well and text field is done purely in Interface Builder.

First, you need a copy of the DocumentIcon palette. This is in MiniExample 1242, DocumentPalette, available from NeXTanswers. Build it, and drop a file well onto your window. Add a TextField. Then link the two together with takeStringValueFrom:, and set an initial value. You will need to include the DBKit library to your Project, as DocumentIcon can also be used with DBKit. This gives you a simple file selector.

To add in the Open Panel, drop in a button, and connect it to a method in your controller:

- selectDoom:sender;
{
        id openPanel = [OpenPanel new];

    if ([openPanel runModal] == NX_OKTAG) {
 [doomText setStringValue:[openPanel filename]];
 [doomWell setStringValue:[openPanel filename]];
    }

    return self;
}

This ties in to two outlets that represent the Text Field and file well.

Controlling UI State

This is also rather simple. My general approach to an Inspector panel is to have a button that processes the results of all the settings on the panel, rather than a method for each individual change. I find this leads to slightly less complex code. Any objects that will change the state of the panel, I will cause to trigger a single state setting method.

The basic technique used is the send the setEnable message to the relevant objects. I check the settings by using either state (for Buttons) or selectedTag (for a Matrix). For example:

It is important to set the initial state of the interface, which is why I use this catch all method:

- changeMode:sender;
{
    switch ([modeButtons selectedTag]) {
    case 0:
 [playerPopup setEnabled:NO];
 [hostMatrix setEnabled:NO];
 break;
    default:
 [playerPopup setEnabled:YES];
 [hostMatrix setEnabled:YES];
    }
    if ([savedgameSwitch state])
 [savedgamePopup setEnabled:YES];
    else
 [savedgamePopup setEnabled:NO];
 
    return self;
}

This allows changes in the nib file to result in the correct UI settings at startup:

- awakeFromNib;
{
    [self changeMode:self];
    return self;
}

Associating with Files

I really want to autolaunch my app when a new Doom wad file is double-clicked, which makes using the many free wads simple to use. To do this, you must first add in the wad file type, and an associated icon, in Project Builder.

To make it work in your code, you must implement two methods in the app's delegate: appAcceptsAnotherFile: and app:openFile:type:. If you forget to make the controller object the app delegate, this won't work.

First appAcceptsAnotherFile:, which is a simple override:

-(BOOL)appAcceptsAnotherFile:sender
{
    return YES;
}

Then app:openFile:type:, which is also very simple:

-(int)app:sender openFile:(const char *)filename type:(const char *)aType
{
    [self runDoom];
    return YES;
}

If you would like a copy of the fully formed app: DoomRun.

Paul_Lynch@plsys.co.uk

Designed by Paul Lynch

using WebPages 1.7

Last updated: Oct 10, 1995