#import "GraphicView.h"
#import "Rectangle.h"
#import "Group.h"
#import "Image.h"
#import "draw.h"
#import <appkit/NXCursor.h>
#import <appkit/NXColorWell.h>
#import <appkit/Matrix.h>
#import <appkit/Panel.h>
#import <appkit/Pasteboard.h>
#import <appkit/Text.h>
#import <appkit/defaults.h>
#import <appkit/nextstd.h>
#import <appkit/tiff.h>
#import <appkit/timer.h>
#import <dpsclient/wraps.h>
#import <objc/List.h>
#import <string.h>
#import <math.h>
#import <zone.h>
#import <mach.h>
#import <sys/param.h>

/* This file is best read in a window as wide as this comment (so that this comment fits on one line). */

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

const char *DrawPboardType = "Draw Graphic List Type";

BOOL InMsgPrint = NO;		/* whether we are in msgPrint: */

#define LEFTARROW	172
#define RIGHTARROW	174
#define UPARROW		173
#define DOWNARROW	175

@implementation GraphicView : View
/*
 * The GraphicView class is the core of a DrawDocument.
 *
 * It overrides the View methods related to drawing and event handling
 * and allows manipulation of Graphic objects.
 *
 * The user is allowed to select objects, move them around, group and
 * ungroup them, change their font, and cut and paste them to the pasteboard.
 * It understands multiple formats including PostScript and TIFF as well as
 * its own internal format.  The GraphicView can also import PostScript and
 * TIFF documents and manipulate them as Graphic objects.
 *
 * This is a very skeleton implementation and is intended purely for
 * example purposes.  It should be very easy to add new Graphic objects
 * merely by subclassing the Graphic class.  No code need be added to the
 * GraphicView class when a new Graphic subclass is added.
 *
 * Moving is accomplished using a selection cache which is shared among
 * all instances of GraphicView in the application.  The objects in the
 * selection are drawn using opaque ink on a transparent background so
 * that when they are moved around, the user can see through them to the
 * objects that are not being moved.
 *
 * All of the drawing is done in an off-screen window which is merely
 * composited back to the screen.  This makes for very fast redraw of
 * areas obscured either by the selection moving or the user's scrolling.
 *
 * The glist instance variable is just an ordered list of all the Graphics
 * in the GraphicView.  The slist is an ordered list of the Graphic objects
 * in the selection.  cacheWindow is the off-screen window into which the
 * objects are drawn.  Flags: dirty indicates that the user has modified
 * the collection of objects since the bit was last cleared; grid is the
 * distance between pixels in the grid imposed on drawing; cacheing is used
 * so that drawSelf:: knows when to composite from the off-screen cache and
 * when to draw into the off-screen cache; groupInSlist is used to keep
 * track of whether a Group Graphic is in the slist so that it knows when
 * to highlight the Ungroup entry in the menu.
 *
 * This class should be able to be used outside the context of this
 * application since it takes great pains not to depend on any other objects
 * in the application.
 */

/*
 * Of course, one should NEVER use global variables in an application, but
 * the following is necessary.  DrawStatus is
 * analogous to the Application Kit's NXDrawingStatus and reflects whether
 * we are in some modal loop.  By definition, that modal loop is "atomic"
 * since we own the mouse during its duration (of course, all bets are off
 * if we have multiple mice!).
 */
 
DrawStatusType DrawStatus = Normal;  /* global state reflecting what we
					are currently doing (resizing, etc.) */

const float DEFAULT_GRID_GRAY = 0.8333;

static id currentGraphic = nil;	/* won't be used if NXApp knows how
				   to keep track of the currentGraphic */

static float KeyMotionDeltaDefault = 0.0;

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

static id createCache(NXSize *size, NXZone *zone)
/*
 * Creates an appropriately size off-screen window to cache bits in.
 */
{
    NXRect cacheRect;

    cacheRect.origin.x = 0.0;
    cacheRect.origin.y = 0.0;
    cacheRect.size = *size;

    return [[[Window allocFromZone:zone] initContent:&cacheRect
					       style:NX_PLAINSTYLE
					     backing:NX_RETAINED
					  buttonMask:0
					       defer:NO] reenableDisplay];
}

/* Code-cleaning macros */

#define stopTimer(timer) if (timer) { \
    NXEndTimer(timer); \
    timer = NULL; \
}

#define startTimer(timer) if (!timer) timer = NXBeginTimer(NULL, 0.1, 0.1);

#define GRID (gvFlags.gridDisabled ? 1.0 : (NXCoord)gvFlags.grid)

#define grid(point) \
    (point).x = floor(((point).x / GRID) + 0.5) * GRID; \
    (point).y = floor(((point).y / GRID) + 0.5) * GRID;

#define DIRTY(condition) \
    if (condition && !gvFlags.dirty) { \
	gvFlags.dirty = YES; \
	[window setDocEdited:YES]; \
    }    

static void getRegion(NXRect *region, const NXPoint *p1, const NXPoint *p2)
/*
 * Returns the rectangle which has p1 and p2 as its corners.
 */
{
    region->size.width = p1->x - p2->x;
    region->size.height = p1->y - p2->y;
    if (region->size.width < 0.0) {
	region->origin.x = p2->x + region->size.width;
	region->size.width = ABS(region->size.width);
    } else {
	region->origin.x = p2->x;
    }
    if (region->size.height < 0.0) {
	region->origin.y = p2->y + region->size.height;
	region->size.height = ABS(region->size.height);
    } else {
	region->origin.y = p2->y;
    }
}

static BOOL checkForGroup(id list)
/*
 * Looks through the given list searching for objects of the Group class.
 * We use this to keep the gvFlags.groupInSlist flag up to date when things
 * are removed from the slist (the list of selected objects).  That way
 * we can efficiently keep the Ungroup menu item up to date.
 */
{
    int i = [list count];
    while (i--) if ([[list objectAt:i] isKindOf:[Group class]]) return YES;
    return NO;
}

/* Hack to support growable Text objects. */

static id createEditView(GraphicView *self)
/*
 * editView is essentially a dumb, FLIPPED (with extra emphasis on the
 * flipped) subview of our GraphicView which completely covers it and
 * which automatically sizes itself to always completely cover the
 * GraphicView.  It is necessary since growable Text objects only work
 * when they are subviews of a flipped view.
 *
 * See TextGraphic for more details about why we need editView
 * (it is purely a workaround for a limitation of the Text object).
 */
{
    id view;

    [self setAutoresizeSubviews:YES];
    view = [[View allocFromZone:[self zone]] initFrame:&(self->frame)];
    [view setFlipped:YES];
    [view setAutosizing:NX_WIDTHSIZABLE|NX_HEIGHTSIZABLE];
    [self addSubview:view];

    return view;
}

/* Routines to check the types in the Pasteboard */

static BOOL matchPasteType(const NXAtom *types, NXAtom type)
{
    if (types) while (*types) if (*types++ == type) return YES;
    return NO;
}

static NXAtom drawPasteType(const NXAtom *types)
/*
 * Returns the pasteboard type in the passed list of types which is preferred
 * by the Draw program for pasting.  The Draw program prefers its own type
 * of course, then it prefers PostScript over TIFF.
 */
{
    if (matchPasteType(types, DrawPboardType)) return DrawPboardType;
    if (matchPasteType(types, NXPostScriptPboardType)) return NXPostScriptPboardType;
    if (matchPasteType(types, NXTIFFPboardType)) return NXTIFFPboardType;
    return NULL;
}

/* Factory methods. */

+ initialize
/*
 * We up the version of the class so that we can read old .draw files.
 * See the read: method for how we use the version.
 */
{
    [self setVersion:1];
    DrawPboardType = NXUniqueStringNoCopy(DrawPboardType);
    return self;
}

/* Lazy Pasteboard evaluation handler */

/*
 * IMPORTANT: The pasteboard:provideData: method is a factory method since the
 * factory object is persistent and there is no guarantee that the INSTANCE of
 * GraphicView that put the Draw format into the Pasteboard will be around
 * to lazily put PostScript or TIFF in there, so we keep one around (actually
 * we only create it when we need it) to do the conversion (scrapper).
 *
 * If you find this part of the code confusing, then you need not even
 * use the provideData: mechanism--simply put the data for all the different
 * types your program knows how to put in the Pasteboard in at the time
 * that you declareTypes:.
 */

+ convert:(NXTypedStream *)ts to:(const char *)type using:(SEL)writer toPasteboard:pb
/*
 * Converts the data in the Pasteboard from Draw internal format to
 * either PostScript or TIFF using the writeTIFFToStream: and writePSToStream:
 * methods.  It sends these messages to the scrapper (a GraphicView cached
 * to perform this very function).  Note that the scrapper view is put in
 * a window, but that window is off-screen, has no backing store, and no
 * title (and is thus very cheap).
 */
{
    id w;
    char *data;
    NXZone *zone;
    NXStream *stream;
    int length, maxlen;
    GraphicView *scrapper;
    const NXRect scrapperFrame = {{0.0, 0.0}, {11.0*72.0, 14.0*72.0}};

    if (!ts) return self;

    zone = NXCreateZone(vm_page_size, vm_page_size, NO);
    NXNameZone(zone, "Scrapper");
    scrapper = [[GraphicView allocFromZone:zone] initFrame:&scrapperFrame];
    w = [[Window allocFromZone:zone] initContent:&scrapperFrame
					   style:NX_PLAINSTYLE
					 backing:NX_NONRETAINED
				      buttonMask:0
					   defer:NO];
    [w reenableDisplay];
    [w setContentView:scrapper];

    stream = NXOpenMemory(NULL, 0, NX_WRITEONLY);
    NXSetTypedStreamZone(ts, zone);
    scrapper->glist = NXReadObject(ts);
    [scrapper perform:writer with:(id)stream];
    NXGetMemoryBuffer(stream, &data, &length, &maxlen);
    [pb writeType:type data:data length:length];
    NXCloseMemory(stream, NX_FREEBUFFER);
    [w free];
    NXDestroyZone(zone);

    return self;
}


