/* DrawDocument.m	by Paul Hegarty, NeXT, Inc.
 * Delegate to drawing Window.
 * Modified to support HippoDraw
 *
 * Copyright (C)  1991  The Board of Trustees of
 * The Leland Stanford Junior University.  All Rights Reserved.
 */

#import "DrawDocument.h"

const char DrawDocument_h_rcsid[] = DRAWDOCUMENT_H_ID;
const char DrawDocument_m_rcsid[]= "$Id: DrawDocument.m,v 1.38 1992/04/13 20:43:55 pfkeb Rel $";

#import "HDraw.h"
#import "HGraphicView.h"
#import "DrawPageLayout.h"
#import "GridView.h"
#import "InspectTuple.h"
#import "SyncScrollView.h"
#import "Ruler.h"
#import "hippo.h"
#import <appkit/Application.h>
#import <appkit/Cursor.h>
#import <appkit/Listener.h>
#import <appkit/Matrix.h>
#import <appkit/Pasteboard.h>
#import <appkit/PrintInfo.h>
#import <appkit/SavePanel.h>
#import <appkit/Speaker.h>
#import <appkit/nextstd.h>
#import <appkit/publicWraps.h>
#import <objc/List.h>
#import <objc/hashtable.h>	/* for NXCopyStringBuffer() */
#import <zone.h>
#import <string.h>
#import <libc.h>
#import <mach.h>
#import <sys/types.h>


#define Notify(title, msg) NXRunAlertPanel(title, msg, "OK", NULL, NULL)

#define NEW_DRAW_VERSION 184

@implementation DrawDocument
/*
 * This class is used to keep track of a Draw document.
 *
 * Its view and window instance variables keep track of the GraphicView
 * comprising the document as well as the window it is in.
 * The printInfo instance variable is used to allow the user to control
 * how the printed page is printed.  It is an instance of a PrintInfo
 * object (which is edited via the PageLayout and PrintPanels).
 * The listener is used to allow the user to drag an icon representing
 * a PostScript or TIFF file into the document.  The iconPathList is the
 * list of files which was last dragged into the document.
 * The name and directory specify where the document is to be saved.
 * haveSavedDocument keeps track of whether a disk file is associated
 * with the document yet (i.e. if it has ever been saved).
 *
 * The DrawDocument class's responsibilities:
 *
 * 1. Manage the window (including the scrolling view) which holds the
 *    document's GraphicView.  This includes constraining the resizing of
 *    the window so that it never becomes larger than the GraphicView, and
 *    ensuring that if the window contains an unsaved document and the user
 *    tries to close it, the user gets an opportunity to save her changes.
 * 2. Handle communication with the Workspace Manager which allows icons
 *    for PostScript and TIFF files to be dragged into the document window
 *    and be assimilated into the document.
 * 3. Saving the document to a disk file.
 * 4. Provide an external interface to saving the contents of the GraphicView
 *    as a PostScript or TIFF file.
 */

#define MIN_WINDOW_WIDTH 50.0
#define MIN_WINDOW_HEIGHT 75.0
#define SCROLLVIEW_BORDER NX_NOBORDER

static NXRect *calcFrame(id printInfo, NXRect *viewRect)
/*
 * Calculates the size of the page the user has chosen minus its margins.
 */
{
    float lm, rm, bm, tm;
    const NXRect *paperRect;

    viewRect->origin.x = viewRect->origin.y = 0.0;
    paperRect = [printInfo paperRect];
    [printInfo getMarginLeft:&lm right:&rm top:&tm bottom:&bm];
    viewRect->size = paperRect->size;
    viewRect->size.width -= lm + rm;
    viewRect->size.height -= tm + bm;

    return viewRect;
}

static void getContentSizeForView(id view, NXSize *contentSize)
/*
 * Calculates the size of the window's contentView by accounting for the
 * existence of the ScrollView around the GraphicView.  No scrollers are
 * assumed since we are interested in the minimum size need to enclose
 * the entire view and, if the entire view is visible, we don't need
 * scroll bars!
 */
{
    NXRect viewFrame;

    [view getFrame:&viewFrame];
    [SyncScrollView getFrameSize:contentSize
	      forContentSize:&viewFrame.size
	       horizScroller:YES vertScroller:YES
		  borderType:SCROLLVIEW_BORDER];
}

#define WINDOW_MASK (NX_MOUSEUPMASK|NX_MOUSEDRAGGEDMASK|NX_FLAGSCHANGEDMASK)

