ftp.nice.ch/pub/next/science/mathematics/HippoDraw.2.0.s.tar.gz#/HippoDraw/Hippo.bproj/Draw.subproj/DrawApp.m

This is DrawApp.m in view mode; [Download] [Up]

#import "draw.h"

const int DrawVersion = 39;	/* minor version of the program */

@implementation DrawApp : Object
/*
 * This class is used primarily to handle the opening of new documents
 * and other application-wide activity (such as responding to messages from
 * the tool palette).  It listens for requests from the Workspace Manager
 * to open a draw-format file as well as target/action messages from the
 * New and Open... menu items.  It also keeps the menus in sync by
 * fielding the menu items' updateActions.
 */

/* Private C functions used to implement methods in this class. */

static void initMenu(Menu *menu)
/*
 * Sets the updateAction for every menu item which sends to the
 * First Responder (i.e. their target is nil).  When autoupdate is on,
 * every event will be followed by an update of each of the menu items
 * which is visible.  This keep all unavailable menu items dimmed out
 * so that the user knows what options are available at any given time.
 * Returns the activate menu if is found in this menu.
 */ 
{
    int count;
    Matrix *matrix;
    MenuCell *cell;
    id matrixTarget, cellTarget;

    matrix = [menu itemList];
    matrixTarget = [matrix target];

    count = [matrix cellCount];
    while (count--) {
	cell = [matrix cellAt:count :0];
	cellTarget = [cell target];
	if (!matrixTarget && !cellTarget) {
	    [cell setUpdateAction:@selector(menuItemUpdate:) forMenu:menu];
	} else if ([cell hasSubmenu]) {
	    initMenu(cellTarget);
	}
    }
}

static DrawDocument *documentInWindow(Window *window)
/*
 * Checks to see if the passed window's delegate is a DrawDocument.
 * If it is, it returns that document, otherwise it returns nil.
 */
{
    id document = [window delegate];
    return [document isKindOf:[DrawDocument class]] ? document : nil;
}

static Window *findDocument(const char *name)
/*
 * Searches the window list looking for a DrawDocument with the specified name.
 * Returns the window containing the document if found.
 * If name == NULL then the first document found is returned.
 */
{
    int count;
    DrawDocument *document;
    Window *window;
    List *windowList;

    windowList = [NXApp windowList];
    count = [windowList count];
    while (count--) {
	window = [windowList objectAt:count];
	document = documentInWindow(window);
	if ([document isSameAs:name]) return window;
    }

    return nil;
}

static DrawDocument *openFile(const char *directory, const char *name, BOOL display)
/*
 * Opens a file with the given name in the specified directory.
 * If we already have that file open, it is ordered front.
 * Returns the document if successful, nil otherwise.
 */
{
    Window *window;
    char buffer[MAXPATHLEN+1], path[MAXPATHLEN+1];

    if (name && *name) {
	if (!directory) {
	    directory = ".";
	} else if (*directory != '/') {
	    strcpy(buffer, "./");
	    strcat(buffer, directory);
	    directory = buffer;
	}
	if (!chdir(directory) && getwd(path)) {
	    strcat(path, "/");
	    strcat(path, name);
	    window = findDocument(path);
	    if (window) {
		if (display) [window makeKeyAndOrderFront:window];
		return [window delegate];
	    } else {
		return [DrawDocument newFromFile:path andDisplay:display];
	    }
	} else {
	    NXRunLocalizedAlertPanel(NULL, "Open", "Invalid path: %s", NULL, NULL, NULL, directory, "Alert shown to user when he has tried to open up a file and has specified a file in a directory which is inaccessible.  The directory is either unreadable or does not exist for some reason.");
	}
    }

    return nil;
}

static DrawDocument *openDocument(const char *document, BOOL display)
{
    char *directory, *name, *ext;
    char buffer[MAXPATHLEN+1];

    strcpy(buffer, document);
    ext = strrchr(buffer, '.');
    if (!ext || strcmp(ext, ".hdraw")) strcat(buffer, ".hdraw");
    name = strrchr(buffer, '/');
    if (name) {
	*name++ = '\0';
	directory = buffer;
    } else {
	name = buffer;
	directory = NULL;
    }

    return openFile(directory, name, display);
}

/* Public methods */

