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

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

#import "draw.h"

@implementation TextGraphic
/*
 * This uses a text object to draw and edit text.
 *
 * The one quirky thing to understand here is that growable Text objects
 * in NeXTSTEP must be subviews of flipped view.  Since a GraphicView is not
 * flipped, we must have a flipped view into the view heirarchy when we
 * edit (this editing view is permanently installed as a subview of the
 * GraphicView--see GraphicView's newFrame: method).
 */

+ initialize
{
    [TextGraphic setVersion:6];	/* class version, see read: */
    return self;
}

static Text *drawText = nil;	/* shared Text object used for drawing */

static void initClassVars()
/*
 * Create the class variable drawText here.
 */
{
    if (!drawText) {
	drawText = [Text new];
	[drawText setMonoFont:NO];
	[drawText setEditable:NO];
	[drawText setSelectable:NO];
	[drawText setFlipped:YES];
    }
}

+ (BOOL)canInitFromPasteboard:(Pasteboard *)pboard
{
    return IncludesType([pboard types], NXRTFPboardType) ||
	   IncludesType([pboard types], NXAsciiPboardType);
}

- init
/*
 * Creates a "blank" TextGraphic.
 */
{
    initClassVars();
    [super init];
    return self;
}

- doInitFromStream:(NXStream *)stream
/*
 * Common code for initFromStream: and reinitFromStream:.
 * Looks at the first 5 characters of the stream and if it
 * looks like an RTF file, then the contents of the stream
 * are parsed as RTF, otherwise, the contents of the stream
 * are assumed to be ASCII text and is passed through the
 * drawText object and turned into RTF (using the method
 * (writeRichText:).
 */
{
    int maxlen;
    char *buffer;

    if (stream) {
	NXGetMemoryBuffer(stream, &buffer, &length, &maxlen);
	if (!strncmp(buffer, "{\\rtf", 5)) {
	    NX_ZONEMALLOC([self zone], data, char, length);
	    bcopy(buffer, data, length);
	    [drawText readRichText:stream];
	} else {
	    [drawText readText:stream];
	    stream = NXOpenMemory(NULL, 0, NX_WRITEONLY);
	    [drawText writeRichText:stream];
	    NXGetMemoryBuffer(stream, &buffer, &length, &maxlen);
	    NX_ZONEMALLOC([self zone], data, char, length);
	    bcopy(buffer, data, length);
	    NXCloseMemory(stream, NX_FREEBUFFER);
	}
	[drawText setSel:0 :0];
	font = [drawText font];
    }

    return self;
}

- initFromStream:(NXStream *)stream
/*
 * Initializes the TextGraphic using data from the passed stream.
 */
{
    initClassVars();
    [super init];
    if (stream) {
	[self doInitFromStream:stream];
	[drawText setHorizResizable:YES];
	[drawText setVertResizable:YES];
	bounds.size.width = bounds.size.height = 10000.0;
	[drawText setMaxSize:&bounds.size];
	[drawText calcLine];
	[drawText getMinWidth:&bounds.size.width minHeight:&bounds.size.height maxWidth:10000.0 maxHeight:10000.0];
	bounds.origin.x = bounds.origin.y = 0.0;
    }
    return self;
}

- initFromFile:(const char *)file
/*
 * Initializes the TextGraphic using data from the passed file.
 */
{
    TextGraphic *retval = nil;
    NXStream *stream = NXMapFile(file, NX_READONLY);
    retval = [self initFromStream:stream];
    NXCloseMemory(stream, NX_FREEBUFFER);
    return retval;
}


- initFromPasteboard:(Pasteboard *)pboard
/*
 * Initializes the TextGraphic using data from the passed Pasteboard.
 */
{
    NXStream *stream;

    if (IncludesType([pboard types], NXRTFPboardType)) {
	stream = [pboard readTypeToStream:NXRTFPboardType];
	[self initFromStream:stream];
	NXCloseMemory(stream, NX_FREEBUFFER);
    } else if (IncludesType([pboard types], NXAsciiPboardType)) {
	stream = [pboard readTypeToStream:NXAsciiPboardType];
	[self initFromStream:stream];
	NXCloseMemory(stream, NX_FREEBUFFER);
    } else {
	[self free];
	return nil;
    }

    return self;
}