+ pasteboard:sender provideData:(const char *)type
/*
 * Called by the Pasteboard whenever PostScript or TIFF data is requested
 * from the Pasteboard by some other application.  The current contents of
 * the Pasteboard (which is in the internal format) is taken out and loaded
 * into a stream, then convert:to:using:toPasteboard: is called.
 */
{
    char *data;
    int length;
    NXStream *stream;
    NXTypedStream *ts;

    if (type) {
	[sender readType:DrawPboardType data:&data length:&length];
	stream = NXOpenMemory(data, length, NX_READONLY);
	if (stream) {
	    ts = NXOpenTypedStream(stream, NX_READONLY);
	    if (ts) {
		if (type == NXPostScriptPboardType) {
		    [self convert:ts to:NXPostScriptPboardType using:@selector(writePSToStream:) toPasteboard:sender];
		} else if (type == NXTIFFPboardType) {
		    [self convert:ts to:NXTIFFPboardType using:@selector(writeTIFFToStream:) toPasteboard:sender];
		}
		NXCloseTypedStream(ts);
	    }
	    NXCloseMemory(stream, NX_FREEBUFFER);
	}
    }

    return self;
}

/* Creation methods. */

static void initClassVars()
/*
 * Sets up any default values and cursors.
 */
{
    static BOOL registered = NO;
    const char *validSendTypes[4];
    const char *validReturnTypes[4];

    if (!KeyMotionDeltaDefault) {
	const char *value = NXGetDefaultValue([NXApp appName], "KeyMotionDelta");
	if (value) KeyMotionDeltaDefault = atof(value);
	KeyMotionDeltaDefault = MAX(KeyMotionDeltaDefault, 1.0);
    }
    if (!registered) {
	registered = YES;
	validSendTypes[0] = NXPostScriptPboardType;
	validSendTypes[1] = NXTIFFPboardType;
	validSendTypes[2] = DrawPboardType;
	validSendTypes[3] = NULL;
	validReturnTypes[0] = NXPostScriptPboardType;
	validReturnTypes[1] = NXTIFFPboardType;
	validReturnTypes[2] = DrawPboardType;
	validReturnTypes[3] = NULL;
	[NXApp registerServicesMenuSendTypes:validSendTypes andReturnTypes:validReturnTypes];
    }
}

- initFrame:(const NXRect *)frameRect
/*
 * Initializes the view's instance variables.
 * This view is considered important enough to allocate it a gstate.
 */
{
    [super initFrame:frameRect];
    glist = [[List allocFromZone:[self zone]] init];
    slist = [[List allocFromZone:[self zone]] init];
    cacheWindow = createCache(&bounds.size, [self zone]);
    gvFlags.grid = 1;
    [self allocateGState];
    gridGray = DEFAULT_GRID_GRAY;
    PSInit();
    currentGraphic = [Rectangle class];	      /* default graphic */
    currentGraphic = [self currentGraphic];   /* trick to allow NXApp to control currentGraphic */
    editView = createEditView(self);
    initClassVars();
    return self;
}

/* Free method */

- free
{
    if (gupCoords) {
	NX_FREE(gupCoords);
	NX_FREE(gupOps);
	NX_FREE(gupBBox);
    }
    [glist freeObjects];
    [glist free];
    [slist free];
    [cacheWindow free];
    if (![editView superview]) [editView free];
    return [super free];
}

/* Services menu methods */

- validRequestorForSendType:(NXAtom)sendType andReturnType:(NXAtom)returnType
/*
 * We are a valid requestor whenever any of the send or
 * return types is PostScript, TIFF, or Draw.
 * Note that we cache around the NXAtom for each
 * type since it is important this this method be fast
 * (it gets called relatively often).
 */
{

    if ((!sendType || !*sendType ||
	((sendType == NXPostScriptPboardType ||
	  sendType == NXTIFFPboardType ||
	  sendType == DrawPboardType) && [slist count])) &&
	(!returnType || !*returnType ||
	  returnType == NXPostScriptPboardType ||
	  returnType == NXTIFFPboardType ||
	  returnType == DrawPboardType)) {
	return self;
    }
 
    return [super validRequestorForSendType:sendType andReturnType:returnType];
}

- (BOOL)writeSelectionToPasteboard:pboard types:(NXAtom *)types
/*
 * If one of the requested types is one of the ones we handle,
 * then we put our selection in the Pasteboard.
 */
{
    while (types && *types) {
	if (*types == NXPostScriptPboardType || *types == NXTIFFPboardType || *types == DrawPboardType) break;
	types++;
    }

    if (types && *types && [self copyToPasteboard:pboard types:types]) {
	serviceActsOnSelection = YES;
	return YES;
    } else {
	return NO;
    }
}

- readSelectionFromPasteboard:pboard
/*
 * When a result comes back from the Services menu request,
 * we replace the selection with the return value.
 * If the user really wants the return value in addition to
 * the current selection, she can simply copy, then paste
 * twice to get two copies, then choose the Services menu item.
 */
{
    if (serviceActsOnSelection) [self delete:self];
    serviceActsOnSelection = NO;
    [[self pasteFromPasteboard:pboard] free];
    return self;
}

/* Private selection management methods. */

- selectionCache
/*
 * Shares an off-screen window used to draw the selection in so that it
 * can be dragged around.  If the current off-screen window is equal in
 * size or larger than the passed size, then it is simply returned.
 * Otherwise, it is resized to be the specified size.
 */
{
    NXRect rect;
    static id selectioncache = nil;

    if (!selectioncache) {
	rect = bounds;
	selectioncache = [Window newContent:&rect
				      style:NX_PLAINSTYLE
				    backing:NX_RETAINED
				 buttonMask:0
				      defer:NO];
	[selectioncache reenableDisplay];
    } else {
	[selectioncache getFrame:&rect];
	if (rect.size.width < bounds.size.width || rect.size.height < bounds.size.height) {
	    [selectioncache sizeWindow:MAX(rect.size.width, bounds.size.width)
				      :MAX(rect.size.height, bounds.size.height)];
	}
    }

    return selectioncache;
}

- getBBox:(NXRect *)bbox of:list extended:(BOOL)extended
/*
 * Returns a rectangle which encloses all the objects in the list.
 */
{
    int i;
    NXRect eb;

    i = [list count];
    if (i) {
	if (extended) {
	    [[list objectAt:--i] getExtendedBounds:bbox];
	    while (i--) NXUnionRect([[list objectAt:i] getExtendedBounds:&eb], bbox);
	} else {
	    [[list objectAt:--i] getBounds:bbox];
	    while (i--) {
		[[list objectAt:i] getBounds:&eb];
		NXUnionRect(&eb, bbox);
	    }
	}
    } else {
	bbox->size.width = bbox->size.height = 0.0;
    }

    return self;
}

- getBBox:(NXRect *)bbox of:list
{
    return [self getBBox:bbox of:list extended:YES];
}

- recacheSelection
 /*
  * Redraws the selection in the off-screen cache (not the selection cache),
  * then composites it back on to the screen.
  */
{
    NXRect sbounds;
    
    if ([slist count]) {
	[self getBBox:&sbounds of:slist];
	gvFlags.cacheing = YES;
	[self drawSelf:&sbounds :1];
	gvFlags.cacheing = NO;
	[self display:&sbounds :1];
    }

    return self;
}

- getSelection
/*
 * Resets slist by going through the glist and locating all the Graphics
 * which respond YES to the isSelected method.
 */
{
    id g;
    int i;

    [slist free];
    slist = [[List allocFromZone:[self zone]] init];
    gvFlags.groupInSlist = NO;
    i = [glist count];
    while (i--) {
	g = [glist objectAt:i];
	if ([g isSelected]) {
	    [slist insertObject:g at:0];
	    gvFlags.groupInSlist = gvFlags.groupInSlist || [g isKindOf:[Group class]];
	}
    }

    return self;
}

- saveSelection
{
	[saveList free];
	saveList = [slist copy];
	return self;
}

- restoreSelection
{
    	int i;
	id g;
	
    	[slist free];
    	slist = [[List allocFromZone:[self zone]] init];
    	gvFlags.groupInSlist = NO;
    	i = [saveList count];
    	while (i--) {
		g = [saveList objectAt:i];
		[g select];
		[slist insertObject:g at:0];
		gvFlags.groupInSlist = gvFlags.groupInSlist || [g isKindOf:[Group class]];
	}
    	return self;
}

- compositeSelection:(const NXRect *)sbounds from:(int)gstate
/*
 * Composites from the specified gstate whatever part of sbounds is
 * currently visible in the View.
 */
{
    PScomposite(0.0, 0.0, NX_WIDTH(sbounds), NX_HEIGHT(sbounds),
                gstate, NX_X(sbounds), NX_Y(sbounds), NX_SOVER);
    [window flushWindow];
    NXPing();
    return self;
}

- (int)cacheSelection
 /*
  * Caches the selection into the application-wide selection cache
  * window (a window which has alpha in it).  See also: selectionCache:.
  * It draws the objects without their knobbies in the selection cache,
  * but it leaves them selected.  Returns the gstate of the selection
  * cache.
  */
{
    int i;
    NXRect sbounds;
    id selectionCache;

    [self getBBox:&sbounds of:slist];
    selectionCache = [self selectionCache];
    [[selectionCache contentView] lockFocus];
    PSsetgray(NX_WHITE);
    PSsetalpha(0.0);	/* fully transparent */
    PStranslate(- sbounds.origin.x, - sbounds.origin.y);
    sbounds.size.width += 1.0;
    sbounds.size.height += 1.0;
    NXRectFill(&sbounds);
    sbounds.size.width -= 1.0;
    sbounds.size.height -= 1.0;
    PSsetalpha(1.0);	/* fully opaque */
    i = [slist count];
    while (i--) [[[[slist objectAt:i] deselect] draw:NULL] select];
    [Graphic showFastRectFills];
    PStranslate(sbounds.origin.x, sbounds.origin.y);
    [[selectionCache contentView] unlockFocus];

    return [selectionCache gState];
}