static id createWindowFor(id view, NXRect *windowContentRect)
/*
 * Creates a window for the specified view.
 * If windowContentRect is NULL, then a window big enough to fit the whole
 * view is created (unless that would be too big to comfortably fit on the
 * screen, in which case a smaller window may be allocated).
 * If windowContentRect is not NULL, then it is used as the contentView of
 * the newly created window.
 *
 * setMiniwindowIcon: sets the name of the bitmap which will be used in
 * the miniwindow of the window (i.e. when the window is miniaturized).
 * The icon "drawdoc" was defined in InterfaceBuilder (take a look in
 * the icon suitcase).
 */
{
    NXSize screenSize;
    id scrollView, window;
    NXRect defaultWindowContentRect;

    if (!windowContentRect) {
	windowContentRect = &defaultWindowContentRect;
	getContentSizeForView(view, &windowContentRect->size);
	[NXApp getScreenSize:&screenSize];
	if (windowContentRect->size.width > screenSize.width / 2.0) {
	    windowContentRect->size.width = floor(screenSize.width / 2.0);
	}
	if (windowContentRect->size.height > screenSize.height - 300.0) {
	    windowContentRect->size.height = screenSize.height - 300.0;
	}
	windowContentRect->origin.x = 200.0; /*screenSize.width - 75.0 - windowContentRect->size.width;*/
	windowContentRect->origin.y = floor(screenSize.height - windowContentRect->size.height-24.0);
    }

    window = [[Window allocFromZone:[view zone]] initContent:windowContentRect
			  style:NX_TITLEDSTYLE
		        backing:(InMsgPrint ? NX_NONRETAINED : NX_BUFFERED)
		     buttonMask:NX_ALLBUTTONS
			  defer:(InMsgPrint ? NO : YES)];

    scrollView = [[SyncScrollView allocFromZone:[view zone]] initFrame:windowContentRect];
    [scrollView setRulerClass:[Ruler class]];
    [scrollView setRulerOrigin:UpperLeft];
    [scrollView setRulerWidths:[Ruler width] :[Ruler width]];
    [scrollView setVertScrollerRequired:YES];
    [scrollView setHorizScrollerRequired:YES];
    [scrollView setBorderType:SCROLLVIEW_BORDER];
    [scrollView setDocView:view];
    [window setContentView:scrollView];
    [window addToEventMask:WINDOW_MASK];
    [window makeFirstResponder:view];
    [window setMiniwindowIcon:"hippo"];

    return window;
}

/* Very private methods needed by factory methods */

+ backupOldDrawDocument:(const char *)file
/*
* We do this because we are still beta-testing the new version.
*/
{
    int version;
    volatile BOOL isOld = NO;
    NXStream *volatile stream;
    char *extension, *dash;
    NXTypedStream *volatile ts = NULL;
    char buffer[MAXPATHLEN+1];

    NX_DURING
	stream = NXMapFile(file, NX_READONLY);
	if (stream) ts = NXOpenTypedStream(stream, NX_READONLY);
	if (ts) {
	    NXReadType(ts, "i", &version);
	    if (version != NEW_DRAW_VERSION) isOld = YES;
	}
    NX_HANDLER
    NX_ENDHANDLER

    if (ts) NXCloseTypedStream(ts);
    if (stream) NXClose(stream);

    if (isOld) {
	strcpy(buffer, file);
	extension = strrchr(buffer, '.');
	if (extension && !strcmp(extension, ".hdraw")) {
	    dash = strrchr(buffer, '-');
	    if (!dash || strcmp(dash, "-1.0.hdraw")) strcpy(extension, "-1.0.hdraw");
	} else {
	    strcat(buffer, "-1.0");
	}
	link(file, buffer);
    }

    return self;
}

- (BOOL)loadDocument:(NXStream *)stream frameSize:(NXRect *)frame
/*
 * Loads an archived document from the specified filename.
 * Loads the window frame specified in the archived document into the
 * frame argument (if the frame argument is NULL, then the frame in
 * the archived document is ignored).  Returns YES if the document
 * has been successfully loaded, NO otherwise.  Note that this method
 * destroys the receiving document, so use with extreme care
 * (essentially, this should only be called when a new document is
 * being created or an existing one is being reverted to its form
 * on disk).
 *
 * An NX_DURING handler is needed around the NXTypedStream operations because
 * if the user has asked that a bogus file be opened, the NXTypedStream will
 * raise an error.  To handle the error, the NXTypedStream must be closed.
 */
{
    int version;
    NXRect dummyRect;
    volatile BOOL retval = YES;
    NXTypedStream *volatile ts = NULL;

    if (!frame) frame = &dummyRect;

    NX_DURING
	ts = NXOpenTypedStream(stream, NX_READONLY);
	if (ts) {
	    NXSetTypedStreamZone(ts, [self zone]);
	    NXReadType(ts, "i", &version);	/* historical */
	    printInfo = NXReadObject(ts);
	    NXReadRect(ts, frame);
	    view = NXReadObject(ts);
	} else {
	    retval = NO;
	}
    NX_HANDLER
	retval = NO;
    NX_ENDHANDLER

    if (ts) NXCloseTypedStream(ts);

    return retval;
}

/* Factory methods */

/*
 * We reuse zones since it doesn't cost us anything to have a
 * zone lying around (e.g. if we open ten documents at the start
 * then don't use 8 of them for the rest of the session, it doesn't
 * cost us anything except VM (no real memory cost)), and it is
 * risky business to go around NXDestroy()'ing zones since if
 * your application accidentally allocates some piece of global
 * data into a zone that gets destroyed, you could have a pointer
 * to freed data on your hands!  We use the List object since it
 * is so easy to use (which is okay as long as 'id' remains a
 * pointer just like (NXZone *) is a pointer!).
 *
 * Note that we don't implement alloc and allocFromZone: because
 * we create our own zone to put ourselves in.  It is generally a
 * good idea to "notImplemented:" those methods if you do not allow
 * an object to be alloc'ed from an arbitrary zone (other examples
 * include Application and all of the Application Kit panels
 * (which allocate themselves into their own zone).
 */