- (NXRect)reinitFromStream:(NXStream *)stream
/*
 * Reinitializes the TextGraphic from the data in the passed stream.
 */
{
    NXRect ebounds;
    [self doInitFromStream:stream];
    [self getExtendedBounds:&ebounds];
    return ebounds;
}

- (NXRect)reinitFromFile:(const char *)file
/*
 * Reinitializes the TextGraphic from the data in the passed file.
 */
{
    NXRect ebounds;
    NXStream *stream = NXMapFile(file, NX_READONLY);
    [self doInitFromStream:stream];
    NXCloseMemory(stream, NX_FREEBUFFER);
    [self getExtendedBounds:&ebounds];
    return ebounds;
}

- (NXRect)reinitFromPasteboard:(Pasteboard *)pboard
/*
 * Reinitializes the TextGraphic from the data in the passed Pasteboard.
 */
{
    NXRect ebounds;
    NXStream *stream;

    if (IncludesType([pboard types], NXRTFPboardType)) {
	stream = [pboard readTypeToStream:NXRTFPboardType];
	[self doInitFromStream:stream];
	[self getExtendedBounds:&ebounds];
	NXCloseMemory(stream, NX_FREEBUFFER);
    } else if (IncludesType([pboard types], NXAsciiPboardType)) {
	stream = [pboard readTypeToStream:NXAsciiPboardType];
	[self doInitFromStream:stream];
	[self getExtendedBounds:&ebounds];
	NXCloseMemory(stream, NX_FREEBUFFER);
    } else {
	ebounds.origin.x = ebounds.origin.y = 0.0;
	ebounds.size.width = ebounds.size.height = 0.0;
    }

    return ebounds;
}

- free
{
    free(data);
    return [super free];
}

/* Link methods */

- setLink:(NXDataLink *)aLink
/*
 * Note that we "might" be linked because even though we obviously
 * ARE linked now, that might change in the future and the mightBeLinked
 * flag is only advisory and is never cleared.  This is because during
 * cutting and pasting, the TextGraphic might be linked, then unlinked,
 * then linked, then unlinked and we have to know to keep trying to
 * reestablish the link.  See readLinkForGraphic:... in gvLinks.m.
 */
{
    NXDataLink *oldLink = link;
    link = aLink;
    gFlags.mightBeLinked = YES;
    return oldLink;
}

- (NXDataLink *)link
{
    return link;
}

/* Form entry methods. */

/*
 * Form Entries are essentially text items whose location, font, etc., are
 * written out separately in an ASCII file when a Draw document is saved.
 * When this is done, an EPS image of the Draw view is also written out
 * (both of these files are place along with the document in the file package).
 * These ASCII descriptions can then be used by other applications to overlay
 * fields on top of a background of what is created by Draw.
 *
 * The most notable client of this right now is the Fax stuff.
 */

- initFormEntry:(const char *)entryName localizable:(BOOL)isLocalizable
/*
 * The localizeFormEntry stuff is used by the Fax stuff in the following manner:
 * If a form entry is localizable, then it appears in Draw in whatever the local
 * language is, but, when written to the ASCII form.info file, it is written out
 * not-localized.  Then, when the entity that reads the form.info file reads it,
 * it is responsible for localizing it.  This enables the entity reading the
 * form to actually semantically understand what a given form entry is (e.g. it
 * is the To: field in a Fax Cover Sheet).
 */ 
{
    char *buffer;
    int maxlen;
    NXStream *stream;

    initClassVars();
    [super init];
    gFlags.isFormEntry = YES;
    gFlags.localizeFormEntry = isLocalizable ? YES : NO;
    bounds.size.width = 300.0;
    bounds.size.height = 30.0;
    [drawText setText:entryName];
    [drawText setSel:0:100000];
    [drawText setSelColor:NX_COLORBLACK];
    [drawText setFont:[Font userFontOfSize:24.0 matrix:NX_FLIPPEDMATRIX]];
    [drawText setHorizResizable:YES];
    [drawText setVertResizable:YES];
    bounds.size.width = bounds.size.height = 10000.0;
    [drawText setMaxSize:&bounds.size];
    [drawText calcLine];
    [drawText getMinWidth:&bounds.size.width minHeight:&bounds.size.height maxWidth:10000.0 maxHeight:10000.0];
    bounds.origin.x = bounds.origin.y = 0.0;
    bounds.size.width = 300.0;
    stream = NXOpenMemory(NULL, 0, NX_WRITEONLY);
    [drawText writeRichText:stream];
    NXGetMemoryBuffer(stream, &buffer, &length, &maxlen);
    NX_ZONEMALLOC([self zone], data, char, length);
    bcopy(buffer, data, length);
    NXCloseMemory(stream, NX_FREEBUFFER);
    
    return self;
}

