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.