+ initialize
/*
 * Initializes the defaults.
 */
{
    const NXDefaultsVector DrawDefaults = {
	{ "KnobWidth", "5" },
	{ "KnobHeight", "5" },
	{ "KeyMotionDelta", "1"},
	{ "Quit", NULL },
	{ "RemoteControl", "YES" },
	{ "HideCursorOnMove", NULL },
	{ NULL, NULL }
    };

    NXRegisterDefaults("Draw", DrawDefaults);

    return self;
}

- init
/*
 * setAutoupdate:YES means that updateWindows will be called after
 * every event is processed (this is how we keep our inspector and
 * our menu items up to date).
 */
{
    NXBundle	*bundle;
    char	buffer[MAXPATHLEN+1];
    
   [super init];
    bundle = [NXBundle bundleForClass:[self class]];
    if ( [bundle getPath:buffer forResource:"DrawApp" ofType:"nib"] ) {
    	[NXApp loadNibFile:buffer owner:self
                 withNames:YES  fromZone:[self zone]];
    }
    NXNameObject( "DrawInstance", self, NXApp );
    PSInit();
    [NXApp setAutoupdate:YES];
    return self;
}

/* General application status and information querying/modifying methods. */

- currentGraphic
/*
 * The current factory to use to create new Graphics.
 */
{
    return currentGraphic;
}

- (DrawDocument *)currentDocument
/*
 * The DrawDocument in the main window (dark gray title bar).
 */
{
    return documentInWindow([NXApp mainWindow]);
}

- (const char *)currentDirectory
/*
 * Directory where Draw is currently "working."
 */
{
    const char *cdir = [[self currentDocument] directory];
    return (cdir && *cdir) ? cdir : (haveOpenedDocument ? [[OpenPanel new] directory] : NXHomeDirectory());
}

/*
 * Call these to enter/exit the TextGraphic tool.
 * These are used currently by the Undo architecture.
 */

- startEditMode
{
    [tools selectCellAt:1 :0];
    [tools sendAction];
    return self;
}

- endEditMode
{
    [tools selectCellAt:0 :0];
    [tools sendAction];
    return self;
}

/* Application-wide shared panels */

static const char *cleanTitle(const char *menuItem, char *realTitle)
/*
 * Just strips off trailing "..."'s.
 */
{
    int length = menuItem ? strlen(menuItem) : 0;

    if (length > 3 && !strcmp(menuItem+length-3, "...")) {
	strcpy(realTitle, menuItem);
	realTitle[length-3] = '\0';
	return realTitle;
    } else if (length > 1 && (menuItem[length-1] == '\274')) {
	strcpy(realTitle, menuItem);
	realTitle[length-1] = '\0';
	return realTitle;
    } else {
	return menuItem;
    }
}

- (SavePanel *)saveToPanel:sender
/*
 * Returns a SavePanel with the accessory view which allows the user to
 * pick which type of file she wants to save.  The title of the Panel is
 * set to whatever was on the menu item that brought it up (it is assumed
 * that sender is the menu that has the item in it that was chosen to
 * cause the action which requires the SavePanel to come up).
 */
{
    NXBundle	*bundle;
    char	buffer[MAXPATHLEN+1];
    char title[100];
    const char *theTitle;
    SavePanel *savepanel = [SavePanel new];

    if (!savePanelAccessory) {
	bundle = [NXBundle bundleForClass:[self class]];
	if ( [bundle getPath:buffer
	         forResource:"SavePanelAccessory" ofType:"nib"] ) {
	    [NXApp loadNibFile:buffer owner:self
			withNames:NO  fromZone:[savepanel zone]];
	}
	[spamatrix selectCellAt:0 :0];
    }
    theTitle = cleanTitle([[sender selectedCell] title], title);
    if (theTitle) [savepanel setTitle:theTitle];
    [savepanel setAccessoryView:savePanelAccessory];
    [ [self currentDocument] changeSaveType: spamatrix];
    return savepanel;
}

- (SavePanel *)saveAsPanel:sender
/*
 * Returns a regular SavePanel with "draw" as the required file type.
 */
{
    char title[100];
    const char *theTitle;
    SavePanel *savepanel = [SavePanel new];

    [savepanel setAccessoryView:nil];
    theTitle = cleanTitle([[sender selectedCell] title], title);
    if (theTitle) [savepanel setTitle:theTitle];
    [savepanel setRequiredFileType:"hdraw"];

    return savepanel;
}