static id zoneList = nil;

+ (NXZone *)newZone
{
    if (!zoneList || ![zoneList count]) {
	return NXCreateZone(vm_page_size, vm_page_size, YES);
    } else {
	return (NXZone *)[zoneList removeLastObject];
    }
}

+ (void)reuseZone:(NXZone *)aZone
{
    if (!zoneList) zoneList = [List new];
    [zoneList addObject:(id)aZone];
    NXNameZone(aZone, "Unused");
}

+ allocFromZone:(NXZone *)aZone
{
    return [self notImplemented:@selector(allocFromZone:)];
}

+ alloc
{
    return [self notImplemented:@selector(alloc)];
}

/* Creation methods */

+ new
/*
 * Creates a new, empty, document.
 *
 * Creates a PrintInfo object; creates a view whose size depends on the
 * default PrintInfo created; creates a window for that view; sets self
 * as the window's delegate; orders the window front; registers the window
 * with the Workspace Manager.  Note that the default margins are set
 * to 1/2 inch--that's more appropriate for a draw program than 1 or 1.25
 * inches.
 */
{
    NXZone *zone;
    NXRect frameRect;
    NXPoint top;

    zone = [self newZone];
    self = [super allocFromZone:zone];
    [self registerForServicesMenu];
    printInfo = [PrintInfo new];
    [printInfo setMarginLeft:36.0 right:36.0 top:36.0 bottom:36.0];
    calcFrame(printInfo, &frameRect);
    view = [[HGraphicView allocFromZone:zone] initFrame:&frameRect];
    [view setClipping:NO];			/* since it is in a ClipView */
    [view setGridSpacing:10];
    [view setGridVisible:YES];
    window = createWindowFor(view, NULL);
    [window setDelegate:self];
    top.x = 0;
    top.y = frameRect.size.height;
    [view scrollPoint:&top];
    [self resetScrollers];
    [self setName:NULL andDirectory:NULL];
    [window makeKeyAndOrderFront:self];
    [self registerWindow];

    return self;
}

+ newFromStream:(NXStream *)stream
/*
 * Creates a new document from what is in the passed stream.
 */
{
    NXZone *zone;
    NXRect contentViewFrame, frameRect;
    NXPoint top;

    zone = [self newZone];
    self = [super allocFromZone:zone];
    [self registerForServicesMenu];
    if (stream && [self loadDocument:stream frameSize:&contentViewFrame]) {
	window = createWindowFor(view, &contentViewFrame);
	[window setDelegate:self];
	[view getFrame:&frameRect];
	top.x = 0;
	top.y = frameRect.size.height;
	[view scrollPoint:&top];
	[self resetScrollers];
	if (!InMsgPrint) {
	    [window makeKeyAndOrderFront:self];
	    [self registerWindow];
	}
	haveSavedDocument = YES;
	return self;
    } else {
	Notify("Open Draw Document", "I/O error.  Can't open file.");
	[self free];
	return nil;
    }
}

+ newFromFile:(const char *)file
/*
 * Opens an existing document from the specified file.
 */
{
    NXStream           *stream;

    [self backupOldDrawDocument:file];
    stream = NXMapFile(file, NX_READONLY);
    if (stream) {
	if (self = [self newFromStream:stream]) [self setName:file];
	NXClose(stream);
	return self;
    } else {
	return nil;
    }
}
- openTupleFile:(const char *)path
{
    id		inspectTuple;
    BOOL	refFlag;
    int		irc;
    
    if ( hDraw == nil ) {
	hDraw = NXGetNamedObject("HDrawInstance", NXApp);
    }
    inspectTuple = [hDraw inspectTuple];
    irc = NXRunAlertPanel( "Open",
		"Import tuple by reference?",
		"Yes", "No", NULL);
    refFlag = (irc == NX_ALERTDEFAULT) ? YES : NO ;
    irc = [inspectTuple openTupleFile:path by:refFlag];
    if ( irc == HD_CANCEL ) {
        return self;
    }
    if ( irc == HD_REPLACE ) {
        [inspectTuple bindDisplays];
    }
    [view reDrawPlot];
    return self;
}
- free
{
    [printInfo free];
    [window free];
    NX_FREE(name);
    NX_FREE(directory);
    NX_FREE(iconPathList);
    [[self class] reuseZone:[self zone]];
    return [super free];
}

/* Services menu support methods. */

/* Services menu registrar */

- registerForServicesMenu
{
    static BOOL registered = NO;
    const char *validSendTypes[2];

    if (!registered) {
	registered = YES;
	validSendTypes[0] = NXFilenamePboardType;
	validSendTypes[1] = NULL;
	[NXApp registerServicesMenuSendTypes:validSendTypes andReturnTypes:NULL];
    }

    return self;
}

