This is GraphicView.m in view mode; [Download] [Up]
#import "draw.h" extern NXAtom NXDataLinkPboardType; /* This file is best read in a window as wide as this comment (so that this comment fits on one line). */ @interface GraphicView(PrivateMethods) /* Private methods */ - selectionCache; - recacheSelection:(BOOL)updateLinks; - compositeSelection:(const NXRect *)sbounds from:(int)gstate; - (int)cacheSelection; - dirty:sender; - resetGUP; - moveGraphicsBy:(NXPoint *)vector andDraw:(BOOL)drawFlag; - dragSelect:(NXEvent *)event; - alignGraphicsBy:(AlignmentType)alignType edge:(NXCoord *)edge; - alignBy:(AlignmentType)alignType; @end const char *DrawPboardType = "Draw Graphic List Type version 3.0"; 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. In the original Draw code it was almost always kept * in the same order as the glist, but could be jumbled by doing a shift-drag * select to add to an existing selection. We are now extremely careful about * keeping the slist in the same order as the glist. * * cacheWindow is the off-screen window into which the objects are * drawn. Flags: 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 Graphic *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 Window *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 : (gvFlags.grid ? (NXCoord)gvFlags.grid : 1.0)) #define grid(point) \ (point).x = floor(((point).x / GRID) + 0.5) * GRID; \ (point).y = floor(((point).y / GRID) + 0.5) * GRID; 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(List *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 View *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). */ { View *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; } /* 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. */ { [GraphicView setVersion:1]; DrawPboardType = NXUniqueStringNoCopy(DrawPboardType); return self; } /* Alignment methods */ + (SEL)actionFromAlignType:(AlignmentType)alignType { switch (alignType) { case LEFT: return @selector(moveLeftEdgeTo:); case RIGHT: return @selector(moveRightEdgeTo:); case BOTTOM: return @selector(moveBottomEdgeTo:); case TOP: return @selector(moveTopEdgeTo:); case HORIZONTAL_CENTERS: return @selector(moveVerticalCenterTo:); case VERTICAL_CENTERS: return @selector(moveHorizontalCenterTo:); case BASELINES: return @selector(moveBaselineTo:); } return (SEL)NULL; } /* Creation methods. */ static void initClassVars() /* * Sets up any default values. */ { 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; [NXApp registerServicesMenuSendTypes:validSendTypes andReturnTypes:[NXImage imagePasteboardTypes]]; validReturnTypes[0] = DrawPboardType; validReturnTypes[1] = 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 = 10; gvFlags.gridDisabled = 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(); [self registerForDragging]; drawInstance = NXGetNamedObject("DrawInstance", NXApp); 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]; } /* Used by Change's */ - setGroupInSlist:(BOOL)setting { gvFlags.groupInSlist = setting; return self; } - resetGroupInSlist { gvFlags.groupInSlist = checkForGroup(slist); return self; } - resetLockedFlag { int i, count; gvFlags.locked = NO; count = [glist count]; for (i = 0; (i < count) && (!gvFlags.locked); i++) if ([[glist objectAt:i] isLocked]) gvFlags.locked = YES; return self; } - redrawGraphics:graphicsList afterChangeAgent:changeAgent performs:(SEL)aSelector { NXRect afterBounds, beforeBounds; if ([graphicsList count]) { [self getBBox:&beforeBounds of:graphicsList]; [changeAgent perform:aSelector]; [self getBBox:&afterBounds of:graphicsList]; NXUnionRect(&beforeBounds, &afterBounds); [self cache:&afterBounds]; // recache after change object did something } else { [changeAgent perform:aSelector]; } return self; } /* Public interface methods. */ - (BOOL)isEmpty { return [glist count] == 0; } - (BOOL)hasEmptySelection { return [slist count] == 0; } - dirty { if ([[window delegate] respondsTo:@selector(dirty:)]) [[window delegate] dirty:self]; return self; } - getSelection /* * Resets slist by going through the glist and locating all the Graphics * which respond YES to the isSelected method. */ { int i; Graphic *graphic; [slist free]; slist = [[List allocFromZone:[self zone]] init]; gvFlags.groupInSlist = NO; i = [glist count]; while (i--) { graphic = [glist objectAt:i]; if ([graphic isSelected]) { [slist insertObject:graphic at:0]; gvFlags.groupInSlist = gvFlags.groupInSlist || [graphic isKindOf:[Group class]]; } } return self; } - getBBox:(NXRect *)bbox of:(List *)list { return [self getBBox:bbox of:list extended:YES]; } - getBBox:(NXRect *)bbox of:(List *)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; } - graphicsPerform:(SEL)aSelector /* * 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:. */ { int i, count; Graphic *graphic; NXRect eb, affectedBounds; count = [slist count]; if (count) { [[slist objectAt:0] getExtendedBounds:&affectedBounds]; for (i = 1; i < count; i++) { graphic = [slist objectAt:i]; NXUnionRect([graphic getExtendedBounds:&eb], &affectedBounds); } for (i = 0; i < count; i++) { graphic = [slist objectAt:i]; [graphic perform:aSelector]; NXUnionRect([graphic getExtendedBounds:&eb], &affectedBounds); } [self cache:&affectedBounds]; // graphicsPerform: } return self; } - graphicsPerform:(SEL)aSelector with:(void *)argument { int i, count; Graphic *graphic; NXRect eb, affectedBounds; count = [slist count]; if (count) { [[slist objectAt:0] getExtendedBounds:&affectedBounds]; for (i = 1; i < count; i++) { graphic = [slist objectAt:i]; NXUnionRect([graphic getExtendedBounds:&eb], &affectedBounds); } for (i = 0; i < count; i++) { graphic = [slist objectAt:i]; [graphic perform:aSelector with:argument]; NXUnionRect([graphic getExtendedBounds:&eb], &affectedBounds); } [self cache:&affectedBounds]; // graphicsPerform:with: } return self; } - cache:(const NXRect *)rect andUpdateLinks:(BOOL)updateLinks /* * Draws all the Graphics intersected by rect into the off-screen cache, * then composites the rect back to the screen (but does NOT flushWindow). * If updateLinks is on, then we check to see if the redrawn area intersects * an area that someone has created a link to (see gvLinks.m). */ { gvFlags.cacheing = YES; [self drawSelf:rect :1]; gvFlags.cacheing = NO; if ([self canDraw]) { [self lockFocus]; [self drawSelf:rect :1]; [self unlockFocus]; } if (updateLinks && !gvFlags.suspendLinkUpdate) [self updateTrackedLinks:rect]; return self; } - cache:(const NXRect *)rect { return [self cache:rect andUpdateLinks:YES]; } - cacheAndFlush:(const NXRect *)rect { [self cache:rect]; // cacheAndFlush: [window flushWindow]; return self; } - insertGraphic:(Graphic *)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) { if ([graphic isSelected]) [slist insertObject:graphic at:0]; [glist insertObject:graphic at:0]; [graphic wasAddedTo:self]; [self cache:[graphic getExtendedBounds:&eb]]; // insertGraphic: if ([graphic isKindOf:[Group class]]) gvFlags.groupInSlist = YES; [window flushWindow]; } return self; } - removeGraphic:(Graphic *)graphic /* * Removes the graphic from the GraphicView and redraws. */ { int i; NXRect eb; Graphic *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]; [graphic wasRemovedFrom:self]; [slist removeObject:g]; if ([g isKindOf:[Group class]]) gvFlags.groupInSlist = checkForGroup(slist); [self cache:&eb]; // removeGraphic: [window flushWindow]; } return self; } - (Graphic *)selectedGraphic /* * If there is one and only one Graphic selected, this method returns it. */ { if ([slist count] == 1) { Graphic *graphic = [slist objectAt:0]; return [graphic isKindOf:[Group class]] ? nil : graphic; } else { return nil; } } - (List *)selectedGraphics /* * Result valid only immediately after call. GraphicView * reserves the right to free the list without warning. */ { return slist; } - (List *)graphics /* * Result valid only immediately after call. GraphicView * reserves the right to free the list without warning. */ { return glist; } /* Methods to modify the grid of the GraphicView. */ - (int)gridSpacing { return gvFlags.grid; } - (BOOL)gridIsVisible { return gvFlags.showGrid; } - (BOOL)gridIsEnabled { return !gvFlags.gridDisabled; } - (float)gridGray { return gridGray; } - setGridSpacing:(int)gridSpacing { id change; if (gridSpacing != gvFlags.grid && gridSpacing > 0 && gridSpacing < 256) { change = [[GridChange alloc] initGraphicView:self]; [change startChange]; gvFlags.grid = gridSpacing; if (gvFlags.showGrid) { [self resetGUP]; [self cache:&bounds andUpdateLinks:NO]; [window flushWindow]; } [change endChange]; } return self; } - setGridEnabled:(BOOL)flag { id change; change = [[GridChange alloc] initGraphicView:self]; [change startChange]; gvFlags.gridDisabled = flag ? NO : YES; [change endChange]; return self; } - setGridVisible:(BOOL)flag { id change; if (gvFlags.showGrid != flag) { change = [[GridChange alloc] initGraphicView:self]; [change startChange]; gvFlags.showGrid = flag; if (flag) [self resetGUP]; [self cache:&bounds andUpdateLinks:NO]; [window flushWindow]; [change endChange]; } return self; } - setGridGray:(float)gray { id change; if (gray != gridGray) { change = [[GridChange alloc] initGraphicView:self]; [change startChange]; gridGray = gray; if (gvFlags.showGrid) { [self cache:&bounds andUpdateLinks:NO]; [window flushWindow]; } [change endChange]; } return self; } - setGridSpacing:(int)gridSpacing andGray:(float)gray { id change; if (gray != gridGray || (gridSpacing != gvFlags.grid && gridSpacing > 0 && gridSpacing < 256)) { change = [[GridChange alloc] initGraphicView:self]; [change startChange]; gridGray = gray; if (gvFlags.grid != gridSpacing && gridSpacing > 0 && gridSpacing < 256) { gvFlags.grid = gridSpacing; if (gvFlags.showGrid) [self resetGUP]; } if (gvFlags.showGrid) { [self cache:&bounds andUpdateLinks:NO]; [window flushWindow]; } [change endChange]; } return self; } - grid:(NXPoint *)p { grid(*p); return self; } /* Public methods for importing foreign data types into the GraphicView */ #define LOAD_IMAGE NXLocalString("Import Data", NULL, "Title of the alert which lets the user know that the image he has incorporated into his document does not fit.") #define IMAGE_TOO_LARGE NXLocalString("The imported data is too large to fit on the page. Resize it to fit?", NULL, "Alert message which asks user if he wants to scale an image or some text that he is incorporating into his document to fit within the size of the document.") #define SCALE NXLocalString("Resize", NULL, "Button choice which allows the user to scale an image he is incorporating into his document so that it will fit within the size of the document.") #define DONT_SCALE NXLocalString("Don't Resize", NULL, "Button choice which allows the user to leave an image he is incorporating into his document the same size even though it will not fit within the size of his document.") - placeGraphic:(Graphic *)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, visibleRect; id change; if (graphic) { [graphic getExtendedBounds:&gbounds]; if (gbounds.size.width > bounds.size.width || gbounds.size.height > bounds.size.height) { scale = NXRunAlertPanel(LOAD_IMAGE, IMAGE_TOO_LARGE, SCALE, DONT_SCALE, CANCEL); if (scale < 0) { [graphic free]; return nil; } 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]; } else { [self getVisibleRect:&visibleRect]; visibleRect.origin.x += floor(visibleRect.size.width / 2.0 + 0.5); visibleRect.origin.y += floor(visibleRect.size.height / 2.0 + 0.5); [graphic centerAt:&visibleRect.origin]; } [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]; change = [[CreateGraphicsChange alloc] initGraphicView:self graphic:graphic]; [change startChangeIn:self]; [self deselectAll:self]; [graphic select]; [self insertGraphic:graphic]; [self scrollGraphicToVisible:graphic]; [change endChange]; } return graphic; } /* 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 andUpdateLinks:NO]; } 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 change, factory; Graphic *g = nil, *startg = nil; BOOL shift, control, gotHit = NO, deepHit = NO, didDrag = 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 && event->data.mouse.click < 2) [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]; i = 0; // See if a Graphic wants to handle this event itself [self lockFocus]; do { g = [glist objectAt:i++]; if ([g handleEvent:event at:&p inView:self]) return self; } while (g != nil); [self unlockFocus]; factory = [self currentGraphic]; if (!control && (factory || (event->data.mouse.click == 2))) { id editFactory = factory; if ((event->data.mouse.click == 2) && ![editFactory isEditable]) editFactory = [TextGraphic class]; if ([editFactory isEditable]) { /* if editable, try to edit one */ i = 0; g = [glist objectAt:i++]; while (g != nil) { if ([g isKindOf:editFactory] && [g hit:&p]) { if ([g isSelected]) { [g deselect]; [self cache:[g getExtendedBounds:&eb] andUpdateLinks:NO]; [slist removeObject:g]; } [g edit:event in:editView]; goto done; } g = [glist objectAt:i++]; } } } if (!control && factory) { if (factory && !g) { /* not editing or no editable graphic found */ g = [[factory allocFromZone:[self zone]] init]; if ([drawInstance respondsTo:@selector(inspectorPanel)]) { [[[drawInstance inspectorPanel] delegate] initializeGraphic:g]; } if ([g create:event in:self]) { change = [[CreateGraphicsChange alloc] initGraphicView:self graphic:g]; [change startChange]; if (!shift) [self deselectAll:self]; [self insertGraphic:g]; [g edit:NULL in:editView]; [change endChange]; } 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; change = [[ResizeGraphicsChange alloc] initGraphicView:self graphic:g]; [change startChange]; [g resize:event by:corner in:self]; [change endChange]; } 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] andUpdateLinks:NO]; [slist removeObject:g]; if ([g isKindOf:[Group class]]) gvFlags.groupInSlist = checkForGroup(slist); } else { gotHit = [self move:event]; if (!gotHit) { deepHit = ![g isOpaque]; if (!deepHit) gotHit = 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]; if (shift) [self getSelection]; if (deepHit || ![self move:event]) { [self cache:[g getExtendedBounds:&eb] andUpdateLinks:NO]; } } else { g = [glist objectAt:i++]; } } while (!gotHit && g != startg); if (!gotHit && !deepHit) { if (!shift) { [self lockFocus]; [self deselectAll:self]; [self unlockFocus]; didDrag = YES; } [self dragSelect:event]; } } done: if (!didDrag && dragRect) { NX_FREE(dragRect); dragRect = NULL; } gvFlags.selectAll = NO; [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. The invalidRect * stuff is to clean up any temporary drawing we have in the view * (currently used only to show a source selection in the links mechanism-- * see showSelection: in gvLinks.m). */ { NXRect *rp; NXRect r, visibleRect; int i, j, gstate; if (rects == NULL) return self; if (!gvFlags.cacheing && invalidRect && (rects != invalidRect)) { NXUnionRect(rects, invalidRect); [self drawSelf:invalidRect :1]; [window flushWindow]; NX_FREE(invalidRect); invalidRect = 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 showFastKnobFills]; 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_COPY); } } } 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 moveGraphicsBy:&p andDraw:YES]; [[self window] flushWindow]; NXPing(); } 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; } - (BOOL)acceptsFirstMouse /* * GraphicView really only wants to accept first mouse if that * first mouse starts a drag, but there is no way to detect that * case, so we're stuck accepting all first mouse events! */ { 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; } /* * 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. */ - (Graphic *)currentGraphic { if ([drawInstance respondsTo:@selector(currentGraphic)]) { return [drawInstance currentGraphic]; } else { return currentGraphic; } } - setCurrentGraphic:sender { currentGraphic = objc_getClass([[sender selectedCell] title]); return self; } /* These methods write out the form information. */ - (BOOL)hasFormEntries { int i; for (i = [glist count]-1; i >= 0; i--) { if ([[glist objectAt:i] isFormEntry]) return YES; } return NO; } - writeFormEntriesToFile:(const char *)file { int i; NXStream *stream; stream = NXOpenMemory(NULL, 0, NX_WRITEONLY); NXPrintf(stream, "Page Size: w = %d, h = %d\n", (int)bounds.size.width, (int)bounds.size.height); for (i = [glist count]-1; i >= 0; i--) { [[glist objectAt:i] writeFormEntryToStream:stream]; } NXSaveToFile(stream, file); NXCloseMemory(stream, NX_FREEBUFFER); return self; } /* * Target/Action methods. */ - delete:sender { int i; Graphic *graphic; id change; i = [slist count]; if (i > 0) { change = [[DeleteGraphicsChange alloc] initGraphicView:self]; [change startChange]; [self graphicsPerform:@selector(deactivate)]; [slist makeObjectsPerform:@selector(activate)]; while (i--) { graphic = [slist objectAt:i]; [glist removeObject:graphic]; [graphic wasRemovedFrom:self]; } if (originalPaste == [slist objectAt:0]) [slist removeObjectAt:0]; [slist free]; slist = [[List allocFromZone:[self zone]] init]; gvFlags.groupInSlist = NO; [window flushWindow]; [change endChange]; return self; } else { return nil; } } - selectAll:sender /* * Selects all the items in the glist. */ { int i; Graphic *g; NXRect visibleRect; i = [glist count]; if (!i) return self; [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 showFastKnobFills]; [[cacheWindow contentView] unlockFocus]; [self getVisibleRect:&visibleRect]; if ([self canDraw]) { [self lockFocus]; [self drawSelf:&visibleRect :1]; [self unlockFocus]; } if (sender != self) [window flushWindow]; gvFlags.selectAll = YES; return self; } - deselectAll:sender /* * Deselects all the items in the slist. */ { NXRect sbounds; if ([slist count] > 0) { [self getBBox:&sbounds of:slist]; [slist makeObjectsPerform:@selector(deselect)]; [self cache:&sbounds andUpdateLinks:NO]; [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:. */ { id change; if ([slist count] > 0) { change = [[LockGraphicsChange alloc] initGraphicView:self]; [change startChange]; gvFlags.locked = YES; [slist makeObjectsPerform:@selector(lock)]; [self deselectAll:sender]; [change endChange]; } return self; } - unlock:sender { id change; change = [[UnlockGraphicsChange alloc] initGraphicView:self]; [change startChange]; [glist makeObjectsPerform:@selector(unlock)]; gvFlags.locked = NO; [change endChange]; 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. */ { id change; int i; i = [slist count]; if (i) { change = [[BringToFrontGraphicsChange alloc] initGraphicView:self]; [change startChange]; while (i--) [glist insertObject:[glist removeObject:[slist objectAt:i]] at:0]; [self recacheSelection]; [change endChange]; } return self; } - sendToBack:sender { int i, count; id change; count = [slist count]; if (count > 0) { change = [[SendToBackGraphicsChange alloc] initGraphicView:self]; [change startChange]; for (i = 0; i < count; i++) [glist addObject:[glist removeObject:[slist objectAt:i]]]; [self recacheSelection]; [change endChange]; } return self; } - group:sender /* * Creates a new Group object with the current slist as its member list. * See the Group class for more info. */ { int i; NXRect eb; Graphic *graphic; id change; i = [slist count]; if (i > 1) { change = [[GroupGraphicsChange alloc] initGraphicView:self]; [change startChange]; while (i--) [glist removeObject:[slist objectAt:i]]; graphic = [[Group allocFromZone:[self zone]] initList:slist]; [change noteGroup:graphic]; [glist insertObject:graphic at:0]; slist = [[List allocFromZone:[self zone]] init]; [slist addObject:graphic]; gvFlags.groupInSlist = YES; [self cache:[graphic getExtendedBounds:&eb]]; if (sender != self) [window flushWindow]; [change endChange]; } 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). */ { int i, k; NXRect sbounds; id graphic; id change; if (!gvFlags.groupInSlist || [slist count] == 0) return nil; change = [[UngroupGraphicsChange alloc] initGraphicView:self]; [change startChange]; [self getBBox:&sbounds of:slist]; i = [slist count]; while (i--) { graphic = [slist objectAt:i]; if ([graphic isKindOf:[Group class]]) { k = [glist indexOf:graphic]; [glist removeObjectAt:k]; [graphic transferSubGraphicsTo:glist at:k]; } } [self cache:&sbounds]; if (sender != self) [window flushWindow]; [self getSelection]; [change endChange]; return self; } - align:sender { [self alignBy:(AlignmentType)[[sender selectedCell] tag]]; return self; } - changeAspectRatio:sender { id change; change = [[AspectRatioGraphicsChange alloc] initGraphicView:self]; [change startChange]; [self graphicsPerform:@selector(sizeToNaturalAspectRatio)]; [change endChange]; [window flushWindow]; return self; } - alignToGrid:sender { id change; change = [[AlignGraphicsChange alloc] initGraphicView:self]; [change startChange]; [self graphicsPerform:@selector(alignToGrid:) with:self]; [window flushWindow]; [change endChange]; return self; } - sizeToGrid:sender { id change; change = [[DimensionsGraphicsChange alloc] initGraphicView:self]; [change startChange]; [self graphicsPerform:@selector(sizeToGrid:) with:self]; [window flushWindow]; [change endChange]; 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; } - showLinks:sender /* * If the tag of the sender is non-zero, then linked items are * shown with a border around them (see redrawLinkOutlines: in gvLinks.m). */ { [linkManager setLinkOutlinesVisible:[sender selectedTag] ? YES : NO]; return self; } - spellCheck:sender { int i; NXCoord curY, newY, maxY = 0.0; NXCoord curX, newX, maxX = 0.0; NXRect egbounds, gbounds; id fr = [window firstResponder]; Graphic *graphic, *editingGraphic, *newEditingGraphic = nil; if (![fr isKindOf:[Text class]] || ![[NXSpellChecker sharedInstance] checkSpelling:NX_CheckSpellingToEnd of:fr]) { if ([fr isKindOf:[Text class]]) { editingGraphic = [fr delegate]; [editingGraphic getBounds:&egbounds]; curY = egbounds.origin.y + egbounds.size.height; curX = egbounds.origin.x; } else { curX = 0.0; curY = 10000.0; } maxY = 0.0; maxX = 10000.0; for (i = [glist count]-1; i >= 0; i--) { graphic = [glist objectAt:i]; if ([graphic isKindOf:[TextGraphic class]]) { [graphic getBounds:&gbounds]; newY = gbounds.origin.y + gbounds.size.height; newX = gbounds.origin.x; if ((newY > maxY || (newY == maxY && newX < maxX)) && (newY < curY || (newY == curY && newX > curX))) { maxY = newY; maxX = newX; newEditingGraphic = graphic; } } } [window makeFirstResponder:self]; if (newEditingGraphic) { [newEditingGraphic edit:NULL in:editView]; return [self spellCheck:sender]; } } return self; } - showGuessPanel:sender { return [[[NXSpellChecker sharedInstance] spellingPanel] makeKeyAndOrderFront:sender]; } /* Cover-Sheet items (see TextGraphic.m). */ - doAddCoverSheetEntry:sender localizable:(BOOL)flag { const char *entry = [[sender selectedCell] altTitle]; if (!entry || !*entry) entry = [[sender selectedCell] title]; [self placeGraphic:[[TextGraphic allocFromZone:[self zone]] initFormEntry:entry localizable:flag] at:NULL]; return self; } - addLocalizableCoverSheetEntry:sender { return [self doAddCoverSheetEntry:sender localizable:YES]; } - addCoverSheetEntry:sender { return [self doAddCoverSheetEntry:sender localizable:NO]; } /* The venerable print: method. */ - 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]; [window flushWindow]; return self; } - takeLineWidthFrom:sender { id change; float width = [sender floatValue]; change = [[LineWidthGraphicsChange alloc] initGraphicView:self lineWidth:width]; [change startChange]; [self graphicsPerform:@selector(setLineWidth:) with:&width]; [window flushWindow]; [change endChange]; return self; } - takeLineJoinFrom:sender { int joinValue; id change; if ([sender respondsTo:@selector(selectedRow)]) joinValue = [sender selectedRow]; else joinValue = [sender intValue]; change = [[LineJoinGraphicsChange alloc] initGraphicView:self lineJoin:joinValue]; [change startChange]; [self graphicsPerform:@selector(setLineJoin:) with:(void *)joinValue]; [window flushWindow]; [change endChange]; return self; } - takeLineCapFrom:sender { int capValue; id change; if ([sender respondsTo:@selector(selectedRow)]) capValue = [sender selectedRow]; else capValue = [sender intValue]; change = [[LineCapGraphicsChange alloc] initGraphicView:self lineCap:capValue]; [change startChange]; [self graphicsPerform:@selector(setLineCap:) with:(void *)capValue]; [window flushWindow]; [change endChange]; return self; } - takeLineArrowFrom:sender { int arrowValue; id change; if ([sender respondsTo:@selector(selectedRow)]) arrowValue = [sender selectedRow]; else arrowValue = [sender intValue]; change = [[ArrowGraphicsChange alloc] initGraphicView:self lineArrow:arrowValue]; [change startChange]; [self graphicsPerform:@selector(setLineArrow:) with:(void *)arrowValue]; [window flushWindow]; [change endChange]; return self; } - takeFillValueFrom:sender { int fillValue; id change; if ([sender respondsTo:@selector(selectedRow)]) fillValue = [sender selectedRow]; else fillValue = [sender intValue]; change = [[FillGraphicsChange alloc] initGraphicView:self fill:fillValue]; [change startChange]; [self graphicsPerform:@selector(setFill:) with:(void *)fillValue]; [window flushWindow]; [change endChange]; return self; } - takeFrameValueFrom:sender { if ([sender respondsTo:@selector(selectedRow)]) { [self graphicsPerform:@selector(setFramed:) with:(void *)[sender selectedRow]]; } else { [self graphicsPerform:@selector(setFramed:) with:(void *)[sender intValue]]; } [window flushWindow]; return self; } - takeLineColorFrom:sender { id change; NXColor color = [sender color]; change = [[LineColorGraphicsChange alloc] initGraphicView:self color:&color]; [change startChange]; [self graphicsPerform:@selector(setLineColor:) with:&color]; [window flushWindow]; [change endChange]; return self; } - takeFillColorFrom:sender { id change; NXColor color = [sender color]; change = [[FillGraphicsChange alloc] initGraphicView:self]; [change startChange]; [self graphicsPerform:@selector(setFillColor:) with:&color]; [window flushWindow]; [change endChange]; return self; } - takeFormEntryStatusFrom:sender { [self graphicsPerform:@selector(setFormEntry:) with:(void *)[sender intValue]]; [window flushWindow]; return self; } - changeFont:sender { id change; if ([window firstResponder] == self) { change = [[MultipleChange alloc] initChangeName:FONT_OPERATION]; [change startChange]; [self graphicsPerform:@selector(changeFont:) with:sender]; [window flushWindow]; [change endChange]; } 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. */ { PSInit(); // The following taken out so we can un-archive functions first. // if (!InMsgPrint) { // cacheWindow = createCache(&bounds.size, [self zone]); // [self cache:&bounds andUpdateLinks:NO]; // } initClassVars(); [self registerForDragging]; drawInstance = NXGetNamedObject("DrawInstance", NXApp); return [super awake]; } - createCacheWindow { /* This method added so it can be invoked from HGraphicView * after displays have been bound. */ cacheWindow = createCache(&bounds.size, [self zone]); return self; } - 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. * We also ensure that no Text object that might be a subview of the * editView gets written out by removing all subviews of the editView. */ { [super write:stream]; if ( [[self class] version] < FIP_ARCHIVE_FIX ) { NXWriteTypes(stream, "@sf", &glist, &gvFlags, &gridGray); } else { NXWriteTypes(stream, "@If", &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; List *evsvs; Graphic *graphic, *newGraphic; [super read:stream]; if (NXTypedStreamClassVersion(stream, [self name]) < FIP_ARCHIVE_FIX) { NXReadTypes(stream, "@sf", &glist, &gvFlags, &gridGray); } else { NXReadTypes(stream, "@If", &glist, &gvFlags, &gridGray); } for (i = [glist count]-1; i >= 0; i--) { graphic = [glist objectAt:i]; newGraphic = [graphic replaceWithImage]; if (graphic != newGraphic) { if (graphic) { [glist replaceObjectAt:i with:newGraphic]; } else { [glist removeObjectAt:i]; } } } [self getSelection]; [self resetGUP]; if (NXTypedStreamClassVersion(stream, [self name]) < 1) { editView = createEditView(self); } else { editView = NXReadObject(stream); } evsvs = [editView subviews]; for (i = [evsvs count]-1; i >= 0; i--) [[evsvs objectAt:i] free]; return self; } /* Methods to deal with being/becoming the First Responder */ /* Strings that appear in menus. */ static const char *HIDE_GRID; static const char *SHOW_GRID; static const char *TURN_GRID_OFF; static const char *TURN_GRID_ON; static const char *SHOW_LINKS; static const char *HIDE_LINKS; static BOOL menuStringsInitted = NO; static void initMenuItemStrings(void) { HIDE_GRID = NXLocalString("Hide Grid", NULL, "Menu item which hides the background grid which the user can overlay on his document."); SHOW_GRID = NXLocalString("Show Grid", NULL, "Menu item which shows a background grid which the user can overlay on his document."); TURN_GRID_OFF = NXLocalString("Turn Grid Off", NULL, "Menu item which turns off the background grid so that items do not move and resize on even grid boundaries. It does not hide the grid if it is currently showing."); TURN_GRID_ON = NXLocalString("Turn Grid On", NULL, "Menu item which turns a background grid on so that the user's actions are rounded to even grid boundaries. It does not show the grid if the grid is currently hidden."); SHOW_LINKS = NXLocalString("Show Links", NULL, "Menu item which turns on the borders around linked items (using Object Links)."); HIDE_LINKS = NXLocalString("Hide Links", NULL, "Menu item which turns off the borders around linked items (using Object Links)."); menuStringsInitted = YES; } /* Validates whether a menu command makes sense now */ static BOOL updateMenuItem(MenuCell *menuCell, const char *zeroItem, const char *oneItem, BOOL state) { if (state) { if (strcmp([menuCell title], zeroItem) || [menuCell tag] != 0) { [menuCell setTitle:zeroItem]; [menuCell setTag:0]; [menuCell setEnabled:NO]; // causes it to get redrawn } } else { if (strcmp([menuCell title], oneItem) || [menuCell tag] != 1) { [menuCell setTitle:oneItem]; [menuCell setTag:1]; [menuCell setEnabled:NO]; // causes it to get redrawn } } return YES; } - (BOOL)validateCommand:(MenuCell *)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. */ { Pasteboard *pb; int i, count, gcount; SEL action = [menuCell action]; static BOOL pboardHasPasteableType = NO; static BOOL pboardHasPasteableLink = NO; static int cachedPasteboardChangeCount = -1; if (!menuStringsInitted) initMenuItemStrings(); 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(align:)) { return([slist count] > 1); } else if (action == @selector(ungroup:)) { return(gvFlags.groupInSlist && [slist count] > 0); } else if (action == @selector(spellCheck:)) { for (i = [glist count]-1; i >= 0; i--) if ([[glist objectAt:i] isKindOf:[TextGraphic class]]) return YES; return NO; } 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:) || action == @selector(pasteAndLink:) || action == @selector(link:)) { pb = [Pasteboard new]; count = [pb changeCount]; if (count != cachedPasteboardChangeCount) { cachedPasteboardChangeCount = count; pboardHasPasteableType = (DrawPasteType([pb types]) != NULL); pboardHasPasteableLink = pboardHasPasteableType ? IncludesType([pb types], NXDataLinkPboardType) : NO; } return (action == @selector(paste:)) ? pboardHasPasteableType : pboardHasPasteableLink; } else if (action == @selector(hideGrid:)) { return (gvFlags.grid >= 4) ? updateMenuItem(menuCell, HIDE_GRID, SHOW_GRID, [self gridIsVisible]) : NO; } else if (action == @selector(enableGrid:)) { return (gvFlags.grid > 1) ? updateMenuItem(menuCell, TURN_GRID_OFF, TURN_GRID_ON, [self gridIsEnabled]) : NO; } else if (action == @selector(showLinks:)) { return linkManager ? updateMenuItem(menuCell, HIDE_LINKS, SHOW_LINKS, [linkManager areLinkOutlinesVisible]) : NO; } else if (action == @selector(print:)) { return([glist count] > 0); } return YES; } /* Useful scrolling routines. */ - scrollGraphicToVisible:(Graphic *)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]; } - scrollSelectionToVisible { NXRect sbounds; if ([slist count]) { [self getBBox:&sbounds of:slist]; return [self scrollRectToVisible:&sbounds]; } return self; } /* Private selection management methods. */ - createCacheWindow:(Window *)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; 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; } - selectionCache { static Window *selectioncache = nil; return selectioncache = [self createCacheWindow:selectioncache]; } - recacheSelection:(BOOL)updateLinks /* * 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]; if (updateLinks && !gvFlags.suspendLinkUpdate) [self updateTrackedLinks:&sbounds]; } return self; } - recacheSelection { return [self recacheSelection:YES]; } - 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)cacheList:(List *)aList into:(Window *)selectionCache withTransparentBackground:(BOOL)transparentBackground /* * 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, scframe; [self getBBox:&sbounds of:aList]; [selectionCache getFrame:&scframe]; if (scframe.size.width < sbounds.size.width || scframe.size.height < sbounds.size.height) { [selectionCache sizeWindow:MAX(scframe.size.width, sbounds.size.width) :MAX(scframe.size.height, sbounds.size.height)]; } [[selectionCache contentView] lockFocus]; PSsetgray(NX_WHITE); PSsetalpha(transparentBackground ? 0.0 : 1.0); /* 0.0 means 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); /* set back to fully opaque */ i = [aList count]; while (i--) [[[[aList objectAt:i] deselect] draw:NULL] select]; [Graphic showFastKnobFills]; PStranslate(sbounds.origin.x, sbounds.origin.y); [[selectionCache contentView] unlockFocus]; return [selectionCache gState]; } - (int)cacheList:(List *)aList into:(Window *)selectionCache { return [self cacheList:aList into:selectionCache withTransparentBackground:YES]; } - (int)cacheSelection { return [self cacheList:slist into:[self selectionCache] withTransparentBackground:YES]; } /* Other private methods. */ - cacheGraphic:(Graphic *)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 showFastKnobFills]; [[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 ? gvFlags.grid : 1); y = (int)bounds.size.height / (gvFlags.grid ? gvFlags.grid : 1); 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 ? gvFlags.grid : 1); gupCoords[j++] = w; gupCoords[j] = gupCoords[j-2]; j++; } for (i = 1; i <= x; i++) { gupCoords[j++] = i * (gvFlags.grid ? gvFlags.grid : 1); 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 restored when there is a TIMER event. */ { int gstate; NXEvent peek; NXCoord dx, dy; NXTrackingTimer *timer = NULL; NXPoint p, start, last, sboundspad; NXRect minbounds, sbounds, startbounds, visibleRect; BOOL canScroll, tracking = YES, alternate, horizConstrain = NO, vertConstrain = NO, hideCursor; last = event->location; alternate = (event->flags & NX_ALTERNATEMASK) ? YES : NO; event = [NXApp getNextEvent:MOVE_MASK]; if (event->type == NX_MOUSEUP) return NO; hideCursor = NXGetDefaultValue([NXApp appName], "HideCursorOnMove") ? YES : NO; if (hideCursor) PShidecursor(); [self convertPoint:&last fromView:nil]; [self grid:&last]; [self lockFocus]; gstate = [self cacheSelection]; gvFlags.suspendLinkUpdate = YES; /* we'll update links when the move is complete */ [self graphicsPerform:@selector(deactivate)]; gvFlags.suspendLinkUpdate = NO; [self getBBox:&sbounds of:slist]; startbounds = sbounds; [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 scrollPointToVisible:&p]; // actually we want to keep the "edges" of the // Graphic being resized that were visible when // the move started visible throughout the // moving session [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); if (hideCursor) PSshowcursor(); p.x = sbounds.origin.x - start.x; p.y = sbounds.origin.y - start.y; if (p.x || p.y) [self moveGraphicsBy:&p andDraw:NO]; gvFlags.suspendLinkUpdate = YES; /* we'll update links when the move is complete */ [self graphicsPerform:@selector(activate)]; gvFlags.suspendLinkUpdate = NO; NXUnionRect(&sbounds, &startbounds); [self updateTrackedLinks:&startbounds]; [self tryToPerform:@selector(updateRulers:) with:nil]; [window flushWindow]; [self unlockFocus]; return YES; } - moveGraphicsBy:(NXPoint *)vector andDraw:(BOOL)drawFlag { id change; change = [[MoveGraphicsChange alloc] initGraphicView:self vector:vector]; [change startChange]; if (drawFlag) { [self graphicsPerform:@selector(moveBy:) with:(id)vector]; } else { [slist makeObjectsPerform:@selector(moveBy:) with:(id)vector]; } [change endChange]; return self; } #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. */ { int i; Graphic *graphic; NXPoint p, last, start; NXTrackingTimer *timer = NULL; NXRect visibleRect, eb, region, oldRegion; BOOL mustContain, shift, canScroll, oldRegionSet = NO; 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(®ion, &p, &start); [window disableFlushWindow]; if (oldRegionSet) { NXInsetRect(&oldRegion, -1.0, -1.0); [self drawSelf:&oldRegion :1]; } if (canScroll) { [self scrollRectToVisible:®ion]; [self scrollPointToVisible:&p]; } PSrectstroke(region.origin.x, region.origin.y, region.size.width, region.size.height); [self tryToPerform:@selector(updateRulers:) with:(void *)®ion]; [[window reenableFlushWindow] flushWindow]; oldRegion = region; oldRegionSet = YES; last = p; NXPing(); } p = event->location; event = [NXApp getNextEvent:DRAG_MASK]; } if (canScroll) stopTimer(timer); for (i = [glist count] - 1; i >= 0; i--) { graphic = [glist objectAt:i]; [graphic getExtendedBounds:&eb]; if (![graphic isLocked] && ![graphic isSelected] && ((mustContain && NXContainsRect(®ion, &eb)) || (!mustContain && NXIntersectsRect(®ion, &eb)))) { [graphic select]; } } [self getSelection]; if (!dragRect) NX_MALLOC(dragRect, NXRect, 1); *dragRect = region; NXInsetRect(®ion, -1.0, -1.0); [self drawSelf:®ion :1]; [self recacheSelection:NO]; [self tryToPerform:@selector(updateRulers:) with:nil]; [self unlockFocus]; return self; } - alignGraphicsBy:(AlignmentType)alignType edge:(NXCoord *)edge { SEL action; id change; change = [[AlignGraphicsChange alloc] initGraphicView:self]; [change startChange]; action = [GraphicView actionFromAlignType:alignType]; [self graphicsPerform:action with:edge]; [change endChange]; return self; } - alignBy:(AlignmentType)alignType { int i; NXRect rect; Graphic *graphic; NXCoord minEdge = 10000.0; NXCoord maxEdge = 0.0; NXCoord baseline = 0.0; for (i = [slist count]-1; i >= 0 && !baseline; i--) { graphic = [slist objectAt:i]; [graphic getBounds:&rect]; switch (alignType) { case LEFT: if (rect.origin.x < minEdge) minEdge = rect.origin.x; break; case RIGHT: if (rect.origin.x + rect.size.width > maxEdge) maxEdge = rect.origin.x + rect.size.width; break; case BOTTOM: if (rect.origin.y < minEdge) minEdge = rect.origin.y; break; case TOP: if (rect.origin.y + rect.size.height > maxEdge) maxEdge = rect.origin.y + rect.size.height; break; case HORIZONTAL_CENTERS: if (rect.origin.y + floor(rect.size.height / 2.0) < minEdge) minEdge = rect.origin.y + floor(rect.size.height / 2.0); break; case VERTICAL_CENTERS: if (rect.origin.x + floor(rect.size.width / 2.0) < minEdge) minEdge = rect.origin.x + floor(rect.size.width / 2.0); break; case BASELINES: baseline = [graphic baseline]; break; } } switch (alignType) { case LEFT: case BOTTOM: case HORIZONTAL_CENTERS: case VERTICAL_CENTERS: [self alignGraphicsBy:alignType edge:&minEdge]; break; case RIGHT: case TOP: [self alignGraphicsBy:alignType edge:&maxEdge]; break; case BASELINES: if (baseline) [self alignGraphicsBy:alignType edge:&baseline]; } [window flushWindow]; return self; } @end
These are the contents of the former NiCE NeXT User Group NeXTSTEP/OpenStep software archive, currently hosted by Netfuture.ch.