#define LOCAL_FORM_ENTRY(s) \
    NXLoadLocalStringFromTableInBundle("CoverSheet", [NXBundle mainBundle], s, NULL)
#define FORM_ENTRY_BUF_SIZE 100

- prepareFormEntry
/*
 * Loads up the drawText with all the right attributes to
 * display a form entry.  Called from draw.
 */
{
    NXCoord width, height;
    char *s, buffer[FORM_ENTRY_BUF_SIZE];

    [drawText setTextGray:NX_LTGRAY];
    [drawText setFont:[drawText font]];
    [drawText setAlignment:NX_LEFTALIGNED];
    [drawText getSubstring:buffer start:0 length:FORM_ENTRY_BUF_SIZE];
    buffer[FORM_ENTRY_BUF_SIZE-1] = '\0';
    if ((s = strchr(buffer, '\n')) || gFlags.localizeFormEntry) {
	if (s) *s = '\0';
	if (gFlags.localizeFormEntry) {
	    [drawText setText:LOCAL_FORM_ENTRY(buffer)];
	} else {
	    [drawText setText:buffer];
	}
    }
    [drawText setHorizResizable:YES];
    [drawText setVertResizable:YES];
    [drawText setMaxSize:&bounds.size];
    [drawText calcLine];
    [drawText getMinWidth:&width minHeight:&height maxWidth:10000.0 maxHeight:10000.0];
    if (width > bounds.size.width) width = bounds.size.width;
    if (height > bounds.size.height) height = bounds.size.height;
    [drawText sizeTo:width :height];
    [drawText moveTo:bounds.origin.x + floor((bounds.size.width - width) / 2.0)
		    :bounds.origin.y + floor((bounds.size.height - height) / 2.0)];

    return self;
}

- (BOOL)isFormEntry
{
    return gFlags.isFormEntry;
}

- setFormEntry:(int)flag
{
    gFlags.isFormEntry = flag ? YES : NO;
    return self;
}

- (Font *)getFormEntry:(char *)buffer andGray:(float *)gray
/*
 * Gets the information which will be written out into the
 * form.info ASCII form entry description file.  Specifically,
 * it gets the gray value, the actually name of the entry, and
 * the Font of the entry.
 */
{
    char *s;
    NXStream *stream;

    if (gFlags.isFormEntry) {
	stream = NXOpenMemory(data, length, NX_READONLY);
	[drawText readRichText:stream];
	[drawText setSel:0 :0];
	if (gray) *gray = [drawText selGray];
	NXCloseMemory(stream, NX_SAVEBUFFER);
	[drawText getSubstring:buffer start:0 length:FORM_ENTRY_BUF_SIZE];
	buffer[FORM_ENTRY_BUF_SIZE-1] = '\0';
	if (s = strchr(buffer, '\n')) *s = '\0';
	return [drawText font];
    }

    return nil;
}

- (BOOL)writeFormEntryToStream:(NXStream *)stream
/*
 * Writes out the ASCII representation of the location, gray,
 * etc., of this form entry.  This is called only during
 * the saving of a Draw document.
 */
{
    Font *myFont;
    float gray;
    char buffer[FORM_ENTRY_BUF_SIZE];

    if (myFont = [self getFormEntry:buffer andGray:&gray]) {
	NXPrintf(stream, "Entry: %s\n", buffer);
	NXPrintf(stream, "Font: %s\n", [myFont name]);
	NXPrintf(stream, "Font Size: %f\n", [myFont pointSize]);
	NXPrintf(stream, "Text Gray: %f\n", gray);
    	NXPrintf(stream, "Location: x = %d, y = %d, w = %d, h = %d\n",
	    (int)bounds.origin.x, (int)bounds.origin.y,
	    (int)bounds.size.width, (int)bounds.size.height);
	return YES;
    }

    return NO;
}