- validRequestorForSendType:(NXAtom)sendType andReturnType:(NXAtom)returnType
/*
 * Services menu support.
 * We are a valid requestor if the send type is filename
 * and there is no return data from the request.
 */
{
    return (haveSavedDocument && sendType == NXFilenamePboardType && (!returnType || !*returnType)) ? self : nil;
}

- writeSelectionToPasteboard:pboard types:(NXAtom *)types
/*
 * Services menu support.
 * Here we are asked by the Services menu mechanism to supply
 * the filename (which we said we were a valid requestor for
 * in the above method).
 */
{
    int save;

    if (haveSavedDocument) {
	while (types && *types) if (*types == NXFilenamePboardType) break; else types++;
	if (types && *types) {
	    if ([view isDirty]) {
		save = NXRunAlertPanel("Service", "Do you wish to save this document before your request is serviced?", "Save", "Don't Save", NULL);
		if (save == NX_ALERTDEFAULT) [self save];
	    }
	    [pboard declareTypes:&NXFilenamePboardType num:1 owner:self];
	    [pboard writeType:NXFilenamePboardType data:[self filename] length:strlen([self filename])+1];
	    return self;
	}
    }

    return nil;
}

/* Other methods. */

- loadImageFile:(const char *)file at:(const NXPoint *)p allowAlpha:(BOOL)alphaOk
/*
 * Maps in the specified file and asks the view to load the PostScript in
 * from the resulting NXStream.  The PostScript image will be centered at
 * the point p (in the GraphicView's coordinate system).  This is called
 * from the icon-dragging mechanism (icons dragged from the Workspace into
 * the document--see registerWindow).
 */
{
    NXStream *stream = NXMapFile(file, NX_READONLY);
    [view loadImageFromStream:stream at:p allowAlpha:alphaOk];
    NXClose(stream);
    return self;
}

- resetScrollers
/*
 * Checks to see if the new window size is too large.
 * Called whenever the page layout (either by user action or
 * by the opening or reverting of a file) is changed or
 * the user resizes the window.
 */
{
    id scrollView;
    NXSize contentSize;
    NXRect contentRect, windowFrame;

    if (window) {
	[window getFrame:&windowFrame];
	[[window class] getContentRect:&contentRect
			  forFrameRect:&windowFrame
				 style:[window style]];
	scrollView = [window contentView];
	getContentSizeForView(view, &contentSize);
	if ([scrollView horizontalRulerIsVisible]) contentSize.height += [Ruler width];
	if ([scrollView verticalRulerIsVisible]) contentSize.width += [Ruler width];
	if (contentRect.size.width >= contentSize.width || contentRect.size.height >= contentSize.height) {
	    contentSize.width = MIN(contentRect.size.width, contentSize.width);
	    contentSize.height = MIN(contentRect.size.height, contentSize.height);
	    [window sizeWindow:contentSize.width :contentSize.height];
	}
    }

    return self;
}- view
/*
 * Returns the GraphicView associated with this document.
 */
{
    return view;
}

- printInfo
{
    return printInfo;
}

/* Target/Action methods */

- changeLayout:sender
/*
 * Puts up a PageLayout panel and allows the user to pick a different
 * size paper to work on.  After she does so, the view is resized to the
 * new paper size.
 * Since the PrintInfo is effectively part of the document, we dirty
 * the view (by performing the dirty method).
 */
{
    NXRect frame;

    if ( hDraw == nil ) {
	hDraw = NXGetNamedObject("HDrawInstance", NXApp);
    }
    if ([[hDraw pageLayout] runModal] == NX_OKTAG) {
	calcFrame(printInfo, &frame);
	[view sizeTo:frame.size.width :frame.size.height];
	[self resetScrollers];
	[view display];
	[view dirty];
    }

    return self;
}
- getPageFrame:(NXRect *)frame
{
    calcFrame(printInfo, frame);
    return self;
}
- changeGrid:sender
/*
 * Changes the grid by putting up a modal panel asking the user what
 * she wants the grid to look like.
 */
{
    if ( hDraw == nil ) {
	hDraw = NXGetNamedObject("HDrawInstance", NXApp);
    }
    [[hDraw gridInspector] runModalForGraphicView:view];
    return self;
}

- close:sender
{
    [window performClose:self];
    return self;
}

- save:sender
/*
 * Saves the file.  If this document has never been saved to disk,
 * then a SavePanel is put up to ask the user what file name she
 * wishes to use to save the document.
 */
{
    id savepanel;

    if (!haveSavedDocument) {
	if ( hDraw == nil ) {
	    hDraw = NXGetNamedObject("HDrawInstance", NXApp);
	}
	savepanel = [hDraw saveAsPanel];
	if ([savepanel runModalForDirectory:directory file:name]) {
	    [self setName:[savepanel filename]];
	} else {
	    return self;
	}
    }

    [self save];

    return self;
}

- saveAs:sender
{
    [view dirty];
    haveSavedDocument = NO;
    return [self save:sender];
}