- (GridView *)gridInspector
/*
 * Returns the application-wide inspector for a document's grid.
 * Note that if we haven't yet loaded the GridView panel, we do it.
 * The instance variable gridInspector is set in setGridInspector:
 * since it is set as an outlet of the owner (self, i.e. DrawApp).
 */
{
    NXBundle	*bundle;
    char	buffer[MAXPATHLEN+1];
    
    if (!gridInspector) {
	NXZone *zone = NXCreateChildZone([self zone], vm_page_size, vm_page_size, YES);
	NXNameZone(zone, "GridView");
	bundle = [NXBundle bundleForClass:[self class]];
	if ( [bundle getPath:buffer forResource:"GridView" ofType:"nib"] ) {
	    [NXApp loadNibFile:buffer owner:self
			withNames:NO  fromZone:zone];
	}
	NXMergeZone(zone);
    }
    return gridInspector;
}

- (Panel *)inspectorPanel
/*
 * Returns the application-wide inspector for Graphics.
 */
{
    NXBundle	*bundle;
    char	buffer[MAXPATHLEN+1];
    
    if (!inspectorPanel) {
	NXZone *zone = NXCreateChildZone([self zone], vm_page_size, vm_page_size, YES);
	NXNameZone(zone, "Inspector");
	bundle = [NXBundle bundleForClass:[self class]];
	if ( [bundle getPath:buffer
	         forResource:"InspectorPanel" ofType:"nib"] ) {
	    [NXApp loadNibFile:buffer owner:self
			withNames:NO  fromZone:zone];
	}
	[inspectorPanel setFrameAutosaveName:"Inspector"];
	[inspectorPanel setBecomeKeyOnlyIfNeeded:YES];
	[[inspectorPanel delegate] preset];
	NXMergeZone(zone);
    }
    return inspectorPanel;
}
- (Box *)contentBox
{
    if ( !inspectorPanel) {
        inspectorPanel = [self inspectorPanel];
    }
    return contentBox;
}
- (DrawPageLayout *)pageLayout
/*
 * Returns the application-wide DrawPageLayout panel.
 */
{
    static DrawPageLayout *dpl = nil;
    NXBundle	*bundle;
    char	buffer[MAXPATHLEN+1];

    if (!dpl) {
	dpl = [DrawPageLayout new];
	bundle = [NXBundle bundleForClass:[self class]];
	if ( [bundle getPath:buffer
	         forResource:"PageLayoutAccessory" ofType:"nib"] ) {
	    [NXApp loadNibFile:buffer owner:dpl
			withNames:NO  fromZone:[dpl zone]];
	}
    }

    return dpl;
}

- orderFrontInspectorPanel:sender
/*
 * Creates the inspector panel if it doesn't exist, then orders it front.
 */
{
    [[self inspectorPanel] orderFront:self];
    return self;
}

/* Setting up the Fax Cover Sheet menu */

#define FAX_NOTE NXLocalString("Notes", NULL, "Fax Cover Sheet item which allows the user to type in any thing he/she wants.")

- setFaxCoverSheetMenu:anObject
/*
 * This goes through all the entries in CoverSheet.strings and makes
 * an entry in the cover sheet menu for them.  This is kind of a kooky
 * method, but it makes Draw much more usable as a Fax Cover Sheet
 * editor.
 */
{
    Menu *fcsMenu;
    MenuCell *cell;
    const char *key, *value;
    NXStringTable *table;
    char path[MAXPATHLEN+1];

    if (fcsMenu = [anObject target]) {
	if ([[NXBundle mainBundle] getPath:path forResource:"CoverSheet" ofType:"strings"]) {
	    if (table = [[[NXStringTable allocFromZone:[self zone]] init] readFromFile:path]) {
		NXHashState state = [table initState];
		while ([table nextState:&state key:(void **)&key value:(void **)&value]) {
		    cell = [fcsMenu addItem:value action:@selector(addLocalizableCoverSheetEntry:) keyEquivalent:0];
		    [cell setAltTitle:key];
		}
		[table free];
	    }
	}
	[fcsMenu addItem:FAX_NOTE action:@selector(addCoverSheetEntry:) keyEquivalent:0];
    }

    return self;
}

/* Target/Action methods */