/* Other private methods. */

- cacheGraphic:graphic
 /*
  * Draws the graphic into the off-screen cache, then composites
  * it back to the screen.
  * NOTE: This ONLY works if the graphic is on top of the list!
  * That is why it is a private method ...
  */
{
    NXRect eb;

    [[cacheWindow contentView] lockFocus];
    [graphic draw:NULL];
    [Graphic showFastRectFills];
    [[cacheWindow contentView] unlockFocus];
    [self display:[graphic getExtendedBounds:&eb] :1];

    return self;
}

- resetGUP
/*
 * The "GUP" is the Grid User Path.  It is a user path which draws a grid
 * the size of the bounds of the GraphicView.  This gets called whenever
 * the View is resized or the grid spacing is changed.  It sets up all
 * the arguments to DPSDoUserPath() called in drawSelf::.
 */
{
    int x, y, i, j;
    short w, h;

    if (gvFlags.grid < 4) return self;

    x = (int)bounds.size.width / gvFlags.grid;
    y = (int)bounds.size.height / gvFlags.grid;
    gupLength = (x << 2) + (y << 2);
    if (gupCoords) {
	NX_FREE(gupCoords);
	NX_FREE(gupOps);
	NX_FREE(gupBBox);
    }
    NX_ZONEMALLOC([self zone], gupCoords, short, gupLength);
    NX_ZONEMALLOC([self zone], gupOps, char, gupLength >> 1);
    NX_ZONEMALLOC([self zone], gupBBox, short, 4);
    w = bounds.size.width;
    h = bounds.size.height;
    j = 0;
    for (i = 1; i <= y; i++) {
	gupCoords[j++] = 0.0;
	gupCoords[j++] = i * gvFlags.grid;
	gupCoords[j++] = w;
	gupCoords[j] = gupCoords[j-2];
	j++;
    }
    for (i = 1; i <= x; i++) {
	gupCoords[j++] = i * gvFlags.grid;
	gupCoords[j++] = 0.0;
	gupCoords[j] = gupCoords[j-2];
	j++;
	gupCoords[j++] = h;
    }
    i = gupLength >> 1;
    while (i) {
	gupOps[--i] = dps_lineto;
	gupOps[--i] = dps_moveto;
    }
    gupBBox[0] = gupBBox[1] = 0;
    gupBBox[2] = bounds.size.width + 1;
    gupBBox[3] = bounds.size.height + 1;

    return self;
}

#define MOVE_MASK NX_MOUSEUPMASK|NX_MOUSEDRAGGEDMASK

- (BOOL)move:(NXEvent *)event
/*
 * Moves the selection by cacheing the selected graphics into the
 * selection cache, then compositing them repeatedly as the user
 * moves the mouse.  The tracking loop uses TIMER events to autoscroll
 * at regular intervals.  TIMER events do not have valid mouse coordinates,
 * so the last coordinates are saved and restore when there is a TIMER event.
 */
{
    int gstate;
    NXEvent peek;
    NXCoord dx, dy;
    NXTrackingTimer *timer = NULL;
    NXPoint p, start, last, sboundspad;
    NXRect minbounds, sbounds, visibleRect;
    BOOL canScroll, tracking = YES, alternate, horizConstrain = NO, vertConstrain = NO;

    last = event->location;
    alternate = (event->flags & NX_ALTERNATEMASK) ? YES : NO;

    event = [NXApp getNextEvent:MOVE_MASK];
    if (event->type == NX_MOUSEUP) return NO;

//    PShidecursor();

    [self convertPoint:&last fromView:nil];
    [self grid:&last];

    [self lockFocus];

    gstate = [self cacheSelection];
    [self graphicsPerform:@selector(deactivate) andDraw:YES];
    [self getBBox:&sbounds of:slist];
    [self getBBox:&minbounds of:slist extended:NO];
    sboundspad.x = minbounds.origin.x - sbounds.origin.x;
    sboundspad.y = minbounds.origin.y - sbounds.origin.y;
    [self compositeSelection:&sbounds from:gstate];

    [self getVisibleRect:&visibleRect];
    canScroll = !NXEqualRect(&visibleRect, &bounds);

    start = sbounds.origin;

    while (tracking) {
	p = event->location;
	[self convertPoint:&p fromView:nil];
	[self grid:&p];
	dx = p.x - last.x;
	dy = p.y - last.y;
	if (dx || dy) {
	    [self drawSelf:&sbounds :1];
	    if (alternate && (dx || dy)) {
		if (ABS(dx) > ABS(dy)) {
		    horizConstrain = YES;
		    dy = 0.0;
		} else {
		    vertConstrain = YES;
		    dx = 0.0;
		}
		alternate = NO;
	    } else if (horizConstrain) {
		dy = 0.0;
	    } else if (vertConstrain) {
		dx = 0.0;
	    }
	    NXOffsetRect(&sbounds, dx, dy);
	    minbounds.origin.x = sbounds.origin.x + sboundspad.x;
	    minbounds.origin.y = sbounds.origin.y + sboundspad.y;
	    [self tryToPerform:@selector(updateRulers:) with:(void *)&minbounds];
	    if (!canScroll || NXContainsRect(&visibleRect, &sbounds)) {
		[self compositeSelection:&sbounds from:gstate];
		stopTimer(timer);
	    }
	    last = p;
	}
	tracking = (event->type != NX_MOUSEUP);
	if (tracking) {
	    if (canScroll && !NXContainsRect(&visibleRect, &sbounds)) {
		[window disableFlushWindow];
		[self scrollRectToVisible:&sbounds];
		[self getVisibleRect:&visibleRect];
		[self compositeSelection:&sbounds from:gstate];
		[[window reenableFlushWindow] flushWindow];
		startTimer(timer);
	    }
	    p = event->location;
	    if (![NXApp peekNextEvent:MOVE_MASK into:&peek]) {
		event = [NXApp getNextEvent:MOVE_MASK|NX_TIMERMASK];
	    } else {
		event = [NXApp getNextEvent:MOVE_MASK];
	    }
	    if (event->type == NX_TIMER) event->location = p;
	}
    }

    if (canScroll) stopTimer(timer);

//    PSshowcursor();

    p.x = sbounds.origin.x - start.x;
    p.y = sbounds.origin.y - start.y;
    if (p.x || p.y) [self graphicsPerform:@selector(moveBy:) with:(id)&p andDraw:NO];

    [self graphicsPerform:@selector(activate) andDraw:YES];

    [self tryToPerform:@selector(updateRulers:) with:nil];

    [window flushWindow];
    [self unlockFocus];

    return YES;
}

#define DRAG_MASK (NX_MOUSEUPMASK|NX_MOUSEDRAGGEDMASK|NX_TIMERMASK)

- dragSelect:(NXEvent *)event
/*
 * Allows the user the drag out a box to select all objects either
 * intersecting the box, or fully contained within the box (depending
 * on the state of the ALTERNATE key).  After the selection is made,
 * the slist is updated.
 */
{
    id g;
    int i;
    NXPoint p, last, start;
    NXTrackingTimer *timer = NULL;
    BOOL mustContain, shift, canScroll;
    NXRect visibleRect, eb, region, oldRegion;

    p = start = event->location;
    [self convertPoint:&start fromView:nil];
    last = start;

    shift = (event->flags & NX_SHIFTMASK) ? YES : NO;
    mustContain = (event->flags & NX_ALTERNATEMASK) ? YES : NO;

    [self lockFocus];

    [self getVisibleRect:&visibleRect];
    canScroll = !NXEqualRect(&visibleRect, &bounds);
    if (canScroll) startTimer(timer);

    PSsetgray(NX_LTGRAY);
    PSsetlinewidth(0.0);

    event = [NXApp getNextEvent:DRAG_MASK];
    while (event->type != NX_MOUSEUP) {
	if (event->type == NX_TIMER) event->location = p;
	p = event->location;
	[self convertPoint:&p fromView:nil];
	if (p.x != last.x || p.y != last.y) {
	    getRegion(&region, &p, &start);
	    NXInsetRect(&oldRegion, -1.0, -1.0);
	    [window disableFlushWindow];
	    [self drawSelf:&oldRegion :1];
	    if (canScroll) {
		[self scrollRectToVisible:&region];
		[self scrollPointToVisible:&p];
	    }
	    PSrectstroke(region.origin.x, region.origin.y, region.size.width, region.size.height);
	    [self tryToPerform:@selector(updateRulers:) with:(void *)&region];
	    [[window reenableFlushWindow] flushWindow];
	    oldRegion = region;
	    last = p;
	    NXPing();
	}
	p = event->location;
	event = [NXApp getNextEvent:DRAG_MASK];
    }

    if (canScroll) stopTimer(timer);

    for (i = [glist count] - 1; i >= 0; i--) {
 	g = [glist objectAt:i];
	[g getExtendedBounds:&eb];
	if (![g isLocked] && ![g isSelected] && 
	    ((mustContain && NXContainsRect(&region, &eb)) ||
	     (!mustContain && NXIntersectsRect(&region, &eb)))) {
	    [slist insertObject:[g select] at:0];
	    gvFlags.groupInSlist = gvFlags.groupInSlist || [g isKindOf:[Group class]];
	}
    }

    NXInsetRect(&region, -1.0, -1.0);
    [self drawSelf:&region :1];
    [self recacheSelection];

    [self tryToPerform:@selector(updateRulers:) with:nil];

    [self unlockFocus];

    return self;
}

/* Public interface methods. */

- dirty
{
    gvFlags.dirty = YES;
    [window setDocEdited:YES];
    return self;
}

- (BOOL)isDirty
{
    return gvFlags.dirty;
}

- (BOOL)isEmpty
{
    return [glist count] == 0;
}

- (BOOL)hasEmptySelection
{
    return [slist count] == 0;
}