- changeSaveType:sender
/*
 * Called by the SavePanel accessory view whenever the user chooses
 * a different type of file to save to.  The window of the sender
 * is, of course, the SavePanel itself.  setRequiredFileType: does
 * not affect the SavePanel while it is running.  It only has effect
 * when the user has chosen a file, and the SavePanel ensures that it
 * has the correct extension by adding it if it doesn't have it already.
 * This message gets here via the Responder chain from the SavePanel.
 */
{
    switch ([sender selectedRow]) {
	case 0: [[sender window] setRequiredFileType:"hdraw"]; break;
	case 1: [[sender window] setRequiredFileType:"eps"]; break;
	case 2: [[sender window] setRequiredFileType:"tiff"]; break;
	case 3: [[sender window] setRequiredFileType:"hippo"]; break;
    }
    return self;
}

- saveTo:sender
/*
 * This takes the document and saves it as a Draw document file, PostScript
 * file, or TIFF file.  If the document type chosen is Draw document, then
 * this saves the file, but DOES NOT make that file the currently edited
 * file (this makes it easy to save your document elsewhere as a backup
 * and keep on going in the current document).
 *
 * If PostScript or TIFF is selected, then the document is written out
 * in the appropriate format.  In the case of PostScript and TIFF, the
 * actual saving is done using the more general method saveAs:using:.
 */
{
    id savepanel;
    const char *file;
    char *type, *savedName, *savedDirectory;
    BOOL viewIsDirty, reallyHaveSavedDocument;
    char buffer[MAXPATHLEN+1];

    strcpy(buffer, name);
    type = strrchr(buffer, '.');
    if (type) *type = '\0';
    if ( hDraw == nil ) {
	hDraw = NXGetNamedObject("HDrawInstance", NXApp);
    }
    savepanel = [hDraw saveToPanel];
    if (![savepanel runModalForDirectory:directory file:buffer]) return self;

    file = [savepanel filename];
    type = strrchr(file, '.');
    if (type) {
	if (!strcmp(type, ".eps")) {
	    [self saveTo:file using:@selector(writePSToStream:)];
	} else if (!strcmp(type, ".tiff")) {
	    [self saveTo:file using:@selector(writeTIFFToStream:)];
	} else if (!strcmp(type, ".hdraw")) {
	    viewIsDirty = [view isDirty];	/* is it really dirty? */
	    [view dirty];			/* temporarily dirty it */
	    reallyHaveSavedDocument = haveSavedDocument;
	    savedName = name;			/* save current name */
	    savedDirectory = directory;		/* save current directory */
	    name = NULL; directory = NULL;	/* clear current filename */
	    [self setName:file];		/* temporarily change name */
	    [self save];			/* save, then restore name */
	    [self setName:savedName andDirectory:savedDirectory];
	    if (viewIsDirty) [view dirty];	/* restore correct dirtiness */
	    haveSavedDocument = reallyHaveSavedDocument;
	} else if ( !strcmp(type, ".hippo") ) {
	    [view saveAsExportFile:file];
	}
    }

    return self;
}


- revertToSaved:sender
/*
 * Revert the document back to what is on the disk.
 */ 
{
    NXStream *stream;
    NXRect viewFrame, visibleRect;

    if (!haveSavedDocument || ![view isDirty] || (NXRunAlertPanel("Revert", "%s has been edited.  Are you sure you want to undo changes?", "Revert", "Cancel", NULL, name) != NX_ALERTDEFAULT)) {
	return self;
    }

    [view getVisibleRect:&visibleRect];
    [window endEditingFor:self];

    stream = NXMapFile([self filename], NX_READONLY);
    if (stream && [self loadDocument:stream frameSize:NULL]) {
	[[[window contentView] setDocView:view] free];
	calcFrame(printInfo, &viewFrame);

	[view sizeTo:viewFrame.size.width :viewFrame.size.height];
	[self resetScrollers];
	[view scrollRectToVisible:&visibleRect];
	[view display];
	[window reenableFlushWindow];
	[window flushWindow];
	[window makeFirstResponder:view];
	[window setDocEdited:NO];
	NXClose(stream);
    } else {
	if (stream) NXClose(stream);
	Notify("Revert", "I/O error.  Can't revert.");
    }

    return self;
}

- showTextRuler:sender
/*
 * Sent to cause the Text object ruler to be displayed.
 * Only does anything if the rulers are already visible.
 * This doesn't currently work due to an AppKit bug.
 */
{
    id scrollView = [window contentView];

    if ([scrollView verticalRulerIsVisible] && [scrollView horizontalRulerIsVisible]) {
	[scrollView showHorizontalRuler:NO];
	[sender toggleRuler:sender];
    }

    return self;
}