- info:sender
/*
 * Brings up the information panel.
 */
{
    NXBundle	*bundle;
    char	buffer[MAXPATHLEN+1];
    char buf[20];

    if (!infoPanel) {
	NXZone *zone = NXCreateChildZone([self zone], vm_page_size, vm_page_size, YES);
	NXNameZone(zone, "InfoPanel");
	bundle = [NXBundle bundleForClass:[self class]];
	if ( [bundle getPath:buffer forResource:"InfoPanel" ofType:"nib"] ) {
	    [NXApp loadNibFile:buffer owner:self
			withNames:NO  fromZone:zone];
	}
	[infoPanel setFrameAutosaveName:"InfoPanel"];
	sprintf(buf, "(v%2d)", DrawVersion);
	[version setStringValue:buf];
	NXMergeZone(zone);
    }

    [infoPanel orderFront:self];

    return self;
}

#define NO_HELP NXLocalString("Couldn't find any help!  Sorry ...", NULL, "Message given to the user when the help document could not be found.")

- help:sender
/*
 * Loads up the Help draw document.
 * Note the use of NXBundle so that the Help document can be localized.
 */
{
    DrawDocument *document = nil;
    char path[MAXPATHLEN+1];

    if ([[NXBundle mainBundle] getPath:path forResource:"Help" ofType:"draw"]) {
	if (document = openDocument(path, NO)) {
	    [document setTemporaryTitle:NXLocalString("Help", NULL, "The title of the help document.")];
	    [[[document view] window] makeKeyAndOrderFront:self];
	}
    }

    if (!document) NXRunAlertPanel(NULL, NO_HELP, NULL, NULL, NULL);

    return self;
}

- new:sender
/*
 * Creates a new document--called by pressing New in the Document menu.
 */
{
    [DrawDocument new];
    return self;
}

- open:sender
/*
 * Called by pressing Open... in the Window menu.
 */
{
    const char *directory;
    const char *const *files;
    char 	**myfiles;
    const char *const drawFileTypes[] = { "hdraw", "hdraw~", NULL };
    OpenPanel *openpanel = [[OpenPanel new] allowMultipleFiles:YES];
    int		i, nfiles;

    directory = [self currentDirectory];
    if (directory && (*directory == '/')) [openpanel setDirectory:directory];
    if ([openpanel runModalForTypes:drawFileTypes]) {
	files = [openpanel filenames];
	directory = [openpanel directory];
	nfiles = 0;
	while ( files && *files ) {
	    nfiles++;
	    files++;
	}
	files = [openpanel filenames];
	NX_MALLOC( myfiles, char *, nfiles );
	for ( i = 0; i < nfiles; i++ ) {
	    myfiles[i] = NXCopyStringBuffer(*files);
	    files++;
	}
	for ( i = 0; i < nfiles; i++ ) {
	    haveOpenedDocument = openFile(directory, myfiles[i], YES)
	                      || haveOpenedDocument;
	    NX_FREE( myfiles[i] );
	}
	NX_FREE( myfiles );
    }

    return self;
}

- saveAll:sender
/*
 * Saves all the documents.
 */
{
    int count;
    Window *window;
    id windowList;

    windowList = [NXApp windowList];
    count = [windowList count];
    while (count--) {
	window = [windowList objectAt:count];
	[documentInWindow(window) save:sender];
    }

    return nil;
}

#define UNSAVED_DOCUMENTS NXLocalString("You have unsaved documents.", NULL, "Message given to user when he tries to quit the application without saving all of his documents.")
#define REVIEW_UNSAVED NXLocalString("Review Unsaved", NULL, "Choice (on a button) given to user which allows him to review all his unsaved documents if he quits the application without saving them all first.")
#define QUIT_ANYWAY NXLocalString("Quit Anyway", NULL, "Choice (on a button) given to user which allows him to quit the application even though he has not saved all of his documents first.")
#define QUIT NXLocalString("Quit", NULL, "The operation of exiting the application.")