- graphicsPerform:(SEL)aSelector andDraw:(BOOL)flag
/*
 * Performs the given aSelector on each member of the slist, then
 * recaches and redraws the larger of the area covered by the objects before
 * the selector was applied and the area covered by the objects after the
 * selector was applied.  If you want to perform a method on each item
 * in the slist and NOT redraw, then use the List method makeObjectsPerform:.
 */
{
    id g;
    int i, count;
    NXRect eb, affectedBounds;

    if (flag) {
	count = [slist count];
	if (count) {
	    [[slist objectAt:0] getExtendedBounds:&affectedBounds];
	    for (i = 1; i < count; i++) {
		g = [slist objectAt:i];
		NXUnionRect([g getExtendedBounds:&eb], &affectedBounds);
	    }
	    for (i = 0; i < count; i++) {
		g = [slist objectAt:i];
		[g perform:aSelector];
		NXUnionRect([g getExtendedBounds:&eb], &affectedBounds);
	    }
	    [self cache:&affectedBounds];
	}	
    } else {
	[slist makeObjectsPerform:aSelector];
    }

    return self;
}

- graphicsPerform:(SEL)aSelector with:(void *)argument andDraw:(BOOL)flag
{
    id g;
    int i, count;
    NXRect eb, affectedBounds;

    if (flag) {
	count = [slist count];
	if (count) {
	    [[slist objectAt:0] getExtendedBounds:&affectedBounds];
	    for (i = 1; i < count; i++) {
		g = [slist objectAt:i];
		NXUnionRect([g getExtendedBounds:&eb], &affectedBounds);
	    }
	    for (i = 0; i < count; i++) {
		g = [slist objectAt:i];
		[g perform:aSelector with:argument];
		NXUnionRect([g getExtendedBounds:&eb], &affectedBounds);
	    }
	    [self cache:&affectedBounds];
	}
    } else {
	[slist makeObjectsPerform:aSelector with:argument];
    }

    return self;
}

- graphicsPerformSingle:(SEL)aSelector with:(void *)argument on:(id) g
{
    NXRect  affectedBounds;

	    	[g getExtendedBounds:&affectedBounds];
		[g perform:aSelector with:argument];
	    	[self cache:&affectedBounds];

    return self;
}

- cache:(const NXRect *)rect
/*
 * Draws all the Graphics intersected by rect into the off-screen cache,
 * then composites the rect back to the screen (but does NOT flushWindow).
 */
{
    gvFlags.cacheing = YES;
    [self drawSelf:rect :1];
    gvFlags.cacheing = NO;
    if ([self canDraw]) {
	[self lockFocus];
	[self drawSelf:rect :1];
	[self unlockFocus];
    }
    return self;
}

- insertGraphic:graphic
/*
 * Inserts the specified graphic into the glist and draws it.
 * The new graphic will join the selection, not replace it.
 */
{
    NXRect eb;

    if (graphic) {
	[slist insertObject:graphic at:0];
	[glist insertObject:graphic at:0];
	[self cache:[graphic getExtendedBounds:&eb]];
	if ([graphic isKindOf:[Group class]]) gvFlags.groupInSlist = YES;
	[window flushWindow];
	DIRTY(YES);
    }

    return self;
}

- insertGraphicNoSelect:graphic
/*
 * Inserts the specified graphic into the glist and draws it.
 */
{
    NXRect eb;

    if (graphic) {
	[graphic deselect];
	[glist insertObject:graphic at:0];
	[self cache:[graphic getExtendedBounds:&eb]];
	[window flushWindow];
	DIRTY(YES);
    }

    return self;
}

- removeGraphic:graphic
/*
 * Removes the graphic from the GraphicView and redraws.
 */
{
    int i;
    NXRect eb;
    id g = nil;

    if (!graphic) return self;

    i = [glist count];
    while (g != graphic && i--) g = [glist objectAt:i];
    if (g == graphic) {
	[g getExtendedBounds:&eb];
	[glist removeObjectAt:i];
	[slist removeObject:g];
	if ([g isKindOf:[Group class]]) gvFlags.groupInSlist = checkForGroup(slist);
	[self cache:&eb];
	[window flushWindow];
	DIRTY(YES);
	[NXApp delayedFree:g];
    }

    return self;
}

- selectedGraphic
/*
 * If there is one and only one Graphic selected, this method returns it.
 */
{
    if ([slist count] == 1) {
	id g = [slist objectAt:0];
	return [g isKindOf:[Group class]] ? nil : g;
    } else {
	return nil;
    }
}

/* Methods to modify the grid of the GraphicView. */

- (int)gridSpacing
{
    return gvFlags.gridDisabled ? 1 : gvFlags.grid;
}

- (BOOL)gridIsVisible
{
    return gvFlags.showGrid;
}

- (BOOL)gridIsEnabled
{
    return !gvFlags.gridDisabled;
}

- (float)gridGray
{
    return gridGray;
}

- setGridSpacing:(int)gridSpacing
{
    if (gridSpacing != gvFlags.grid && gridSpacing > 0 && gridSpacing < 256) {
	gvFlags.grid = gridSpacing;
	if (gvFlags.showGrid) {
	    [self resetGUP];
	    [self cache:&bounds];
	    [window flushWindow];
	}
    }
    return self;
}

- setGridEnabled:(BOOL)flag
{
    gvFlags.gridDisabled = flag ? NO : YES;
    return self;
}

- setGridVisible:(BOOL)flag
{
    if (gvFlags.showGrid != flag) {
	gvFlags.showGrid = flag;
	if (flag) [self resetGUP];
	[self cache:&bounds];
	[window flushWindow];
    }
    return self;
}

- setGridGray:(float)gray
{
    if (gray != gridGray) {
	gridGray = gray;
	if (gvFlags.showGrid) {
	    [self cache:&bounds];
	    [window flushWindow];
	}
    }
    return self;
}

- setGridSpacing:(int)gridSpacing andGray:(float)gray
{
    if (gray != gridGray || (gridSpacing != gvFlags.grid && gridSpacing > 0 && gridSpacing < 256)) {
	gridGray = gray;
	if (gvFlags.grid != gridSpacing && gridSpacing > 0 && gridSpacing < 256) {
	    gvFlags.grid = gridSpacing;
	    if (gvFlags.showGrid) [self resetGUP];
	}
	if (gvFlags.showGrid) {
	    [self cache:&bounds];
	    [window flushWindow];
	}
    }
    return self;
}

- grid:(NXPoint *)p
{
    grid(*p);
    return self;
}

/* Public methods for importing foreign data types into the GraphicView */

- placeGraphic:graphic at:(const NXPoint *)location
/*
 * Places the graphic centered at the given location on the page.
 * If the graphic is too big, the user is asked whether the graphic
 * should be scaled.
 */
{
    int scale;
    NXPoint offset;
    float sx, sy, factor;
    NXRect gbounds, myBounds;

    if (graphic) {
	[graphic getExtendedBounds:&gbounds];
	if (gbounds.size.width > bounds.size.width || gbounds.size.height > bounds.size.height) {
	    scale = NXRunAlertPanel("Load Image",
		"The image is too large to fit on the page.  Scale it to fit?",
		"Scale", "Don't Scale", "Cancel");
	    if (scale < 0) {
		[graphic free];
		return self;
	    } else if (scale > 0) {
		sx = (bounds.size.width / gbounds.size.width) * 0.95;
		sy = (bounds.size.height / gbounds.size.height) * 0.95;
		factor = MIN(sx, sy);
		gbounds.size.width *= factor;
		gbounds.size.height *= factor;
		[graphic sizeTo:&gbounds.size];
	    }
	}
	if (location) [graphic centerAt:location];
	[graphic getExtendedBounds:&gbounds];
	myBounds = bounds;
	NXContainRect(&myBounds, &gbounds);
	offset.x = bounds.origin.x - myBounds.origin.x;
	offset.y = bounds.origin.y - myBounds.origin.y;
	if (offset.x || offset.y) [graphic moveBy:&offset];
	[self deselectAll:self];
	[self insertGraphic:graphic];
	[self scrollGraphicToVisible:graphic];
    }

    return graphic;
}

- loadImageFromStream:(NXStream *)stream at:(const NXPoint *)location allowAlpha:(BOOL)alphaOk
/*
 * Creates a new Image object using the PostScript or TIFF found in the
 * given stream and inserts it as the only item in the selection (i.e.
 * it deslects everything else).   The new Graphic is centered at the
 * given point p, and the GraphicView is scrolled to make the Graphic
 * visible.
 */
{
    return [self placeGraphic:[[Image allocFromZone:[self zone]] initFromStream:stream allowAlpha:alphaOk] at:location];
}


/* Writing data in different forms (other than the internal Draw format) */

- writePSToStream:(NXStream *)stream
/*
 * Writes out the PostScript generated by drawing all the objects in the
 * glist.  The bounding box of the generated encapsulated PostScript will
 * be equal to the bounding box of the objects in the glist (not the
 * bounds of the view).
 */
{
    NXRect bbox;

    if (stream) {
	[self getBBox:&bbox of:glist];
	[self copyPSCodeInside:&bbox to:stream];
    }

    return self;
}

- writeTIFFToStream:(NXStream *)stream
/*
 * Images all of the objects in the glist and writes out the result in
 * the Tagged Image File Format (TIFF).  The image WILL have alpha in it.
 */
{
    int size;
    NXRect sbounds;
    NXImageInfo image;
    unsigned char *data;
    id cache, savedslist;

    if (!stream) return self;

    savedslist = slist;
    slist = glist;
    [self cacheSelection];
    [self getBBox:&sbounds of:slist];
    slist = savedslist;
    cache = [[self selectionCache] contentView];
    [cache lockFocus];
    sbounds.origin.x = sbounds.origin.y = 0.0;
    NXSizeBitmap(&sbounds, &size, &image.width, &image.height,
		 &image.bitsPerSample, &image.samplesPerPixel,
		 &image.planarConfig, &image.photoInterp);
    NX_MALLOC(data, unsigned char, size);
    NXReadBitmap(&sbounds, image.width, image.height, image.bitsPerSample,
	image.samplesPerPixel, image.planarConfig, image.photoInterp,
	data, data + (size >> 1), NULL, NULL, NULL);
    [cache unlockFocus];
    NXWriteTIFF(stream, &image, data);
    NX_FREE(data);

    return self;
}