- hideRuler:sender
/*
 * If sender is nil, we assume the sender wants the
 * ruler hidden, otherwise, we toggle the ruler.
 * If sender is the field editor itself, we do nothing
 * (this allows the field editor to demand that the
 * ruler stay up).
 * This doesn't currently work properly when editing text
 * due to an AppKit bug.
 */
{
    id scrollView = [window contentView];
    id fe = [window getFieldEditor:NO for:NXApp];

    if (!sender && [scrollView verticalRulerIsVisible]) {
	[fe toggleRuler:sender];
	[window disableDisplay];
	[scrollView toggleRuler:nil];
	if ([scrollView verticalRulerIsVisible]) [scrollView showHorizontalRuler:YES];
	[window reenableDisplay];
	[scrollView resizeSubviews:(NXSize *)0];
    } else if (sender) {
	[fe toggleRuler:sender];
	if ([scrollView verticalRulerIsVisible]) {
	    [scrollView showVerticalRuler:NO];
	    [scrollView showHorizontalRuler:NO];
	    if (![fe window]) [scrollView toggleRuler:nil];
	} else {
	    [scrollView showVerticalRuler:YES];
	    if ([fe window]) {
		[scrollView showHorizontalRuler:NO];
	    } else {
		[scrollView toggleRuler:nil];
		[scrollView showHorizontalRuler:YES];
	    }
	}
    }

    return self;
}

/* Methods related to naming/saving this document. */

- (const char *)filename
/*
 * Gets the fully specified file name of the document.
 * If directory is NULL, then the currentDirectory is used.
 * If name is NULL, then the default title is used.
 */
{
    static char filenamebuf[MAXPATHLEN+1];

    if (!directory && !name) {
	[self setName:NULL andDirectory:NULL];
    }
    if (directory) {
	strcpy(filenamebuf, directory);
	strcat(filenamebuf, "/");
    } else {
	filenamebuf[0] = '\0';
    }
    if (name) {
	strcat(filenamebuf, name);
    }

    return filenamebuf;
}

- (const char *)directory
{
    return directory;
}

- (const char *)name
{
    return name;
}

- setName:(const char *)newName andDirectory:(const char *)newDirectory
/*
 * Updates the name and directory of the document.
 * newName or newDirectory can be NULL, in which case the name or directory
 * will not be changed (unless one is currently not set, in which case
 * a default name will be used).
 */
{
    char oldName[MAXPATHLEN+1];
	
    if ( hDraw == nil ) {
	hDraw = NXGetNamedObject("HDrawInstance", NXApp);
    }
    if (directory && name) {
	strcpy(oldName, [self filename]);
    } else {
	oldName[0] = '\0';
    }

    if ((newName && *newName) || !name) {
 	if (!newName || !*newName) newName = "UNTITLED";
	NX_FREE(name);
	name = NXCopyStringBufferFromZone(newName, [self zone]);
    }

    if ((newDirectory && (*newDirectory == '/')) || !directory) {
 	if (!newDirectory || (*newDirectory != '/')) {
	    newDirectory = [hDraw currentDirectory];
	}
	NX_FREE(directory);
	directory = NXCopyStringBufferFromZone(newDirectory, [self zone]);
    }

    [window setTitleAsFilename:[self filename]];
    NXNameZone([self zone], [self filename]);

    return self;
}

- setName:(const char *)file
/*
 * If file is a full path name, then both the name and directory of the
 * document is updated appropriately, otherwise, only the name is changed.
 */
{
    char *lastComponent;
    char path[MAXPATHLEN+1];

    if (file) {
	strcpy(path, file);
	lastComponent = strrchr(path, '/');
	if (lastComponent) {
	    *lastComponent++ = '\0';
	    return [self setName:lastComponent andDirectory:path];
	} else {
	    return [self setName:file andDirectory:NULL];
	}
    }

    return self;
}

- saveTo:(const char *)file using:(SEL)streamWriter
/*
 * Performed by the saveTo: method, this method uses the streamWriter method
 * to have the GraphicView write itself in some foreign format (i.e., not
 * in Draw archive format).  It does some work to make the default name
 * of the file being saved to be the name of the document with the appropriate
 * extension.  It brings up the SavePanel so the user can specify the name
 * of the file to save to.
 */
{
    NXStream *stream;

    if (!file || !streamWriter) return self;

    stream = NXOpenMemory(NULL, 0, NX_WRITEONLY);
    if (stream) {
	[view perform:streamWriter with:(id)stream];
	NXSaveToFile(stream, file);
	NXCloseMemory(stream, NX_FREEBUFFER);
    }

    return self;
}


- save
/*
 * Writes out the document in three steps:
 *
 * 1. Write the printInfo object
 * 2. Write the rectangle of the contentView of the window
 * 3. Write the GraphicView itself.
 *
 * See GraphicView's write: method for more details on how the GraphicView
 * is archived.
 */
{
    id list;
    int version;
    NXTypedStream *ts;
    const char *saveFile = [self filename];
    NXRect windowFrame, contentViewRect;
    char buffer[MAXPATHLEN+1];

    if ([view isDirty]) {
	strcpy(buffer, saveFile);
	strcat(buffer, "~");
	unlink(buffer);
	link(saveFile, buffer);
	unlink(saveFile);
	list = [view plotList];
	[list makeObjectsPerform:@selector(changeRefFileNameIfValid:)
	        	    with:(id)saveFile];
	ts = NXOpenTypedStreamForFile(saveFile, NX_WRITEONLY);
	if (ts) {
	    haveSavedDocument = YES;
	    version = NEW_DRAW_VERSION;
	    NXWriteType(ts, "i", &version);
	    NXWriteRootObject(ts, printInfo);
	    [window getFrame:&windowFrame];
	    [[window class] getContentRect:&contentViewRect
			      forFrameRect:&windowFrame
				     style:[window style]];
	    NXWriteRect(ts, &contentViewRect);
	    NXWriteRootObject(ts, view);
	    NXCloseTypedStream(ts);
	} else {
	    Notify("Save", "Can't create file.");
	}
    }

    return self;
}