- terminate:sender cancellable:(BOOL)cancellable
/*
 * Makes sure all documents get an opportunity
 * to be saved before exiting the program.  If we are terminating because
 * the user logged out of the workspace (or powered off), then we cannot
 * give the user the option of cancelling the quit (that's what the
 * cancellable flag is for).
 */
{
    int count, choice;
    Window *window;
    id document;
    id windowList;

    windowList = [NXApp windowList];
    count = [windowList count];
    while (count--) {
	window = [windowList objectAt:count];
 	document = [window delegate];
	if ([document respondsTo:@selector(isDirty)] && [document isDirty]) {
	    if (cancellable) {
		choice = NXRunAlertPanel(QUIT, UNSAVED_DOCUMENTS, REVIEW_UNSAVED, QUIT_ANYWAY, CANCEL);
	    } else {
		choice = NXRunAlertPanel(QUIT, UNSAVED_DOCUMENTS, REVIEW_UNSAVED, QUIT_ANYWAY, NULL);
	    }
	    if (choice == NX_ALERTOTHER)  {
		return self;
	    } else if (choice == NX_ALERTDEFAULT) {
		count = [windowList count];
		while (count--) {
		    window = [windowList objectAt:count];
		    document = [window delegate];
		    if ([document respondsTo:@selector(windowWillClose:cancellable:)]) {
			if (![document windowWillClose:sender cancellable:cancellable] && cancellable) {
			    return self;
			} else {
			    [document close];
			    [window close];
			}
		    }
		}
	    }
	    break;
	}
    }

    return nil;
}

- terminate:sender
/*
 * Overridden to give user the opportunity to save unsaved files.
 */
{
    if (![self terminate:sender cancellable:YES]) {
	return [NXApp terminate:sender];
    } else {
	return self;
    }
}

/*
 * Application object delegate methods.
 * Since we don't have an application delegate, messages that would
 * normally be sent there are sent to the Application object itself instead.
 */

- appDidInit:(Application *)sender
/*
 * Makes the tool palette not ever become the key window.
 * Check for files to open specified on the command line.
 * Initialize the menus.
 * If there are no open documents (and we are not being
 * launched to service a Services request or otherwise
 * being invoked due to interapplication communication),
 * then open a blank one.
 */
{
    int i;
    Panel *toolWindow;

//    [[NXApp appListener ] setServicesDelegate:self];
    toolWindow = [tools window];
    [toolWindow setFrameAutosaveName:[toolWindow title]];
    [toolWindow setBecomeKeyOnlyIfNeeded:YES];
    [toolWindow setFloatingPanel:YES];
    [toolWindow orderFront:self];

    if (NXArgc > 1) {
	for (i = 1; i < NXArgc; i++) {
	    haveOpenedDocument = openDocument(NXArgv[i], YES) || haveOpenedDocument;
	}
    }

//     if (!haveOpenedDocument && !NXGetDefaultValue([NXApp appName], "NXServiceLaunch")) [self new:self];

    if (NXGetDefaultValue([NXApp appName], "Quit")) {
	[NXApp activateSelf:YES];
	NXPing();
	[self terminate:nil];
    }

    initMenu([NXApp mainMenu]);

    return self;
}

- (int)app:sender openFile:(const char *)path type:(const char *)type
/*
 * This method is performed whenever a user double-clicks on an icon
 * in the Workspace Manager representing a Draw program document.
 */
{
    if (type && !strcmp(type, "hdraw")) {
	if (openDocument(path, YES)) {
	    haveOpenedDocument = YES;
	    return YES;
	}
    }

    return NO;
}

- (BOOL)appAcceptsAnotherFile:(Application *)sender
/*
 * We accept any number of appOpenFile:type: messages.
 */
{
    return YES;
}

- app:sender powerOffIn:(int)ms andSave:(int)andSave
/*
 * Give the user a chance to save his documents.
 */
{
    if (andSave) [self terminate:nil cancellable:NO];
    return self;
}

/* Listener/Speaker remote methods */

/*
 * Note that anybody can send these messages to a running version
 * of Draw, so we protect the ones that can affect the current
 * document by only allowing them to happen if the Draw default
 * "RemoteControl" is set.
 */

- (int)msgDirectory:(const char **)fullPath ok:(int *)flag
{
    *fullPath = [self currentDirectory];
    if (*fullPath) {
	*flag = YES;
    } else {
	*fullPath = "";
	*flag = NO;
    }
    return 0;
}

- (int)msgVersion:(const char **)aString ok:(int *)flag
{
    char buf[20];

    sprintf(buf, "3.0 (v%2d)", DrawVersion);
    *aString = NXCopyStringBuffer(buf);
    *flag = YES;

    return 0;
}