/* Methods overridden from superclass. */

- sizeTo:(NXCoord)width :(NXCoord)height
/*
 * Overrides View's sizeTo:: so that the cacheWindow is resized when
 * the View is resized.
 */
{
    if (width != bounds.size.width || height != bounds.size.height) {
	[super sizeTo:width :height];
	[cacheWindow free];
	cacheWindow = createCache(&bounds.size, [self zone]);
	[self resetGUP];
	[self cache:&bounds];
    }
    return self;
}

- mouseDown:(NXEvent *)event
/*
 * This method handles a mouse down.
 *
 * If a current tool is in effect, then the mouse down causes a new
 * Graphic to begin being created.  Otherwise, the selection is modified
 * either by adding elements to it or removing elements from it, or moving
 * it.  Here are the rules:
 *
 * Tool in effect
 *    Shift OFF
 *	create a new Graphic which becomes the new selection
 *    Shift ON
 *	create a new Graphic and ADD it to the current selection
 *    Control ON
 *	leave creation mode, and start selection
 * Otherwise
 *    Shift OFF
 *	a. Click on a selected Graphic -> select graphic further back
 *	b. Click on an unselected Graphic -> that Graphic becomes selection
 *    Shift ON
 *	a. Click on a selected Graphic -> remove it from selection
 *	b. Click on unselected Graphic -> add it to selection
 *    Alternate ON
 *	if no affected graphic, causes drag select to select only objects
 *	completely contained within the dragged box.
 *
 * Essentially, everything works as one might expect except the ability to
 * select a Graphic which is deeper in the list (i.e. further toward the
 * back) by clicking on the currently selected Graphic.
 *
 * This is a very hairy mouseDown:.  Most need not be this scary.
 */
{
    NXPoint p;
    NXRect eb;
    int i, corner, oldMask;
    id factory, g = nil, startg = nil;
    BOOL shift, control, gotHit = NO, deepHit = NO;

    /*
     * You only need to do the following line in a mouseDown: method if
     * you receive this message because one of your subviews gets the
     * mouseDown: and does not respond to it (thus, it gets passed up the
     * responder chain to you).  In this case, our editView receives the
     * mouseDown:, but doesn't do anything about it, and when it comes
     * to us, we want to become the first responder.
     *
     * Normally you won't have a subview which doesn't do anything with
     * mouseDown:, in which case, you need only return YES from the
     * method acceptsFirstResponder (see that method below) and will NOT
     * need to do the following makeFirstResponder:.  In other words,
     * don't put the following line in your mouseDown: implementation!
     *
     * Sorry about confusing this issue ... 
     */
  
    if ([window firstResponder] != self) [window makeFirstResponder:self];

    shift = (event->flags & NX_SHIFTMASK) ? YES : NO;
    control = (event->flags & NX_CONTROLMASK) ? YES : NO;

    p = event->location;
    [self convertPoint:&p fromView:nil];

    oldMask = [window addToEventMask:NX_MOUSEDRAGGEDMASK|NX_MOUSEUPMASK];

    factory = [self currentGraphic];
    if (!control && factory) {
	if ([factory isEditable]) {	/* if editable, try to edit one */
	    i = 0;
	    g = [glist objectAt:i++];
	    while (g != nil) {
		if ([g isKindOf:factory] && [g hit:&p]) {
		    if ([g isSelected]) {
			[g deselect];
			[self cache:[g getExtendedBounds:&eb]];
			[slist removeObject:g];
			if ([g isKindOf:[Group class]]) gvFlags.groupInSlist = checkForGroup(slist);
			DIRTY(YES);
		    }
		    [g edit:event in:editView];
		    break;
		}
		g = [glist objectAt:i++];
	    }
	}
	if (!g) {	/* not editing or no editable graphic found */
	    g = [[factory allocFromZone:[self zone]] init];
	    if ([NXApp respondsTo:@selector(inspectorPanel)]) {
		[[[NXApp inspectorPanel] delegate] initializeGraphic:g];
	    }
	    if ([g create:event in:self]) {
		if (!shift) [self deselectAll:self];
		[glist insertObject:g at:0];
		if ([g isSelected]) [slist insertObject:g at:0];
		gvFlags.groupInSlist = gvFlags.groupInSlist || [g isKindOf:[Group class]];
		[self cacheGraphic:g];
		[g edit:NULL in:editView];
		DIRTY(YES);
	    } else {
		[g free];
	    }
	}
    } else {		/* selecting/resizing/moving */
	i = 0;
	g = [glist objectAt:i++];
	while (g != nil && !gotHit) {
	    corner = [g knobHit:&p];
	    if (corner > 0) {			/* corner hit */
		gotHit = YES;
		[g resize:event by:corner in:self];
		DIRTY(YES);
	    } else if (corner) {		/* complete miss */
		g = [glist objectAt:i++];
	    } else g = nil;			/* non-corner opaque hit */
	}
	i = 0;
	if (!gotHit) g = [glist objectAt:i++];
	while (g && !gotHit && !deepHit) {
	    if ([g isSelected] && [g hit:&p]) {
		if (shift) {
		    gotHit = YES;
		    [g deselect];
		    [self cache:[g getExtendedBounds:&eb]];
		    [slist removeObject:g];
		    if ([g isKindOf:[Group class]]) gvFlags.groupInSlist = checkForGroup(slist);
		    DIRTY(YES);
		} else {
		    gotHit = [self move:event];
		    if (!gotHit) {
			deepHit = ![g isOpaque];
			if (!deepHit) gotHit = YES;
		    } else {
			DIRTY(YES);
		    }
		}
	    }
	    g = [glist objectAt:i++];
	}
	startg = g;
	if (!gotHit) do {
	    if (!g) {
		i = 0;
		g = [glist objectAt:i++];
	    }
	    if (![g isSelected] && [g hit:&p]) {
		gotHit = YES;
		if (!shift) {
		    [self deselectAll:self];
		    [slist addObject:g];
		    gvFlags.groupInSlist = [g isKindOf:[Group class]];
		}
		[g select];
		DIRTY(YES);
		if (shift) [self getSelection];
		if (deepHit || ![self move:event]) [self cache:[g getExtendedBounds:&eb]];
	    } else {
		g = [glist objectAt:i++];
	    }
	} while (!gotHit && g != startg);

	if (!gotHit && !deepHit) {
	    if (!shift) {
		DIRTY([slist count] > 0);
		[self lockFocus];
		[self deselectAll:self];
		[self unlockFocus];
	    }
	    [self dragSelect:event];
	}
    }

    [window flushWindow];
    [window setEventMask:oldMask];

    return self;
}

- drawSelf:(const NXRect *)rects :(int)rectCount
/*
 * Draws the GraphicView.
 *
 * If cacheing is on or if NXDrawingStatus != NX_DRAWING, then all the
 * graphics which intersect the specified rectangles will be drawn (and
 * clipped to those rectangles).  Otherwise, the specified rectangles
 * are composited to the screen from the off-screen cache.
 */
{
    NXRect *rp;
    NXRect r, visibleRect;
    int i, j, gstate;

    if (rects == NULL) return self;

    if (gvFlags.cacheing || NXDrawingStatus != NX_DRAWING) {
	if (NXDrawingStatus == NX_DRAWING) {
	    [[cacheWindow contentView] lockFocus];
	    PSsetgray(NX_WHITE);
	    for (j = (rectCount > 1) ? 1 : 0; j < rectCount; j++) {
		NXRectFill(&rects[j]);
		if (gvFlags.showGrid && gvFlags.grid >= 4) {
		    NXRectClip(&rects[j]);
		    PSsetlinewidth(0.0);
		    PSsetgray(gridGray);
		    DPSDoUserPath(gupCoords, gupLength, dps_short,
				  gupOps, gupLength >> 1, gupBBox, dps_ustroke);
		    PSsetgray(NX_WHITE);
		}
	    }
	}
	for (j = (rectCount > 1) ? 1 : 0; j < rectCount; j++) {
	    NXRectClip(&rects[j]);
	    i = [glist count];
	    while (i--) [[glist objectAt:i] draw:&rects[j]];
	}
	[Graphic showFastRectFills];
	if (NXDrawingStatus == NX_DRAWING) [[cacheWindow contentView] unlockFocus];
    }

    if (!gvFlags.cacheing && NXDrawingStatus == NX_DRAWING) {
	gstate = [cacheWindow gState];
	[self getVisibleRect:&visibleRect];
	for (j = 0; j < rectCount; j++) {
	    rp = &r;
	    r = rects[j];
	    if (!NXEqualRect(rp, &visibleRect)) rp = NXIntersectionRect(&visibleRect, rp);
	    if (rp) {
		PScomposite(NX_X(rp), NX_Y(rp), NX_WIDTH(rp), NX_HEIGHT(rp), gstate,
			    NX_X(rp), NX_Y(rp), NX_SOVER);
	    }
	}
    }

    return self;
}