/* Factory methods overridden from superclass */

+ (BOOL)isEditable
{
    return YES;
}

+ cursor
{
    return NXIBeam;
}

/* Instance methods overridden from superclass */

- (const char *)title
{
    return NXLocalStringFromTable("Operations", "Text", NULL, "The %s of the `New %s' operation corresponding to creating an area for the user to type into.");
}

- (BOOL)create:(NXEvent *)event in:(GraphicView *)view
 /*
  * We are only interested in where the mouse goes up, that's
  * where we'll start editing.
  */
{
    NXRect viewBounds;

    event = [NXApp getNextEvent:NX_MOUSEUPMASK];
    bounds.size.width = bounds.size.height = 0.0;
    bounds.origin = event->location;
    [view convertPoint:&bounds.origin fromView:nil];
    [view getBounds:&viewBounds];
    gFlags.selected = NO;

    return NXMouseInRect(&bounds.origin, &viewBounds, NO);
}

- (BOOL)edit:(NXEvent *)event in:(View *)view
{
    id change;
    NXRect eb;

    if (gFlags.isFormEntry && gFlags.localizeFormEntry) return NO;
    if ([self link]) return NO;

    editView = view;
    graphicView = [editView superview];
    
    /* Get the field editor in this window. */

    if (gFlags.isFormEntry) {
	gFlags.isFormEntry = NO;
	[[view superview] cache:[self getExtendedBounds:&eb]];	// gFlags.isFormEntry starts editing
	[[view window] flushWindow];
	gFlags.isFormEntry = YES;
    }

    change = [[StartEditingGraphicsChange alloc] initGraphic:self];
    [change startChange];
	[self prepareFieldEditor];
	if (event) {  
	    [fe selectNull];	/* eliminates any existing selection */
	    [fe mouseDown:event]; /* Pass the event on to the Text object */
	} 
    [change endChange];

    return YES;
}

- draw
 /*
  * If the region has already been created, then we must draw the text.
  * To do this, we first load up the shared drawText Text object with
  * our rich text.  We then set the frame of the drawText object
  * to be our bounds.  Finally, we add the Text object as a subview of
  * the view that is currently being drawn in ([NXApp focusView])
  * and tell the Text object to draw itself.  We then remove the Text
  * object view from the view heirarchy.
  */
{
    NXStream *stream;

    if (data && (!gFlags.isFormEntry || NXDrawingStatus == NX_DRAWING)) {
	stream = NXOpenMemory(data, length, NX_READONLY);
	[drawText readRichText:stream];
	NXCloseMemory(stream, NX_SAVEBUFFER);
	if (gFlags.isFormEntry) {
	    [self prepareFormEntry];
	} else {
	    [drawText setFrame:&bounds];
	}
	[[NXApp focusView] addSubview:drawText];
	[drawText display];
	[drawText removeFromSuperview];
	if (DrawStatus == Resizing || gFlags.isFormEntry) {
	    PSsetgray(NX_LTGRAY);
	    NXFrameRect(&bounds);
	}
    }

    return self;
}

- performTextMethod:(SEL)aSelector with:(void *)anArgument
/*
 * This performs the given aSelector on the text by loading up
 * a Text object and applying aSelector to it (with selectAll:
 * having been done first).  See PerformTextGraphicsChange.m
 * in graphicsUndo.subproj.
 */
{
    id change;

    if (data) {
	change = [PerformTextGraphicsChange alloc];
	[change initGraphic:self view:graphicView];
	[change startChangeIn:graphicView];
	    [change loadGraphic];
	    [[change editText] perform:aSelector with:anArgument];
	    [change unloadGraphic];
	[change endChange];
    }

    return self;
}

- setFont:aFont
{
    font = aFont;
    return self;
}

- (char *)data
{
    return data;
}