- (BOOL)needsSaving
{
    return ([view isDirty] && (haveSavedDocument || ![view isEmpty]));
}

/* Window delegate methods. */

- windowWillClose:sender action:(const char *)action
/*
 * If the GraphicView has been edited, then this asks the user if she
 * wants to save the changes before closing the window.  When the window
 * is closed, the DrawDocument itself must be freed.  This is accomplished
 * via Application's delayedFree: mechanism.  Unfortunately, by the time
 * delayedFree: frees the DrawDocument, the window and view instance variables
 * will already have automatically been freed by virtue of the window's being
 * closed.  Thus, those instance variables must be set to nil to avoid their
 * being freed twice.
 *
 * Returning nil from this method informs the caller that the window should
 * NOT be closed.  Anything else implies it should be closed.
 */
{
    int save;

    if ([self needsSaving]) {
	save = NXRunAlertPanel(action, "%s has changes. Save them?", "Save", "Don't Save", "Cancel", name);
	if (save != NX_ALERTDEFAULT && save != NX_ALERTALTERNATE) {
	    return nil;
	} else {
	    [sender endEditingFor:self];	/* terminate any editing */
	    if (save == NX_ALERTDEFAULT) {
		[self save:nil];
	    }
	}
    }

    [self unregisterWindow];
    window = nil;
    view = nil;
    [NXApp setPrintInfo:nil];
    [NXApp delayedFree:self];

    return self;
}

- windowWillClose:sender
{
    return [self windowWillClose:sender action:"Close"];
}

- windowDidBecomeMain:sender
/*
 * Switch the Application's PrintInfo to the document's when the document
 * window becomes the main window.  Also set the cursor appropriately
 * depending on which tool is currently selected.
 */
{
    [NXApp setPrintInfo:printInfo];
    [self resetCursor];
    return self;
}

- windowWillResize:sender toSize:(NXSize *)size
/*
 * Constrains the size of the window to never be larger than the
 * GraphicView inside it (including the ScrollView around it).
 */
{
    NXRect fRect, cRect;

    getContentSizeForView(view, &cRect.size);
    [[window class] getFrameRect:&fRect forContentRect:&cRect style:[window style]];
    if ([[window contentView] horizontalRulerIsVisible]) fRect.size.height += [Ruler width];
    if ([[window contentView] verticalRulerIsVisible]) fRect.size.width += [Ruler width];
    size->width = MIN(fRect.size.width, size->width);
    size->height = MIN(fRect.size.height, size->height);
    size->width = MAX(MIN_WINDOW_WIDTH, size->width);
    size->height = MAX(MIN_WINDOW_HEIGHT, size->height);

    return self;
}

- windowWillMiniaturize:sender toMiniwindow:counterpart
{
    char *dot;
    char title[MAXPATHLEN+1];

    strcpy(title, [self name]);
    dot = strrchr(title, '.');
    if (dot && !strcmp(dot, ".hdraw")) *dot = '\0';
    [counterpart setTitle:title];
    return self;
}

/* Icon dragging methods */

- registerWindow
/*
 * Registers the document window with the Workspace Manager so that when the
 * user picks up an icon in the Workspace and drags it over our document window
 * and lets go, iconEntered:... and iconReleasedAt::ok: messages will be
 * sent to the DrawDocument from the Workspace Manager.  Allows the user to
 * drag PostScript and TIFF files into the document.
 */
{
    unsigned int windowNum;
    id speaker = [NXApp appSpeaker];

    listener = [[Listener allocFromZone:[self zone]] init];
    [listener setDelegate:self];
    [listener usePrivatePort];
    [listener addPort];
    NXConvertWinNumToGlobal([window windowNum], &windowNum);
    [speaker setSendPort:NXPortFromName(NX_WORKSPACEREQUEST, NULL)];
    [speaker registerWindow:windowNum toPort:[listener listenPort]];

    return self;
}

- unregisterWindow
/*
 * Undoes what registerWindow does.
 */
{
    unsigned int windowNum;
    id speaker = [NXApp appSpeaker];

    if (listener) {
	[speaker setSendPort:NXPortFromName(NX_WORKSPACEREQUEST, NULL)];
	NXConvertWinNumToGlobal([window windowNum], &windowNum);
	[speaker unregisterWindow:windowNum];
	[listener free];
    }

    return self;
}