- keyDown:(NXEvent *)event
/*
 * Handles one of the arrow keys being pressed.
 * Note that since it might take a while to actually move the selection
 * (if it is large), we check to see if a bunch of arrow key events have
 * stacked up and move them all at once.
 */
{
    NXPoint p;
    NXEvent e;
    NXCoord delta;
    BOOL gotOne, first;
    NXEvent* eptr = event;

    if ((event->data.key.charSet != NX_ASCIISET ||
	 event->data.key.charCode != 127) &&
	(event->data.key.charSet != NX_SYMBOLSET ||
	 (event->data.key.charCode != LEFTARROW &&
	  event->data.key.charCode != RIGHTARROW &&
	  event->data.key.charCode != DOWNARROW &&
	  event->data.key.charCode != UPARROW))) {
	return [super keyDown:event];
    }

    if (event->data.key.charSet == NX_ASCIISET) return [self delete:self];

    p.x = p.y = 0.0;
    delta = KeyMotionDeltaDefault;
    delta = floor(delta / GRID) * GRID;
    delta = MAX(delta, GRID);

    first = YES;
    do {
	gotOne = NO;
	if (eptr->data.key.charSet == NX_SYMBOLSET) {
	    switch (eptr->data.key.charCode) {
		case LEFTARROW:
		    p.x -= delta;
		    gotOne = YES;
		    break;
		case RIGHTARROW:
		    p.x += delta;
		    gotOne = YES;
		    break;
		case UPARROW:
		    p.y += delta;
		    gotOne = YES;
		    break;
		case DOWNARROW:
		    p.y -= delta;
		    gotOne = YES;
		    break;
		default:
		    break;
	    }
	}
	if (eptr && gotOne && !first) [NXApp getNextEvent:NX_KEYDOWNMASK];
	first = NO;
    } while (gotOne && (eptr = [NXApp peekNextEvent:NX_KEYDOWNMASK into:&e]));

    if (p.x || p.y) {
	[self graphicsPerform:@selector(moveBy:) with:(id)&p andDraw:YES];
	[[self window] flushWindow];
	NXPing();
    }

    return self;
}


- copySelectionAsPS:(NXStream *)stream
{
    id savedglist;

    if (stream && [slist count]) {
	savedglist = glist;
	glist = slist;
	[self writePSToStream:stream];
	glist = savedglist;
    } else {
	return nil;
    }

    return self;
}

- copySelectionAsTIFF:(NXStream *)stream
{
    id savedglist;

    if (stream && [slist count]) {
	savedglist = glist;
	glist = slist;
	[self writeTIFFToStream:stream];
	glist = savedglist;
    } else {
	return nil;
    }

    return self;
}

- copySelectionToStream:(NXStream *)stream
{
    NXTypedStream *ts;

    if ([slist count]) {
	ts = NXOpenTypedStream(stream, NX_WRITEONLY);
	NXWriteRootObject(ts, slist);
	NXCloseTypedStream(ts);
    } else {
	return nil;
    }

    return self;
}

/*
 * Target/Action methods.
 */

/* Copying the selection to a stream. */
/*
 * These two method set and get the factory object used to create new
 * Graphic objects (i.e. the subclass of Graphic to use).
 * They are kind of weird since they check to see if the
 * Application object knows what the current graphic is.  If it does, then
 * it lets it handle these methods.  Otherwise, it determines the
 * current graphic by querying the sender to find out what its title is
 * and converts that title to the name to a factory object.  This allows
 * the GraphicView to stand on its own, but also use an application wide
 * tool palette if available.
 * If the GraphicView handles the current graphic by itself, it does so
 * by querying the sender of setCurrentGraphic: to find out its title.
 * It assumes, then, that that title is the name of the factory object to
 * use and calls objc_getClass() to get a pointer to it.
 * If the application is not control what our current graphic is, then
 * we restrict creations to be made only when the control key is down.
 * Otherwise, it is the other way around (control key leaves creation
 * mode).  This is due to the fact that the application can be smart
 * enough to set appropriate cursors when a tool is on.  The GraphicView
 * can't be.
 */

- currentGraphic
{
    if ([NXApp respondsTo:@selector(currentGraphic)]) {
	return [NXApp currentGraphic];
    } else {
	return currentGraphic;
    }
}

- setCurrentGraphic:sender
{
    currentGraphic = objc_getClass([[sender selectedCell] title]);
    return self;
}

/* Pasteboard-related target/action methods */

- delete:sender
{
    int i;

    i = [slist count];
    if (i > 0) {
	[self graphicsPerform:@selector(deactivate) andDraw:YES];
	[self graphicsPerform:@selector(activate) andDraw:NO];
	DIRTY(YES);
	while (i--) [glist removeObject:[slist objectAt:i]];
	if (originalPaste == [slist objectAt:0]) {
	    [slist removeObjectAt:0];
	    gvFlags.freeOriginalPaste = YES;
	}
	[slist freeObjects];
	[slist free];
	slist = [[List allocFromZone:[self zone]] init];
	gvFlags.groupInSlist = NO;
	[window flushWindow];
    }

    return self;
}

- cut:sender
/*
 * Calls copy: then delete:.
 */
{
    if ([slist count] > 0) {
	[self copy:sender];
	[self delete:sender];
	consecutivePastes = 0;
	return self;
    } else {
	return nil;
    }

}

- copy:sender
{
    if ([slist count]) {
	[self copyToPasteboard:[Pasteboard new]];
	lastPastedChangeCount = [[Pasteboard new] changeCount];
	consecutivePastes = 1;
	if (gvFlags.freeOriginalPaste) [originalPaste free];
	gvFlags.freeOriginalPaste = NO;
	originalPaste = [slist objectAt:0];
    }
    return self;
}

static BOOL includesType(NXAtom *types, NXAtom type)
{
    while (types && *types) {
	if (type == *types) return YES;
	types++;
    }
    return NO;
}

- copyToPasteboard:pboard types:(NXAtom *)typesList
/*
 * Puts all the objects in the slist into the Pasteboard by archiving
 * the slist itself.  Also registers the PostScript and TIFF types since
 * the GraphicView knows how to convert its internal type to PostScript
 * or TIFF via the copy{PS,TIFF}ToPasteboardFrom: methods.
 */
{
    char *data;
    NXStream *stream;   
    const char *types[3];
    int i = 0, length, maxlen;

    if ([slist count]) {
	types[i++] = DrawPboardType;
	if (!typesList || includesType(typesList, NXPostScriptPboardType)) {
	    types[i++] = NXPostScriptPboardType;
	}
	if (!typesList || includesType(typesList, NXTIFFPboardType)) {
	    types[i++] = NXTIFFPboardType;
	}
	stream = NXOpenMemory(NULL, 0, NX_WRITEONLY);
	[self copySelectionToStream:stream];
	NXGetMemoryBuffer(stream, &data, &length, &maxlen);
	[pboard declareTypes:types num:i owner:[self class]];
	[pboard writeType:DrawPboardType data:data length:length];
	NXCloseMemory(stream, NX_FREEBUFFER);
	return self;
    } else {
	return nil;
    }
}

- copyToPasteboard:pboard
{
    return [self copyToPasteboard:pboard types:NULL];
}

- paste:sender
/*
 * Pastes from the normal pasteboard.
 * This paste implements "smart paste" which goes like this: if the user
 * pastes in a single item (a Group is considered a single item), then
 * pastes that item again and moves that second item somewhere, then
 * subsequent pastes will be positioned at the same offset between the
 * first and second pastes (this is also known as "transform again").
 */
{
    id g, pblist;
    NXPoint offset;
    NXAtom type;
    NXRect originalBounds, secondBounds;
    static id secondPaste;
    static NXPoint pasteOffset;

    if (!(pblist = [self pasteFromPasteboard:[Pasteboard new]])) return nil;

    g = [pblist objectAt:0];

    if ([g isKindOf:[Image class]]) {
	[pblist free];
	return self;
    }

    type = drawPasteType([[Pasteboard new] types]);
    if (type == DrawPboardType) {
	if (lastPastedChangeCount != [[Pasteboard new] changeCount]) {
	    consecutivePastes = 0;
	    lastPastedChangeCount = [[Pasteboard new] changeCount];
	    if (gvFlags.freeOriginalPaste) [originalPaste free];
	    gvFlags.freeOriginalPaste = NO;
	    originalPaste = g;
	} else {
	    if (consecutivePastes == 1) {	/* smart paste */
		pasteOffset.x = 10.0;
		pasteOffset.y = -10.0;
		secondPaste = g;
	    } else if ((consecutivePastes==2) && [pblist count]==1) {
		[originalPaste getBounds:&originalBounds];
		[secondPaste getBounds:&secondBounds];
		pasteOffset.x = secondBounds.origin.x - originalBounds.origin.x;
		pasteOffset.y = secondBounds.origin.y - originalBounds.origin.y;
	    }
	    offset.x = pasteOffset.x * consecutivePastes;
	    offset.y = pasteOffset.y * consecutivePastes;
	    [self graphicsPerform:@selector(moveBy:) with:&offset andDraw:NO];
	}
	consecutivePastes++;
	[self recacheSelection];
    }

    [pblist free];

    return self;
}

- pasteFromPasteboard:pboard
/*
 * Pastes any type available from the specified Pasteboard into the GraphicView.
 * If the type in the Pasteboard is the internal type, then the objects
 * are simply added to the slist and glist.  If it is PostScript or TIFF,
 * then an Image object is created using the contents of
 * the Pasteboard.  Returns a list of the pasted objects (which should be freed
 * by the caller).
 */
{
    char *data;
    int i, length;
    NXAtom type;
    NXStream *stream;
    NXTypedStream *ts;
    id g = nil, pblist = nil;

    type = drawPasteType([pboard types]);
    if (type) {
	[self deselectAll:self];
	[pboard readType:type data:&data length:&length];
	stream = NXOpenMemory(data, length, NX_READONLY);
	if (type == DrawPboardType) {
	    ts = NXOpenTypedStream(stream, NX_READONLY);
	    pblist = NXReadObject(ts);
	    i = [pblist count];
	    if (i) {
		DIRTY(YES);
		[self deselectAll:self];
		while (i--) {
		    g = [pblist objectAt:i];
		    [slist insertObject:g at:0];
		    [glist insertObject:g at:0];
		    gvFlags.groupInSlist = gvFlags.groupInSlist || [g isKindOf:[Group class]];
		}
	    } else {
		[pblist free];
		pblist = nil;
	    }
	    NXCloseTypedStream(ts);
	    NXCloseMemory(stream, NX_FREEBUFFER);
	} else {
	    if (type == NXPostScriptPboardType || type == NXTIFFPboardType) {
		g = [self loadImageFromStream:stream at:NULL allowAlpha:(type == NXTIFFPboardType)];
		if (g) {
		    pblist = [[List allocFromZone:[self zone]] initCount:1];
		    [pblist addObject:g];
		}
		NXCloseMemory(stream, NX_SAVEBUFFER);
	    } else {
		NXCloseMemory(stream, NX_FREEBUFFER);	// shouldn't happen!
	    }
	}
    }

    return pblist;
}