- setData:(char *)newData
{
    if (data) NX_FREE(data);
    data = newData;
    return self;
}

- (int)length
{
    return length;
}

- setLength:(int)newLength
{
    length = newLength;
    return self;
}

- changeFont:sender
{
    [self performTextMethod:@selector(changeFont:) with:sender];
    return self;
}

- (Font *)font
{
    NXStream *stream;

    if (!font && data) {
	stream = NXOpenMemory(data, length, NX_READONLY);
	[drawText readRichText:stream];
	NXCloseMemory(stream, NX_SAVEBUFFER);
	[drawText setSel:0 :0];
	font = [drawText font];
    }

    return font;
}

- (BOOL)isOpaque
/*
 * We are never opaque.
 */
{
    return NO;
}

- (BOOL)isValid
/*
 * Any size TextGraphic is valid (since we fix up the size if it is
 * too small in our override of create:in:).
 */
{
    return YES;
}

- (NXColor)lineColor
{
    return NX_COLORBLACK;
}

- (NXColor)fillColor
{
    return NX_COLORWHITE;
}

- (NXCoord)baseline
{
    NXCoord ascender, descender, lineHeight;

    if (!font) [self font];
    if (font) {
	NXTextFontInfo(font, &ascender, &descender, &lineHeight);
	return bounds.origin.y + bounds.size.height + ascender;
    }

    return 0;
}

- moveBaselineTo:(NXCoord *)y
{
    NXCoord ascender, descender, lineHeight;

    if (y && !font) [self font];
    if (y && font) {
	NXTextFontInfo(font, &ascender, &descender, &lineHeight);
	bounds.origin.y = *y - ascender - bounds.size.height;
    }

    return self;
}

/* Public methods */