- (int)msgFile:(const char **)fullPath ok:(int *)flag
{
    const char *file;

    file = [[self currentDocument] filename];
    if (file) {
	*fullPath = file;
	*flag = YES;
    } else {
	*fullPath = "";
	*flag = NO;
    }

    return 0;
}

- (BOOL)shouldRunPrintPanel:sender
/*
 * When NXApp prints, don't bring up the PrintPanel.
 */
{
    return NO;
}

- (int)msgPrint:(const char *)fullPath ok:(int *)flag
{
    BOOL close;
    id document = nil;

    if (NXGetDefaultValue("Draw", "RemoteControl")) {
	InMsgPrint = YES;
	if (document = [findDocument(fullPath) delegate]) {
	    close = NO;
	} else {
	    document = openDocument(fullPath, NO);
	    if (document) haveOpenedDocument = YES;
	    close = YES;
	}
	if (document && ![[document view] isEmpty]) {
	    [NXApp setPrintInfo:[document printInfo]];
	    [[document view] printPSCode:self];
	    if (close) [[[document view] window] close];
	    *flag = YES;
	} else {
	    *flag = NO;
	}
	InMsgPrint = NO;
    } else {
	*flag = NO;
    }

    return 0;
}

- (int)msgSelection:(const char **)bytes length:(int *)len asType:(const char *)aType ok:(int *)flag
{
    int maxlen;
    NXStream *stream;

    if (NXGetDefaultValue("Draw", "RemoteControl")) {
	if (!strcmp(aType, DrawPboardType)) {
	    stream = NXOpenMemory(NULL, 0, NX_WRITEONLY);
	    if ([[[self currentDocument] view] copySelectionToStream:stream]) {
		NXGetMemoryBuffer(stream, (char **)bytes, len, &maxlen);
		*flag = YES;
	    } else {
		*flag = NO;
	    }
	    NXClose(stream);
	} else if (!strcmp(aType, NXPostScriptPboardType)) {
	    stream = NXOpenMemory(NULL, 0, NX_WRITEONLY);
	    if ([[[self currentDocument] view] copySelectionAsPSToStream:stream]) {
		NXGetMemoryBuffer(stream, (char **)bytes, len, &maxlen);
		*flag = YES;
	    } else {
		*flag = NO;
	    }
	    NXClose(stream);
	} else if (!strcmp(aType, NXTIFFPboardType)) {
	    stream = NXOpenMemory(NULL, 0, NX_WRITEONLY);
	    if ([[[self currentDocument] view] copySelectionAsTIFFToStream:stream]) {
		NXGetMemoryBuffer(stream, (char **)bytes, len, &maxlen);
		*flag = YES;
	    } else {
		*flag = NO;
	    }
	    NXClose(stream);
	} else {
	    *flag = NO;
	}
    } else {
	*flag = NO;
    }
    
    if (!*flag) {
	*bytes = "";
	*len = 0;
    }

    return 0;
}

- (int)msgCopyAsType:(const char *)aType ok:(int *)flag
{
    if (NXGetDefaultValue("Draw", "RemoteControl")) {
	if (!strcmp(aType, NXPostScriptPboardType) ||
	    !strcmp(aType, NXTIFFPboardType) ||
	    !strcmp(aType, DrawPboardType)) {
	    *flag = ([[[self currentDocument] view] copy:self] ? YES : NO);
	} else {
	    *flag = NO;
	}
    } else {
	*flag = NO;
    }
    return 0;
}

- (int)msgCutAsType:(const char *)aType ok:(int *)flag
{
    if (NXGetDefaultValue("Draw", "RemoteControl")) {
	if (!strcmp(aType, NXPostScriptPboardType) ||
	    !strcmp(aType, NXTIFFPboardType) ||
	    !strcmp(aType, DrawPboardType)) {
	    *flag = ([[[self currentDocument] view] cut:self] ? YES : NO);
	} else {
	    *flag = NO;
	}
    } else {
	*flag = NO;
    }
    return 0;
}

- (int)msgPaste:(int *)flag;
{
    if (NXGetDefaultValue("Draw", "RemoteControl")) {
	*flag = ([[[self currentDocument] view] paste:self] ? YES : NO);
    } else {
	*flag = NO;
    }
    return 0;
}