/* Other target/action methods */

- selectAll:sender
/*
 * Selects all the items in the glist.
 */
{
    id g;
    int i;
    NXRect visibleRect;

    i = [glist count];
    if (!i) return self;

    DIRTY(YES);
    [slist free];
    slist = [[List allocFromZone:[self zone]] init];
    [[cacheWindow contentView] lockFocus];
    while (i--) {
	g = [glist objectAt:i];
	if (![g isLocked]) {
	    [g select];
	    [g draw:NULL];
	    [slist insertObject:g at:0];
	    gvFlags.groupInSlist = gvFlags.groupInSlist || [g isKindOf:[Group class]];
	}
    }
    [Graphic showFastRectFills];
    [[cacheWindow contentView] unlockFocus];
    [self getVisibleRect:&visibleRect];
    if ([self canDraw]) {
	[self lockFocus];
	[self drawSelf:&visibleRect :1];
	[self unlockFocus];
    }
    if (sender != self) [window flushWindow];
    
    return self;
}

- deselectAll:sender
/*
 * Deselects all the items in the slist.
 */
{
    NXRect sbounds;

    if ([slist count] > 0) {
	DIRTY(YES);
	[self getBBox:&sbounds of:slist];
	[self graphicsPerform:@selector(deselect) andDraw:NO];
	[self cache:&sbounds];
	[slist free];
	slist = [[List allocFromZone:[self zone]] init];
	gvFlags.groupInSlist = NO;
	if (sender != self) [window flushWindow];
    }

    return self;
}

- lock:sender
/*
 * Locks all the items in the selection so that they can't be selected
 * or resized or moved.  Useful if there are some Graphics which are getting
 * in your way.  Undo this with unlock:.
 */
{
    gvFlags.locked = ([slist count] > 0);
    if (gvFlags.locked) {
	[slist makeObjectsPerform:@selector(lock)];
	[self deselectAll:sender];
    }
    return self;
}

- unlock:sender
{
    [glist makeObjectsPerform:@selector(unlock)];
    gvFlags.locked = NO;
    return self;
}

- bringToFront:sender
/*
 * Brings each of the items in the slist to the front of the glist.
 * The item in the front of the slist will be the new front element
 * in the glist.
 */
{
    int i;

    i = [slist count];
    if (i) {
	DIRTY(YES);
	while (i--) [glist insertObject:[glist removeObject:[slist objectAt:i]] at:0];
	[self recacheSelection];
    }

    return self;
}

- sendToBack:sender
{
    int i, count;

    count = [slist count];
    if (count > 0) {
	DIRTY(YES);
	for (i = 0; i < count; i++) [glist addObject:[glist removeObject:[slist objectAt:i]]];
	[self recacheSelection];
    }

    return self;
}

- group:sender
/*
 * Creates a new Group object with the current slist as its member list.
 * See the Group class for more info.
 */
{
    id g;
    int i;
    NXRect eb;

    i = [slist count];
    if (i > 1) {
	DIRTY(YES);
	while (i--) [glist removeObject:[slist objectAt:i]];
	g = [[Group allocFromZone:[self zone]] initList:slist];
	[glist insertObject:g at:0];
	slist = [[List allocFromZone:[self zone]] init];
	[slist addObject:g];
	gvFlags.groupInSlist = YES;
	[self cache:[g getExtendedBounds:&eb]];
	if (sender != self) [window flushWindow];
    }

    return self;
}


- ungroup:sender
/*
 * Goes through the slist and ungroups any Group objects in it.
 * Does not descend any further than that (i.e. all the Group objects
 * in the slist are ungrouped, but any Group objects in those ungrouped
 * objects are NOT ungrouped).
 */
{
    id g;
    int i, k;
    NXRect sbounds;
    BOOL found = NO;

    [self getBBox:&sbounds of:slist];
    i = [slist count];
    while (i--) {
	g = [slist objectAt:i];
	if ([g isKindOf:[Group class]]) {
	    k = [glist indexOf:g];
	    [glist removeObjectAt:k];
	    found = YES;
	    [[g transferSubGraphicsTo:glist at:k] free];
	}
    }

    if (found) {
	DIRTY(YES);
	[self cache:&sbounds];
	if (sender != self) [window flushWindow];
	[self getSelection];
    }

    return self;
}

- alignLeftEdges:sender
{
    int i;
    NXRect rect;
    NXCoord minEdge = 10000.0;

    for (i = [slist count]-1; i >= 0; i--) {
	[[slist objectAt:i] getBounds:&rect];
	if (rect.origin.x < minEdge) minEdge = rect.origin.x;
    }
    [self graphicsPerform:@selector(moveLeftEdgeTo:) with:&minEdge andDraw:YES];
    [window flushWindow];

    return self;
}

- alignRightEdges:sender
{
    int i;
    NXRect rect;
    NXCoord maxEdge = 0.0;

    for (i = [slist count]-1; i >= 0; i--) {
	[[slist objectAt:i] getBounds:&rect];
	if (rect.origin.x + rect.size.width > maxEdge) maxEdge = rect.origin.x + rect.size.width;
    }
    [self graphicsPerform:@selector(moveRightEdgeTo:) with:&maxEdge andDraw:YES];
    [window flushWindow];

    return self;
}

- alignBottomEdges:sender
{
    int i;
    NXRect rect;
    NXCoord minEdge = 10000.0;

    for (i = [slist count]-1; i >= 0; i--) {
	[[slist objectAt:i] getBounds:&rect];
	if (rect.origin.y < minEdge) minEdge = rect.origin.y;
    }
    [self graphicsPerform:@selector(moveBottomEdgeTo:) with:&minEdge andDraw:YES];
    [window flushWindow];

    return self;
}

- alignTopEdges:sender
{
    int i;
    NXRect rect;
    NXCoord maxEdge = 0.0;

    for (i = [slist count]-1; i >= 0; i--) {
	[[slist objectAt:i] getBounds:&rect];
	if (rect.origin.y + rect.size.height > maxEdge) maxEdge = rect.origin.y + rect.size.height;
    }
    [self graphicsPerform:@selector(moveTopEdgeTo:) with:&maxEdge andDraw:YES];
    [window flushWindow];

    return self;
}

- alignVerticalCenters:sender
{
    int i;
    NXRect rect;
    NXCoord minEdge = 10000.0;

    for (i = [slist count]-1; i >= 0; i--) {
	[[slist objectAt:i] getBounds:&rect];
	if (rect.origin.x + floor(rect.size.width / 2.0) < minEdge) {
	    minEdge = rect.origin.x + floor(rect.size.width / 2.0);
	}
    }
    [self graphicsPerform:@selector(moveHorizontalCenterTo:) with:&minEdge andDraw:YES];
    [window flushWindow];

    return self;
}

- alignHorizontalCenters:sender
{
    int i;
    NXRect rect;
    NXCoord minEdge = 10000.0;

    for (i = [slist count]-1; i >= 0; i--) {
	[[slist objectAt:i] getBounds:&rect];
	if (rect.origin.y + floor(rect.size.height / 2.0) < minEdge) {
	    minEdge = rect.origin.y + floor(rect.size.height / 2.0);
	}
    }
    [self graphicsPerform:@selector(moveVerticalCenterTo:) with:&minEdge andDraw:YES];
    [window flushWindow];

    return self;
}
  
- changeAspectRatio:sender
{
    [self graphicsPerform:@selector(sizeToNaturalAspectRatio) andDraw:YES];
    [window flushWindow];
    return self;
}

- alignToGrid:sender
{
    [self graphicsPerform:@selector(alignToGrid:) with:self andDraw:YES];
    [window flushWindow];
    return self;
}

- sizeToGrid:sender
{
    [self graphicsPerform:@selector(sizeToGrid:) with:self andDraw:YES];
    [window flushWindow];
    return self;
}

- enableGrid:sender
/*
 * If the tag of the sender is non-zero, then gridding is enabled.
 * If the tag is zero, then gridding is disabled.
 */
{
    [self setGridEnabled:[sender selectedTag] ? YES : NO];
    return self;
}

- hideGrid:sender
/*
 * If the tag of the sender is non-zero, then the grid is made visible
 * otherwise, it is hidden (but still conceivable in effect).
 */
{
    [self setGridVisible:[sender selectedTag] ? YES : NO];
    return self;
}

- print:sender
{
    return [self printPSCode:sender];
}

/*
 * Target/Action methods to change Graphic parameters from a Control.
 * If the sender is a Matrix, then the selectedRow is used to determine
 * the value to use (for linecap, linearrow, etc.) otherwise, the
 * sender's floatValue or intValue is used (whichever is appropriate).
 * This allows interface builders the flexibility to design different
 * ways of setting those values.
 */

- takeGridValueFrom:sender
{
    [self setGridSpacing:[sender intValue]];
    return self;
}

- takeGridGrayFrom:sender
{
    [self setGridGray:[sender floatValue]];
    return self;
}

- takeGrayValueFrom:sender
{
    float value;

    value = [sender floatValue];
    [self graphicsPerform:@selector(setGray:) with:&value andDraw:YES];
    [window flushWindow];

    return self;
}

- takeLineWidthFrom:sender
{
    float value;

    value = [sender floatValue];
    [self graphicsPerform:@selector(setLineWidth:) with:&value andDraw:YES];
    [window flushWindow];

    return self;
}

