This is MultApp.m in view mode; [Download] [Up]
#import "MultApp.h"
#import "MultDoc.h"
//#import "MultDrop.h"
#import "PrefDelegate.h"
#import "PageMargin.h"
#import "Utility.h"
#define PAGE_LAYOUT LocalString("Page_Layout")
#define INFO_PANEL LocalString("Info_Panel")
#define HELP_PANEL LocalString("Help_Panel")
#define PREF_PANEL LocalString("Preferences_Panel")
#import <appkit/Menu.h>
#import <appkit/MenuCell.h>
#import <appkit/Listener.h>
#import <appkit/Matrix.h>
#import <appkit/OpenPanel.h>
#import <appkit/publicWraps.h>
#import <appkit/Speaker.h>
#import <mach/mach_init.h>
#import <objc/List.h>
@implementation MultApp:Object
// Canon Information Systems is not responsible for anything anyone does with
// this code, nor are they responsible for the correctness of this code.
// Basically, this has very little to do with the company I work for, and you
// can't blame them.
// This file is best read in a window as wide as this comment, and with tab
// settings of 4 spaces and indent setting of 4 spaces (at least, that's what
// I use).
// You are welcome to do as you would with this file under the following
// conditions.
// First, I accept no blame for anything that goes wrong no matter how you
// use it, no matter how catastrophic, not even if it stems from a bug in my
// code. Second, please keep my notices on it when/if you distribute it. // Third, if you discover any bugs or have any comments, PLEASE TELL ME!
// Code won't get better without people picking it apart and giving the writer
// feedback. Fourth, if you modify it, please keep a notice that your version
// is based on mine in the source files (and keep the notice that mine is based
// on four other pieces of code :<). Thanks, and have fun.
// - Subrata Sircar, ssircar@canon.com
/* This class is intended as an Application Delegate. As such it */
/* is supposed to provide the document-management functionality */
/* that a multiple-document, drag-and-drop application should be */
/* able to do. This includes keeping track of the current thing, */
/* managing the shared panels, updating the defaults and menus, */
/* and controlling communication with the Workspace Manager. */
/* The paradigm used is that the application delegate relays new messages */
/* to the factory Document class, and every other doc message goes to */
/* the first responder. The delegate opens nib sections containing */
/* the info and preferences panel (the prefs panel should have a delegate */
/* to update it) and lets the helpObject worry about itself. Otherwise */
/* it acts on delegate messages. */
/* Source code stolen from Draw, Acceptor, and WhatsUpDoc. Thanks! */
/* Source code also taken from Ernest Prabhakar's BasicApp example, which */
/* includes more methods than mine does (for things like services, etc.) */
/* His examples are well worth checking out, especially if you need more */
/* functionality than this class can provide. */
/* - Subrata Sircar ssircar@canon.com Canon Information Systems */
/* Version 0.9b Apr-19-92 First Public Release */
/* Version 0.95b Jun-10-92 Minor File path fixes */
/* Version 1.00b Aug-10-92 Cleaned up #imports */
/* Version 1.1b Jan-10-93 Modified for 3.0 */
/* Preprocessor macros */
#define DocClass [[[self class] docClass] class]
#define DropClass [[[self class] dropClass] class]
#define DocExtension [DocClass extension]
#define theVersion ((float)([[self class] version])/100.0)
/* Factory (Class) Variables */
/* These are declared as static internal because I felt that all members */
/* of this class would use the same values for these variables. Subclasses */
/* can only manipulate these variables through the provided class methods. */
/* BE VERY CAREFUL about overriding methods which depends on the class */
/* variables without calling the super method - it might cause problems. */
static id docClass; // Class of Document to use
static id dropClass; // Class of DropView to use
static const char *theAppName; // Cache the appName for later use
static const char *noValue; // Cache the local no and yes
static const char *yesValue;
static const char *VersionFormat; // Version Format String
static const int myVersion = 110; // Version multiplied by 100
BOOL InMsgPrint = NO;
/* Factory (Class) Methods */
+ initialize
/* Class variable initialization. DO NOT call from subclasses. */
{
if (self == [MultApp class]) {
[self setVersion:myVersion];
[self setDocClass:[MultDoc class]];
// [self setDropClass:[MultDrop class]];
VersionFormat = LocalString("Version %.2f");
theAppName = NXCopyStringBufferFromZone([NXApp appName],MyZone);
noValue = LocalString("NO");
yesValue = LocalString("YES");
}
return self;
}
+ setDocClass:newDoc
/* Sets the document class and the file extensions to be used. Requires */
/* the document class to implement a class method to return the extension */
/* and the file types. */
{
docClass = newDoc;
return self;
}
+ docClass
{
return docClass;
}
+ setDropClass:newDropView
{
dropClass = newDropView;
return self;
}
+ dropClass
{
return dropClass;
}
/* Private C functions used to implement methods in this class. */
static void initMenu(id menu)
/*
* Sets the updateAction for every menu item which sends to the
* First Responder (i.e. their target is nil). When autoupdate is on,
* every event will be followed by an update of each of the menu items
* which is visible. This keep all unavailable menu items dimmed out
* so that the user knows what options are available at any given time.
*/
{
int count;
id matrix, cell;
id matrixTarget, cellTarget;
matrix = [menu itemList];
matrixTarget = [matrix target];
count = [matrix cellCount];
while (count--) {
cell = [matrix cellAt:count :0];
cellTarget = [cell target];
if (!matrixTarget && !cellTarget) {
[cell setUpdateAction:@selector(menuItemUpdate:) forMenu:menu];
} else if ([cell hasSubmenu]) {
initMenu(cellTarget);
} else if (cellTarget == [NXApp delegate])
[cell setUpdateAction:@selector(menuItemUpdate:) forMenu:menu];
}
}
static id documentInWindow(id window)
/*
* Checks to see if the passed window's delegate is a document.
* If it is, it returns that document, otherwise it returns nil.
*/
{
id document = [window delegate];
if (document) return [document isKindOf:docClass] ? document : nil;
else return nil;
}
static id findDocument(const char *name)
/*
* Searches the window list looking for a document with the specified name.
* Returns the window containing the document if found.
* If name == NULL then the first document found is returned.
*/
{
int count;
id document, window, windowList;
windowList = [NXApp windowList];
count = [windowList count];
while (count--) {
window = [windowList objectAt:count];
document = documentInWindow(window);
if (document && (!name || !strcmp([document fileName], name))) return window;
}
return nil;
}
static id openFile(const char *directory, const char *name, BOOL display)
/*
* Opens a file with the given name in the specified directory.
* If we already have that file open, it is ordered front.
* Returns the document if successful, nil otherwise.
*/
{
id window;
char buffer[MAXPATHLEN+1], path[MAXPATHLEN+1], temp[MAXPATHLEN+1];
if (name && *name) {
if (!directory) directory = ".";
else if (*directory != '/') {
strcpy(buffer, "./");
strcat(buffer, directory);
directory = buffer;
getwd(temp);
if (!chdir(directory) && getwd(path)) chdir(temp);
else {
Notify(LocalString("Open: path invalid"), directory);
return nil;
}
} else strcpy(path,directory);
strcat(path, "/");
strcat(path, name);
window = findDocument(path);
if (window) {
if (display) [window makeKeyAndOrderFront:window];
return [window delegate];
} else return [docClass newFromFile:path];
}
return nil;
}
/* Public Methods */
- openFile:(const char *)path file:(const char *)type flag:(BOOL)flag
{
return openFile(path,type,flag);
}
- currentDocument
{
return documentInWindow([NXApp mainWindow]);
}
- (const char *)currentDirectory
/* Returns the current document's directory */
{
const char *retval = [[self currentDocument] directory];
if (!retval || !*retval) retval = defaultDir;
return retval;
}
- setDefaultDir:(const char *)dir
{
sprintf(defaultDir,"%s",dir);
return self;
}
- (const char *)launchDirectory
{
return launchDir;
}
/* Default support methods */
// These functions manage the app's behavior when it quits with unsaved documents
- (BOOL)saveAll
{
return saveAll;
}
- setSaveAll:(BOOL)value
{
saveAll = value;
return self;
}
- (BOOL)dumpAll
{
return dumpAll;
}
- setDumpAll:(BOOL)value
{
dumpAll = value;
return self;
}
/* Shared Panels */
- info:sender
/*
* Returns the information Panel if it hasn't already been loaded.
*/
{
id versionField; /* version field in info panel */
char buf[20];
if (!infoPanel) {
NXZone *zone = NXCreateChildZone(MyZone, vm_page_size, vm_page_size, YES);
NXNameZone(zone,"infoPanel");
if (LoadLocalNib(LocalString("Info.nib"),self,YES,zone)) {
versionField = NXGetNamedObject("VersionNumber", infoPanel);
if (versionField) {
sprintf(buf, VersionFormat,theVersion);
[versionField setStringValue:buf];
}
[infoPanel setFrameUsingName:INFO_PANEL];
} else {
char temp[MAXPATHLEN+1];
sprintf(temp,"%s %s\n",LocalString("Could not load requested resource"),LocalString("Info.nib"));
Notify(LocalString("Resource Error"),temp);
}
NXMergeZone(zone);
}
[infoPanel orderFront:self];
return infoPanel;
}
- pref:sender
/*
* The preferences panel is a separate nib module and only loaded on demand.
* When loaded, the object that manages the panel is asked to update it.
*/
{
if (!prefPanel) {
NXZone *zone = NXCreateChildZone(MyZone, vm_page_size, vm_page_size, YES);
if (LoadLocalNib(LocalString("Pref.nib"),self,NO,zone)) {
[prefPanel setFrameUsingName:PREF_PANEL];
} else {
char temp[MAXPATHLEN+1];
sprintf(temp,"%s %s\n",LocalString("Could not load requested resource"),LocalString("Pref.nib"));
Notify(LocalString("Resource Error"),temp);
}
NXMergeZone(zone);
}
[[prefPanel delegate] load:self];
[prefPanel makeKeyAndOrderFront:self];
return prefPanel;
}
- saveAsPanel:sender
/*
* Gets us a SavePanel with an accessory
*/
{
id savepanel = [SavePanel new];
[savepanel setAccessoryView:nil];
[savepanel setRequiredFileType:DocExtension];
return savepanel;
}
- pageLayout:sender
/*
* Returns the application-wide PageLayout panel, with margins.
*/
{
return pageMargin;
}
- stringSet:sender
/*
* Returns the application-wide string table, if it exists.
*/
{
if (stringSet) return stringSet;
else return nil;
}
/* Target/Action Methods */
- new:sender
/*
* Called by pressing New in the Document menu.
*/
{
[[docClass class] new];
return self;
}
- open:sender
/*
* Called by pressing Open... in the Document menu.
*/
{
const char *directory;
const char *const *files;
const char *const myType[2] = {DocExtension, NULL};
id openpanel = [[OpenPanel new] allowMultipleFiles:YES];
directory = [self currentDirectory];
if (directory && (*directory == '/')) [openpanel setDirectory:directory];
if ([openpanel runModalForTypes:myType]) {
files = [openpanel filenames];
directory = [openpanel directory];
while (files && *files) {
haveOpenedDocument = openFile(directory, *files, YES) || haveOpenedDocument;
files++;
}
}
return self;
}
- saveAll:sender
/*
* Saves all the documents.
*/
{
int count;
id windowList;
windowList = [NXApp windowList];
count = [windowList count];
while (count--) {
[documentInWindow([windowList objectAt:count]) save:self];
}
return self;
}
- print:sender
{
return [[[self currentDocument] view] printPSCode:sender];
}
- mailToMe:sender
// Stolen from Opener 3.0. Thanks, Michael!
{
char subj[256], w[256] = "whoami";
char body[4096]="\
Subrata:\n\n\
Great source code! It runs like beauty in the night...\n\
BUT! I do have a few comments:\n\n\
<insert accolades, criticisms & suggestions here>\n\n\
Sincerely,\n\
";
#define call(a,b) [s performRemoteMethod:a with:b length:strlen(b)+1]
id s = [NXApp appSpeaker];
port_t mailPort = NXPortFromName("Mail", NULL); // make sure app is launched
mailPort = NXPortFromName("MailSendDemo",NULL);
if (mailPort == PORT_NULL) {
Notify(LocalString("Suggestion attempt failed with Missing Port to Mail"),LocalString("Check if Mail is running."));
return self;
}
[s setSendPort:mailPort];
sprintf(subj,"Comments and suggestions for ``%s'', Version %.2f ",theAppName,theVersion);
strcat(body,execstr(w)); strcat(body,"\n");
call("setTo:","ssircar@canon.com");
call("setSubject:",subj);
call("setBody:",body);
return self;
}
/* Automatic update methods */
- (BOOL)menuItemUpdate:menuCell
/*
* Method called by all menu items which send their actions to the
* First Responder. First, if the object which would respond if the
* action was sent down the responder chain also responds to the message
* validateCommand:, then it is sent validateCommand: to determine
* whether that command is valid now; otherwise, if there is a responder
* to the message, then it is assumed that the item is valid.
* The method returns YES if the cell has changed its appearance (so that
* the caller (a Menu) knows to redraw it).
* This method also traps menu items sending to this class, and sets the
* action the same way.
*/
{
SEL action;
id responder, target;
BOOL enable;
target = [menuCell target];
enable = [menuCell isEnabled];
if (!target) {
action = [menuCell action];
responder = [NXApp calcTargetForAction:action];
if ([responder respondsTo:@selector(validateCommand:)]) {
enable = [responder validateCommand:menuCell];
} else enable = responder ? YES : NO;
} else if (target == self) enable = [self validateCommand:menuCell];
if ([menuCell isEnabled] != enable) {
[menuCell setEnabled:enable];
return YES;
} else return NO;
}
- (BOOL)validateCommand:menuCell
// The only messages the delegate is asked to validate are SaveAll and Print.
{
SEL action = [menuCell action];
if ((action == @selector(saveAll:)) || (action == @selector(print:))) return findDocument(NULL) ? YES : NO;
return YES;
}
- setupPageLayout:(float)lm :(float)rm :(float)tm :(float)bm
{
if (!pageMargin) {
pageMargin = [PageMargin new];
[pageMargin setPlpAccessory:plpAccessory];
[pageMargin setFrameUsingName:PAGE_LAYOUT];
}
[pageMargin setValues:lm right:rm top:tm bottom:bm];
[pageMargin writePrintInfo];
return self;
}
/* Application Delegate Methods */
- appWillInit:sender
{
char temp[MAXPATHLEN+1];
float lm,rm,tm,bm;
#ifndef DEBUG /* Don't Dump Core if not Debugging */
struct rlimit rl={ 0, 0};
getrlimit( RLIMIT_CORE, &rl);
rl.rlim_cur=0;
setrlimit( RLIMIT_CORE, &rl);
#endif
/* Set the document class variable to point to us */
[DocClass setAppDelegate:self];
/* Check and load the application defaults. The basics are the quit */
/* behavior and the page margins. Also sets the menu updating flag. */
if (!NXGetDefaultValue(theAppName,LocalString("SaveAll"))) sprintf(temp,noValue);
else sprintf(temp,NXGetDefaultValue(theAppName,LocalString("SaveAll")));
if (!strcmp(temp,yesValue)) saveAll = YES;
if (!NXGetDefaultValue(theAppName,LocalString("DumpAll"))) sprintf(temp,noValue);
else sprintf(temp,NXGetDefaultValue(theAppName,LocalString("DumpAll")));
if (!strcmp(temp,yesValue)) dumpAll = YES;
if (!NXGetDefaultValue(theAppName,LocalString("LeftMargin"))) sprintf(temp,"36.0");
else sprintf(temp,NXGetDefaultValue(theAppName,LocalString("LeftMargin")));
sscanf(temp,"%f",&lm);
if (!NXGetDefaultValue(theAppName,LocalString("RightMargin"))) sprintf(temp,"36.0");
else sprintf(temp,NXGetDefaultValue(theAppName,LocalString("RightMargin")));
sscanf(temp,"%f",&rm);
if (!NXGetDefaultValue(theAppName,LocalString("TopMargin"))) sprintf(temp,"36.0");
else sprintf(temp,NXGetDefaultValue(theAppName,LocalString("TopMargin")));
sscanf(temp,"%f",&tm);
if (!NXGetDefaultValue(theAppName,LocalString("BottomMargin"))) sprintf(temp,"36.0");
else sprintf(temp,NXGetDefaultValue(theAppName,LocalString("BottomMargin")));
sscanf(temp,"%f",&bm);
[self setupPageLayout:lm :rm :tm :bm];
[NXApp setAutoupdate:YES];
return self;
}
- appDidInit:sender
/*
* Register our window with the Workspace for icon dropping.
* Check for files to open specified on the command line.
* Initialize the menus and check if we were opened to print or quit.
* If there are no open documents, then open blank documents.
*/
{
int i;
char buffer[MAXPATHLEN+1], temp[MAXPATHLEN+1];
char *directory, *name, *ext;
// NXRect theRect;
// View *appIconView = [[NXApp appIcon] contentView];
// id myDropView = [DropClass allocFromZone:[appIconView zone]];
/* register our app icon view for image dragging destination */
// [appIconView getFrame:&theRect];
// [myDropView initFrame:&theRect];
// [appIconView addSubview:myDropView];
// [myDropView registerForDraggedTypes:&NXFilenamePboardType count:1];
// [myDropView registerForDraggedTypes:&NXAsciiPboardType count:1];
launchDir = appDirectory();
[self setDefaultDir:NXHomeDirectory()];
/* Check for command line files to open */
if (NXArgc > 1) {
for (i = 1; i < NXArgc; i++) {
strcpy(buffer, NXArgv[i]);
ext = rindex(buffer, '.');
if (!ext) {
strcat(buffer,".");
strcat(buffer, DocExtension);
}
if (*buffer == '/') {
directory = "/";
name = buffer;
name++;
} else {
name = rindex(buffer, '/');
if (name) {
*name++ = '\0';
sprintf(temp,"%s/%s",launchDir,buffer);
directory = temp;
} else {
name = buffer;
directory = NULL;
}
}
haveOpenedDocument = openFile(directory, name, YES) || haveOpenedDocument;
}
}
if (!haveOpenedDocument) [self new:self]; /* if none opened, open one */
if (NXGetDefaultValue(theAppName,LocalString("Quit"))) {
[NXApp activateSelf:YES];
NXPing();
[NXApp terminate:self];
}
initMenu([NXApp mainMenu]);
return self;
}
- app:sender willShowHelpPanel:panel
{
if (!helpPanel) {
helpPanel = panel;
[helpPanel setFrameUsingName:HELP_PANEL];
}
return self;
}
- appWillTerminate:sender
/*
* Overridden to be sure all documents get an opportunity to be saved
* before exiting the program. Save the defaults too.
*/
{
float lm,rm,tm,bm;
char temp[100];
id window, document;
id windowList = [NXApp windowList];
int choice = 0, count = [windowList count];
[pageMargin saveFrameUsingName:PAGE_LAYOUT];
if (helpPanel) [helpPanel saveFrameUsingName:HELP_PANEL];
if (infoPanel) [infoPanel saveFrameUsingName:INFO_PANEL];
if (prefPanel) [prefPanel saveFrameUsingName:PREF_PANEL];
[pageMargin getValues:&lm right:&rm top:&tm bottom:&bm];
sprintf(temp,"%f",lm);
NXWriteDefault([NXApp appName],LocalString("LeftMargin"),temp);
sprintf(temp,"%f",rm);
NXWriteDefault([NXApp appName],LocalString("RightMargin"),temp);
sprintf(temp,"%f",tm);
NXWriteDefault([NXApp appName],LocalString("TopMargin"),temp);
sprintf(temp,"%f",bm);
NXWriteDefault([NXApp appName],LocalString("BottomMargin"),temp);
if (saveAll) {
while (count--) {
document = [[windowList objectAt:count] delegate];
if ([document respondsTo:@selector(needsSaving)] && [document needsSaving]) {
[document save:self];
}
}
NXWriteDefault(theAppName,LocalString("SaveAll"),yesValue);
NXWriteDefault(theAppName,LocalString("DumpAll"),noValue);
} else if (dumpAll) {
NXWriteDefault(theAppName,LocalString("DumpAll"),yesValue);
NXWriteDefault(theAppName,LocalString("SaveAll"),noValue);
} else {
NXWriteDefault(theAppName,LocalString("DumpAll"),noValue);
NXWriteDefault(theAppName,LocalString("SaveAll"),noValue);
while (count--) {
window = [windowList objectAt:count];
document = [window delegate];
if ([document respondsTo:@selector(needsSaving)] && [document needsSaving]) {
choice = NXRunAlertPanel(LocalString("Quit"),
LocalString("You have unsaved documents."),
LocalString("Review Unsaved"),
LocalString("Quit Anyway"),
LocalString("Cancel"));
if (choice == NX_ALERTOTHER) return nil;
else if (choice == NX_ALERTALTERNATE) return self;
else if (choice == NX_ALERTDEFAULT) {
count = 0;
choice = [windowList count];
while (choice--) {
window = [windowList objectAt:choice];
document = [window delegate];
if ([document respondsTo:@selector(windowWillClose:)]) {
if ([document windowWillClose:self]) [window close];
else return nil;
}
}
}
}
}
}
return self;
}
- (int)appOpenFile:(const char *)path type:(const char *)type
// This method is performed whenever a user double-clicks on an icon
// in the Workspace Manager representing a app program document.
// It is also called via command-drag-n-drop.
{
char *name;
char directory[MAXPATHLEN+1];
if (type && !strcmp(type, DocExtension)) {
strcpy(directory, path);
name = rindex(directory, '/');
if (name) {
#ifdef DEBUG
printf("Directory %s: File %s\n",directory,name);
#endif
if (name != index(directory, '/')) {
*name++ = '\0';
if (openFile(directory, name, YES)) {
haveOpenedDocument = YES;
return YES;
}
} else {
name++;
if (openFile("/", name, YES)) {
haveOpenedDocument = YES;
return YES;
}
}
}
} else {
sprintf(directory,LocalString("%s is not a file %s can open. An attempt to filter appropriate data will be made."),path,[NXApp appName]);
Notify(LocalString("File Open Warning"),directory);
if ([DocClass newFromPasteboard:[Pasteboard newByFilteringFile:path]]) return YES;
}
return NO;
}
- (BOOL)appAcceptsAnotherFile:sender
/*
* We accept any number of appOpenFile:type: messages.
*/
{
return YES;
}
- (int)app:sender unmounting:(const char *)fullPath
{
id windowList = [NXApp windowList];
int count = [windowList count];
id document = NULL;
/* if any of our documents are in that path, close them and notify the user */
while (count--) {
document = documentInWindow([windowList objectAt:count]);
if (document && (!strncmp([document directory],fullPath,strlen(fullPath)))) {
Notify(fullPath,LocalString("is being unmounted; documents here will close."));
/* Use performClose so that the window delegate will be notified. */
[[windowList objectAt:count] performClose:self];
}
}
/* the current directory is wherever our current document is so we're set */
return 1;
}
/* Listener/Speaker remote methods */
// Filename and path methods
- (int)msgDirectory:(const char **)fullPath ok:(int *)flag
{
*fullPath = [self currentDirectory];
if (*fullPath) *flag = YES;
else {
*fullPath = "";
*flag = NO;
}
return 0;
}
- (int)msgFile:(const char **)fullPath ok:(int *)flag
{
const char *file;
file = [[self currentDocument] fileName];
if (file) {
*fullPath = file;
*flag = YES;
} else {
*fullPath = "";
*flag = NO;
}
return 0;
}
// Miscellaneous messages we might want to handle
- (int)msgPrint:(const char *)fullPath ok:(int *)flag
{
BOOL close;
id document = nil;
char *directory, *name;
char temp[MAXPATHLEN+1];
char path[MAXPATHLEN+1];
char buffer[MAXPATHLEN+1];
InMsgPrint = YES;
strcpy(buffer, fullPath);
name = rindex(buffer, '/');
if (name) {
*name++ = '\0';
directory = buffer;
} else {
name = buffer;
getwd(temp);
directory = temp;
}
strcpy(path,directory);
strcat(path, "/");
strcat(path, name);
document = [findDocument(path) delegate];
if (document) close = NO;
else {
document = openFile(directory, name, NO);
if (document) haveOpenedDocument = YES;
close = YES;
}
if (document && ![[document view] isEmpty]) {
[NXApp setPrintInfo:[document printInfo]];
[[document view] printPSCode:self];
if (close) [[[document view] window] performClose:self];
*flag = YES;
} else *flag = NO;
InMsgPrint = NO;
return 0;
}
- (int)msgVersion:(const char **)aString ok:(int *)flag
{
char buf[20];
sprintf(buf, VersionFormat, theVersion);
*aString = NXCopyStringBuffer(buf);
*flag = YES;
return 0;
}
- (int)msgQuit:(int *)flag
{
*flag = ([NXApp terminate:self] ? NO : YES);
return 0;
}
@end
These are the contents of the former NiCE NeXT User Group NeXTSTEP/OpenStep software archive, currently hosted by Netfuture.ch.