- (int)msgQuit:(int *)flag
{
    if (NXGetDefaultValue("Draw", "RemoteControl")) {
	*flag = ([self terminate:self cancellable:YES] ? NO : YES);
	if (*flag) [NXApp perform:@selector(terminate:) with:nil afterDelay:1 cancelPrevious:YES];
    } else {
	*flag = NO;
    }
    return 0;
}

/* Global cursor setting */

- cursor
/*
 * This is called by DrawDocument objects who want to set the cursor
 * depending on what the currently selected tool is (as well as on whether
 * the Control key has been pressed indicating that the select tool is
 * temporarily set--see sendEvent:).
 */
{
    id theCursor = nil;
    if (!cursorPushed) theCursor = [[self currentGraphic] cursor];
    return theCursor ? theCursor : NXArrow;
}

- sendEvent:(NXEvent *)event
/*
 * We override this because we need to find out when the control key is down
 * so we can set the arrow cursor so the user knows she is (temporarily) in
 * select mode.
 */
{
    if (event && event->type < NX_KITDEFINED) {	/* mouse or keyboard event */
	if (event->flags & NX_CONTROLMASK) {
	    if (!cursorPushed && currentGraphic) {
		cursorPushed = YES;
		[[self currentDocument] resetCursor];
	    }
	} else if (cursorPushed) {
	    cursorPushed = NO;
	    [[self currentDocument] resetCursor];
	}
    }

    return [NXApp sendEvent:event];
}

/* Automatic update methods */

- (BOOL)menuItemUpdate:(MenuCell *)menuCell
/*
 * Method called by all menu items which send their actions to the
 * First Responder.  First, if the object which would respond were the
 * action sent down the responder chain also responds to the message
 * validateCommand:, then it is sent validateCommand: to determine
 * whether that command is valid now, otherwise, if there is a responder
 * to the message, then it is assumed that the item is valid.
 * The method returns YES if the cell has changed its appearance (so that
 * the caller (a Menu) knows to redraw it).
 */
{
    SEL action;
    id responder, target;
    BOOL enable;

    target = [menuCell target];
    enable = [menuCell isEnabled];

    if (!target) {
	action = [menuCell action];
	responder = [NXApp calcTargetForAction:action];
	if ([responder respondsTo:@selector(validateCommand:)]) {
	    enable = [responder validateCommand:menuCell];
	} else {
	    enable = responder ? YES : NO;
	}
    }

    if ([menuCell isEnabled] != enable) {
	[menuCell setEnabled:enable];
	return YES;
    } else {
	return NO;
    }
}

- (BOOL)validateCommand:(MenuCell *)menuCell
/*
 * The only command DrawApp itself controls is saveAll:.
 * Save All is enabled only if there are any documents open.
 */
{
    SEL action = [menuCell action];

    if (action == @selector(saveAll:)) {
	return findDocument(NULL) ? YES : NO;
    }

    return YES;
}

/*
 * This is a very funky method and tricks of this sort are not generally
 * recommended, but this hack is done so that new Graphic subclasses can
 * be added to the program without changing even one line of code (except,
 * of course, to implement the subclass itself).
 *
 * The objective-C method objc_lookUpClass() is used to find the factory object
 * corresponding to the name of the icon of the cell sending the setCurrentGraphic:
 * message.
 *
 * Again, this is not recommended procedure, but it illustrates how
 * objective-C can be used to make some funky runtime dependent decisions.
 */

- setCurrentGraphic:sender
/*
 * The sender's icon is queried.  If that name corresponds to the name
 * of a class, then that class is set as the currentGraphic.  If not,
 * then the select tool is put into effect.
 */
{
    id cell;
    const char *className;

    if (cell = [sender selectedCell]) {
	if (className = [cell icon]) {
	    currentGraphic = objc_lookUpClass(className);
	} else {
	    currentGraphic = nil;
	}
	if (!currentGraphic) [tools selectCellAt:0 :0];
	[[self currentDocument] resetCursor];
    }

    return self;
}

/* Added to support HippoDraw since DrawApp is not subclass of
 * Application */
- orderFrontTools:sender
{
    [[tools window] orderFront:self];
    return self;
}
- setHaveOpenedDocument:(BOOL) flag
{
    haveOpenedDocument = flag;
    return self;
}
@end

These are the contents of the former NiCE NeXT User Group NeXTSTEP/OpenStep software archive, currently hosted by Netfuture.ch.