- takeLineJoinFrom:sender
{
    if ([sender respondsTo:@selector(selectedRow)]) {
	[self graphicsPerform:@selector(setLineJoin:) with:(void *)[sender selectedRow] andDraw:YES];
    } else {
	[self graphicsPerform:@selector(setLineJoin:) with:(void *)[sender intValue] andDraw:YES];
    }
    [window flushWindow];
    return self;
}

- takeLineCapFrom:sender
{
    if ([sender respondsTo:@selector(selectedRow)]) {
	[self graphicsPerform:@selector(setLineCap:) with:(void *)[sender selectedRow] andDraw:YES];
    } else {
	[self graphicsPerform:@selector(setLineCap:) with:(void *)[sender intValue] andDraw:YES];
    }
    [window flushWindow];
    return self;
}

- takeLineArrowFrom:sender
{
    if ([sender respondsTo:@selector(selectedRow)]) {
	[self graphicsPerform:@selector(setLineArrow:) with:(void *)[sender selectedRow] andDraw:YES];
    } else {
	[self graphicsPerform:@selector(setLineArrow:) with:(void *)[sender intValue] andDraw:YES];
    }
    [window flushWindow];
    return self;
}

- takeFillValueFrom:sender
{
    if ([sender respondsTo:@selector(selectedRow)]) {
	[self graphicsPerform:@selector(setFill:) with:(void *)[sender selectedRow] andDraw:YES];
    } else {
	[self graphicsPerform:@selector(setFill:) with:(void *)[sender intValue] andDraw:YES];
    }
    [window flushWindow];
    return self;
}

- takeFrameValueFrom:sender
{
    if ([sender respondsTo:@selector(selectedRow)]) {
	[self graphicsPerform:@selector(setFramed:) with:(void *)[sender selectedRow] andDraw:YES];
    } else {
	[self graphicsPerform:@selector(setFramed:) with:(void *)[sender intValue] andDraw:YES];
    }
    [window flushWindow];
    return self;
}

- takeLineColorFrom:sender
{
    NXColor color = [sender color];
    [self graphicsPerform:@selector(setLineColor:) with:(void *)&color andDraw:YES];
    [window flushWindow];
    return self;
}

- takeFillColorFrom:sender
{
    NXColor color = [sender color];
    [self graphicsPerform:@selector(setFillColor:) with:(void *)&color andDraw:YES];
    [window flushWindow];
    return self;
}

- takeTextColorFrom:sender
{
    NXColor color = [sender color];
    [self graphicsPerform:@selector(setGraphicTextColor:) with:(void *)&color andDraw:YES];
    [window flushWindow];
    return self;
}

- changeFont:sender
{
    if ([window firstResponder] == self) {
	[self graphicsPerform:@selector(changeFont:) with:sender andDraw:YES];
	[window flushWindow];
    }
    return self;
}

/* Accepting becoming the First Responder */

- (BOOL)acceptsFirstResponder
/*
 * GraphicView always wants to become the first responder when it is
 * clicked on in a window, so it returns YES from this method.
 */
{
    return YES;
}

/* Printing */

- beginPrologueBBox:(NXRect *)boundingBox creationDate:(char *)dateCreated
    createdBy:(char *)anApplication fonts:(char *)fontNames
    forWhom:(char *)user pages:(int )numPages title:(char *)aTitle
/*
 * Include the window title as the name of the document when printing.
 */
{
    char *s;
    char name[MAXPATHLEN+1];
    const char *title = [window title];

    strcpy(name, title);
    s = strchr(name, ' ');
    if (s) *s = '\0';
    return [super beginPrologueBBox:boundingBox creationDate:dateCreated
		createdBy:anApplication fonts:fontNames
		forWhom:user pages:numPages title:name];
}

- beginSetup
/*
 * Spit out the custom PostScript defs.
 */
{
    [super beginSetup];
    PSInit();
    return self;
}

/* Archiver-related methods. */

- awake
/*
 * After the GraphicView is unarchived, its cache must be created.
 * If we are loading in this GraphicView just to print it, then we need
 * not load up our cache.
 */
{
    if (!InMsgPrint) {
	cacheWindow = createCache(&bounds.size, [self zone]);
	[self cache:&bounds];
    }
    initClassVars();
    return [super awake];
}

- write:(NXTypedStream *)stream
/*
 * Writes out the glist and the flags.
 * No need to write out the slist since it can be regenerated from the glist.
 */
{
    [super write:stream];
    gvFlags.dirty = NO;
    [window setDocEdited:NO];
    NXWriteTypes(stream, "@sf", &glist, &gvFlags, &gridGray);
    NXWriteObject(stream, editView);
    return self;
}

- read:(NXTypedStream *)stream
/*
 * Reads in the glist and the flags, and regenerates the slist from the glist.
 */
{
    int i;
    id g, newg;

    [super read:stream];
    NXReadTypes(stream, "@sf", &glist, &gvFlags, &gridGray);
    for (i = [glist count]-1; i >= 0; i--) {
	g = [glist objectAt:i];
	newg = [g replaceWithImage];
	if (g != newg) {
	    if (g) {
		[glist replaceObjectAt:i with:newg];
	    } else {
		[glist removeObjectAt:i];
	    }
	}
    }
    [self getSelection];
    [self resetGUP];
    if (NXTypedStreamClassVersion(stream, [self name]) < 1) {
	editView = createEditView(self);
    } else {
	editView = NXReadObject(stream);
    }

    return self;
}

/* Methods to deal with being/becoming the First Responder */

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

- (BOOL)validateCommand:menuCell
/*
 * Can be called to see if the specified action is valid on this view now.
 * It returns NO if the GraphicView knows that action is not valid now,
 * otherwise it returns YES.  Note the use of the Pasteboard change
 * count so that the GraphicView does not have to look into the Pasteboard
 * every time paste: is validated.
 */
{
    id pb;
    int i, count, gcount;
    SEL action = [menuCell action];
    static BOOL pboardHasPasteableType = NO;
    static int cachedPasteboardChangeCount = 0;

    if (action == @selector(bringToFront:)) {
	if ((count = [slist count]) && [glist count] > count) {
	    for (i = 0; i < count; i++) {
		if ([slist objectAt:i] != [glist objectAt:i]) {
		    return YES;
		}
	    }
	}
	return NO;
    } else if (action == @selector(sendToBack:)) {
	if ((count = [slist count]) && (gcount = [glist count]) > count) {
	    for (i = 1; i <= count; i++) {
		if ([slist objectAt:count-i] != [glist objectAt:gcount-i]) {
		    return YES;
		}
	    }
	}
	return NO;
    } else if (action == @selector(group:) ||
	action == @selector(alignLeftEdges:) ||
	action == @selector(alignRightEdges:) ||
	action == @selector(alignBottomEdges:) ||
	action == @selector(alignTopEdges:) ||
	action == @selector(alignVerticalCenters:) ||
	action == @selector(alignHorizontalCenters:)) {
	return([slist count] > 1);
    } else if (action == @selector(ungroup:)) {
	return(gvFlags.groupInSlist && [slist count] > 0);
    } else if (action == @selector(deselectAll:) ||
	action == @selector(lock:) ||
	action == @selector(changeAspectRatio:) ||
	action == @selector(cut:) ||
	action == @selector(copy:)) {
	return([slist count] > 0);
    } else if (action == @selector(alignToGrid:) ||
	action == @selector(sizeToGrid:)) {
	return(GRID > 1 && [slist count] > 0);
    } else if (action == @selector(unlock:)) {
	return gvFlags.locked;
    } else if (action == @selector(selectAll:)) {
	return([glist count] > [slist count]);
    } else if (action == @selector(paste:)) {
	pb = [Pasteboard new];
	count = [pb changeCount];
	if (count != cachedPasteboardChangeCount) {
	    cachedPasteboardChangeCount = count;
	    pboardHasPasteableType = (drawPasteType([pb types]) != NULL);
	}
	return pboardHasPasteableType;
    } else if (action == @selector(hideGrid:)) {
	if (GRID >= 4) {
	    if ([self gridIsVisible]) {
		if (strcmp([menuCell title], "Hide Grid")) {
		    [menuCell setTitle:"Hide Grid"];
		    [menuCell setTag:0];
		    [menuCell setEnabled:NO];
		}
	    } else {
		if (strcmp([menuCell title], "Show Grid")) {
		    [menuCell setTitle:"Show Grid"];
		    [menuCell setTag:1];
		    [menuCell setEnabled:NO];
		}
	    }
	    return YES;
	} else {
	    return NO;
	}
    } else if (action == @selector(enableGrid:)) {
	if (gvFlags.grid > 1) {
	    if ([self gridIsEnabled]) {
		if (strcmp([menuCell title], "Turn Grid Off")) {
		    [menuCell setTitle:"Turn Grid Off"];
		    [menuCell setTag:0];
		    [menuCell setEnabled:NO];
		}
	    } else {
		if (strcmp([menuCell title], "Turn Grid On")) {
		    [menuCell setTitle:"Turn Grid On"];
		    [menuCell setTag:1];
		    [menuCell setEnabled:NO];
		}
	    }
	    return YES;
	} else {
	    return NO;
	}
    } else if (action == @selector(print:)) {
	return([glist count] > 0);
    }

    return YES;
}

/* Useful scrolling routines. */

- scrollGraphicToVisible:graphic
{
    NXPoint p;
    NXRect eb;

    p = bounds.origin;
    NXContainRect([graphic getExtendedBounds:&eb], &bounds);
    p.x -= bounds.origin.x;
    p.y -= bounds.origin.y;
    if (p.x || p.y) {
	[graphic moveBy:&p];
	bounds.origin.x += p.x;
	bounds.origin.y += p.y;
	[self scrollRectToVisible:[graphic getExtendedBounds:&eb]];
    }

    return self;
}

- scrollPointToVisible:(const NXPoint *)point
{
    NXRect r;

    r.origin.x = point->x - 5.0;
    r.origin.y = point->y - 5.0;
    r.size.width = r.size.height = 10.0;

    return [self scrollRectToVisible:&r];
}

@end