- prepareFieldEditor
/*
 * Here we are going to use the shared field editor for the window to
 * edit the text in the TextGraphic.  First, we must end any other editing
 * that is going on with the field editor in this window using endEditingFor:.
 * Next, we get the field editor from the window.  Normally, the field
 * editor ends editing when carriage return is pressed.  This is due to
 * the fact that its character filter is NXFieldFilter.  Since we want our
 * editing to be more like an editor (and less like a Form or TextField),
 * we set the character filter to be NXEditorFilter.  What is more, normally,
 * you can't change the font of a TextField or Form with the FontPanel
 * (since that might interfere with any real editable Text objects), but
 * in our case, we do want to be able to do that.  We also want to be
 * able to edit rich text, so we issue a setMonoFont:NO.  Editing is a bit
 * more efficient if we set the Text object to be opaque.  Note that
 * in textDidEnd:endChar: we will have to set the character filter,
 * FontPanelEnabled and mono-font back so that if there were any forms
 * or TextFields in the window, they would have a correctly configured
 * field editor.
 *
 * To let the field editor know exactly where editing is occurring and how
 * large the editable area may grow to, we must calculate and set the frame
 * of the field editor as well as its minimum and maximum size.
 *
 * We load up the field editor with our rich text (if any).
 *
 * Finally, we set self as the delegate (so that it will receive the
 * textDidEnd:endChar: message when editing is completed) and either
 * pass the mouse-down event onto the Text object, or, if a mouse-down
 * didn't cause editing to occur (i.e. we just created it), then we
 * simply put the blinking caret at the beginning of the editable area.
 *
 * The line marked with the "ack!" is kind of strange, but is necessary
 * since growable Text objects only work when they are subviews of a flipped
 * view.
 *
 * This is why GraphicView has an "editView" which is a flipped view that it
 * inserts as a subview of itself for the purposes of providing a superview
 * for the Text object.  The "ack!" line converts the bounds of the TextGraphic
 * (which are in GraphicView coordinates) to the coordinates of the Text
 * object's superview (the editView).  This limitation of the Text object
 * will be fixed post-1.0.  Note that the "ack!" line is the only one
 * concession we need to make to this limitation in this method (there is
 * another such line in resignFieldEditor).
 */
{
    NXSize maxSize;
    NXStream *stream;
    NXRect viewBounds, frame, eb;

    [NXApp sendAction:@selector(disableChanges:) to:nil from:self];
	[[graphicView window] endEditingFor:self];
	fe = [[graphicView window] getFieldEditor:YES for:self];
	
	if ([self isSelected]) {
	    [self deselect];
	    [graphicView cache:[self getExtendedBounds:&eb] andUpdateLinks:NO];
	    [[graphicView selectedGraphics] removeObject:self];
	}
	
	[fe setFont:[[FontManager new] selFont]];
    
	/* Modify it so that it will edit Rich Text and use the FontPanel. */
    
	[fe setCharFilter:NXEditorFilter];
	[fe setFontPanelEnabled:YES];
	[fe setMonoFont:NO];
	[fe setOpaque:YES];
    
	/*
	    * Determine the minimum and maximum size that the Text object can be.
	    * We let the Text object grow out to the edges of the GraphicView,
	    * but no further.
	    */
    
	[editView getBounds:&viewBounds];
	maxSize.width = viewBounds.origin.x+viewBounds.size.width-bounds.origin.x;
	maxSize.height = bounds.origin.y+bounds.size.height-viewBounds.origin.y;
	if (!bounds.size.height && !bounds.size.width) {
	    bounds.origin.y -= floor([fe lineHeight] / 2.0);
	    bounds.size.height = [fe lineHeight];
	    bounds.size.width = 5.0;
	}
	frame = bounds;
	[editView convertRect:&frame fromView:graphicView];	// ack!
	[fe setMinSize:&bounds.size];
	[fe setMaxSize:&maxSize];
	[fe setFrame:&frame];
	[fe setVertResizable:YES];
    
	/*
	    * If we already have text, then put it in the Text object (allowing
	    * the Text object to grow downward if necessary), otherwise, put
	    * no text in, set some initial parameters, and allow the Text object
	    * to grow horizontally as well as vertically
	    */
    
	if (data) {
	    [fe setHorizResizable:NO];
	    stream = NXOpenMemory(data, length, NX_READONLY);
	    [fe readRichText:stream];
	    NXCloseMemory(stream, NX_SAVEBUFFER);
	} else {
	    [fe setHorizResizable:YES];
	    [fe setText:""];
	    [fe setAlignment:NX_LEFTALIGNED];
	    [fe setSelColor:NX_COLORBLACK];
	    [fe unscript:self];
	}
    
	/*
	    * Add the Text object to the view heirarchy and set self as its delegate
	    * so that we will receive the textDidEnd:endChar: message when editing
	    * is finished.
	    */
    
	[fe setDelegate:self];
	[editView addSubview:fe];
    
	/*
	    * Make it the first responder.
	    */
    
	[[graphicView window] makeFirstResponder:fe];
    
	/* Change the ruler to be a text ruler. */
    
	[fe tryToPerform:@selector(showTextRuler:) with:fe];
    
	[fe setSel:0:0];
    [NXApp sendAction:@selector(enableChanges:) to:nil from:self];

    return self;
}

- resignFieldEditor
/* 
 * We must extract the rich text the user has typed from the Text object,
 * and store it away. We also need to get the frame of the Text object
 * and make that our bounds (but, remember, since the Text object must
 * be a subview of a flipped view, we need to convert the bounds rectangle
 * to the coordinates of the unflipped GraphicView).  If the Text object
 * is empty, then we remove this TextGraphic from the GraphicView.
 * We must remove the Text object from the view heirarchy and, since
 * this Text object is going to be reused, we must set its delegate
 * back to nil.
 *
 * For further explanation of the "ack!" line, see edit:in: above.
 */
{
    int maxlen;
    char *buffer;
    NXStream *stream;
    NXRect oldBounds, *redrawRect = NULL;

    [NXApp sendAction:@selector(disableChanges:) to:nil from:self];
	if (data) {
	    NX_FREE(data);
	    data = NULL;
	    length = 0;
	}
    
	NX_ASSERT(editView == [fe superview], "Fault in Text Graphic: Code 2");
	NX_ASSERT(graphicView == [editView superview], "Fault in Text Graphic: Code 3");
    
	if ([fe textLength]) {
	    stream = NXOpenMemory(NULL, 0, NX_WRITEONLY);
	    [fe writeRichText:stream];
	    NXGetMemoryBuffer(stream, &buffer, &length, &maxlen);
	    NX_ZONEMALLOC([self zone], data, char, length);
	    bcopy(buffer, data, length);
	    NXCloseMemory(stream, NX_FREEBUFFER);
	    oldBounds = bounds;
	    [fe getFrame:&bounds];
	    [editView convertRect:&bounds toView:graphicView];	// ack!
	    NXUnionRect(&bounds, &oldBounds);
	    redrawRect = &oldBounds;
	}

	if (redrawRect) [[graphicView window] disableFlushWindow];

	[graphicView tryToPerform:@selector(hideRuler:) with:nil];
	[fe removeFromSuperview];
	[fe setDelegate:nil];
	[fe setSel:0 :0];
	font = [fe font];
    
	if (redrawRect) {
    	    [graphicView cache:redrawRect];
	    [[graphicView window] reenableFlushWindow];
	    [[graphicView window] flushWindow];
	}

	fe = nil;
    [NXApp sendAction:@selector(enableChanges:) to:nil from:self];
    
    return self;
}