- (int)iconEntered:(int)windowNum at:(double)x :(double)y
    iconWindow:(int)iconWindowNum iconX:(double)iconX iconY:(double)iconY
    iconWidth:(double)iconWidth iconHeight:(double)iconHeight
    pathList:(char *)pathList
/*
 * Called whenever an icon is dragged from the Workspace over the document
 * window.  At this point, all that is done is to salt away the list of files
 * represented by the icon.  All the real work is done in iconReleasedAt::ok:.
 */
{
    if (!iconPathList || strcmp(iconPathList, pathList)) {
	NX_FREE(iconPathList);
	iconPathList = NXCopyStringBufferFromZone(pathList, [self zone]);
    }
    return 0;
}

- (int)iconReleasedAt:(double)x :(double)y ok:(int *)flag
/*
 * Goes through the list of files associated with the icon dragged
 * from the Workspace and checks if any of them are PostScript or TIFF.
 * If any are, then the GraphicView is asked to load those in as objects.
 * Very important: an NX_DURING handler is required around all the processing
 * of this method since an uncaught raised error will cause this method not
 * to return and thus hang the Workspace Manager for a while.
 */
{
    id		inspectTuple;
    NXPoint p;
    volatile int foundOne = NO;
    char *file, *tab, *extension;
    BOOL refFlag;
    int  irc;

    p.x = x; p.y = y;
    [window convertScreenToBase:&p];
    [view convertPoint:&p fromView:nil];

    NX_DURING
	file = iconPathList;
	while (file) {
	    tab = strchr(file, '\t');
	    if (tab) *tab = '\0';
	    extension = strrchr(file, '.');
	    if (extension) {
		if (!strcmp(extension, ".ps") || !strcmp(extension, ".eps") ||
		    !strcmp(extension,".tiff") || !strcmp(extension,".tif")) {
		    [self loadImageFile:file at:&p
		          allowAlpha:(strcmp(extension,".tiff") &&
		                      strcmp(extension,".tif"))];
		    foundOne = YES;
		}
		if ( !strcmp(extension, ".hippo") ) {
		    if ( hDraw == nil ) {
			hDraw = NXGetNamedObject("HDrawInstance", NXApp);
		    }
		    inspectTuple = [hDraw inspectTuple];
		    irc = NXRunAlertPanel( "Open",
		             "Import tuple by reference?",
			     "Yes", "No", NULL);
		    refFlag = (irc == NX_ALERTDEFAULT) ? YES : NO ;
		    irc = [inspectTuple openTupleFile:file by:refFlag];
		    if ( irc == HD_REPLACE ) {
			[inspectTuple bindDisplays];
		    }
		    if ( irc != HD_CANCEL ) {
        	        foundOne = YES;
		    }
		}
	    }
	    file = tab ? ++tab : NULL;
	}
	if (foundOne) {
	    [NXApp activateSelf:YES];
	    [window makeKeyAndOrderFront:self];
	}
    NX_HANDLER
    NX_ENDHANDLER

    *flag = foundOne;

    return 0;
}

/* Validates whether a menu command makes sense now */

- (BOOL)validateCommand:menuCell
/*
 * Validates whether a menu command that DrawDocument responds to
 * is valid at the current time.
 */
{
    SEL action = [menuCell action];

    if (action == @selector(save:)) {
	return [view isDirty];
    } else if (action == @selector(revertToSaved:)) {
	return ([view isDirty] && haveSavedDocument);
    } else if (action == @selector(saveAs:)) {
	return (haveSavedDocument || ![view isEmpty]);
    } else if (action == @selector(saveTo:)) {
	return ![view isEmpty];
    } else if (action == @selector(close:)) {
	return YES;
    } else if (action == @selector(hideRuler:)) {
	if ([[window contentView] eitherRulerIsVisible] && strcmp([menuCell title], "Hide Ruler")) {
	    [menuCell setTitle:"Hide Ruler"];
	    [menuCell setEnabled:NO];
	} else if (![[window contentView] eitherRulerIsVisible] && strcmp([menuCell title], "Show Ruler")) {
	    [menuCell setTitle:"Show Ruler"];
	    [menuCell setEnabled:NO];
	}
    } else if (action == @selector(alignSelLeft:) ||
	       action == @selector(alignSelRight:) ||
	       action == @selector(alignSelCenter:) ||
	       action == @selector(checkSpelling:) ||
	       action == @selector(showGuessPanel:)) {
	return [[window getFieldEditor:NO for:NXApp] superview] ? YES : NO;
    }

    return YES;
}

/* Cursor-setting method */

- resetCursor
/*
 * Sets the document's cursor according to whatever the current graphic is.
 * Makes the graphic view the first responder if there isn't one or if
 * no tool is selected (the cursor is the normal one).
 */
{
    id fr, cursor;
    id scrollview = [window contentView];

    if ( hDraw == nil ) {
	hDraw = NXGetNamedObject("HDrawInstance", NXApp);
    }
    cursor = [hDraw cursor];
    [scrollview setDocCursor:cursor];
    fr = [window firstResponder];
    if (!fr || fr == window || cursor == NXArrow) {
	[window makeFirstResponder:view];
    }

    return self;
}
@end