- (BOOL)isEmpty
{
    return data ? NO : YES;
}

/* Text object delegate methods */

/*
 * If we have more than one line, turn off horizontal resizing.
 */
- textDidResize:textObject oldBounds:(const NXRect *)oldBounds invalid:(NXRect *)invalidRect
{
    NXSelPt start,end;

    [textObject getSel:&start :&end];
    if (start.line || end.line)
      [textObject setHorizResizable:NO];
    return self;
}

- textDidEnd:textObject endChar:(unsigned short)endChar
/*
 * This method is called when ever first responder is taken away from a
 * currently editing TextGraphic (i.e. when the user is done editing and
 * chooses to go do something else).  
 */
{
    id change;

    NX_ASSERT(fe == textObject, "Fault in Text Graphic: Code 1")
    
    change = [[EndEditingGraphicsChange alloc] initGraphicView:graphicView  graphic:self];
    [change startChange];
        [self resignFieldEditor];
	if ([self isEmpty])
	    [graphicView removeGraphic:self];
    [change endChange];

    return self;
}

/* Archiving methods */

- awake
{
    initClassVars();
    return [super awake];
}

- write:(NXTypedStream *)stream
 /*
  * Writes the TextGraphic out to the typed stream.
  */
{
    [super write:stream];
    NXWriteTypes(stream, "i", &length);
    NXWriteArray(stream, "c", length, data);
    return self;
}

- read:(NXTypedStream *)stream
 /*
  * Reads the TextGraphic in from the typed stream.
  * This is versioned.  The old way we used to implement
  * this class included using a Cell object.  Now we
  * use the Text object directly.
  */
{
    int version;

    version = NXTypedStreamClassVersion(stream, "TextGraphic");
    [super read:stream];

    if (version < 1) {
	Cell *cell;
	int maxlen;
	NXStream *s;
	char *buffer;
	NXReadTypes(stream, "@", &cell);
	[drawText setText:[cell stringValue]];
	font = [cell font];
	[drawText setFont:[cell font]];
	[drawText setTextColor:[self lineColor]];
	s = NXOpenMemory(NULL, 0, NX_WRITEONLY);
	[drawText writeRichText:s];
	NXGetMemoryBuffer(s, &buffer, &length, &maxlen);
	NX_ZONEMALLOC([self zone], data, char, length);
	bcopy(buffer, data, length);
	NXCloseMemory(s, NX_FREEBUFFER);
    } else {
	NXReadTypes(stream, "i", &length);
	NX_ZONEMALLOC([self zone], data, char, length);
	NXReadArray(stream, "c", length, data);
    }

    if (version > 2 && version < 5) {
	int linkNumber;
	NXReadTypes(stream, "i", &linkNumber);
    } else if (version == 2) {
	int linkNumber;
	link = NXReadObject(stream);
	linkNumber = [link linkNumber];
	link = nil;
    }

    if (version > 3 && version < 6) {
	BOOL isFormEntry;
	NXReadTypes(stream, "c", &isFormEntry);
	gFlags.isFormEntry = isFormEntry ? YES : NO;
    }

    return self;
}

@end

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