This is AppController.m in view mode; [Download] [Up]
#import "AppController.h"
#import "ImageView.h"
#import "Thumber.h"
#define THUMBSIZE 80
NSConditionLock *globalLock;
TransferData global;
@interface AppController(PrivateMethods)
- (BOOL)_internalOpenGigernail:(NSString *)filename;
- (BOOL)_internalOpenGigerwww:(NSString *)filename;
- (BOOL)_internalOpenGiger:(NSString *)filename;
- (BOOL)_internalSaveGigernail:(NSString *)filename;
- (BOOL)_internalSaveGigerwww:(NSString *)filename;
- (BOOL)_internalSaveGiger:(NSString *)filename;
@end
@implementation AppController(PrivateMethods)
- (BOOL)_internalSaveGiger:(NSString *)filename
{
NSArray *cells;
if(!numberOfThumbnails)
return NO;
cells = [thumbView cells];
if(![NSArchiver archiveRootObject:cells toFile:filename])
return NO;
return YES;
}
- (BOOL)_internalOpenGiger:(NSString *)filename
{
NSArray *cells;
cells = [NSUnarchiver unarchiveObjectWithFile:filename];
if(cells){
[thumbView removeRow:0];
[thumbView renewRows:0 columns:[cells count]];
[thumbView addRowWithCells:cells];
[thumbView sizeToCells];
[[thumbView window] display];
numberOfThumbnails = [cells count];
return YES;
} else
return NO;
}
- (BOOL)_internalOpenGigernail:(NSString *)filename
{
NSString *componentName;
NSArray *cells, *thumbNames;
unsigned int i,n;
NSImage *image;
NSButtonCell *cell;
componentName = [NSString stringWithFormat:@"%@/Thumbs.plist",filename];
thumbNames = [NSArray arrayWithContentsOfFile:componentName];
if(thumbNames){
n = [thumbNames count];
[thumbView removeRow:0];
[thumbView renewRows:1 columns:n];
cells = [thumbView cells];
for(i = 0; i < n; i++){
componentName = [NSString stringWithFormat:@"%@/t%d.tiff", filename, i];
cell = [cells objectAtIndex:i];
image = [[NSImage alloc] initWithContentsOfFile:componentName];
[cell setImage:image];
[image release];
[cell setRepresentedObject:[thumbNames objectAtIndex:i]];
}
[thumbView sizeToCells];
[[thumbView window] display];
numberOfThumbnails = n;
return YES;
} else
return NO;
}
- (BOOL)_internalOpenGigerwww:(NSString *)filename
{
NSString *componentName;
NSArray *cells, *thumbNames;
unsigned int i,n;
NSImage *image;
NSBitmapImageRep *bitmap;
NSButtonCell *cell;
componentName = [NSString stringWithFormat:@"%@/Thumbs.plist",filename];
thumbNames = [NSArray arrayWithContentsOfFile:componentName];
if(thumbNames){
n = [thumbNames count];
[thumbView removeRow:0];
[thumbView renewRows:1 columns:n];
cells = [thumbView cells];
for(i = 0; i < n; i++){
componentName = [NSString stringWithFormat:@"%@/t%d.gif", filename, i];
cell = [cells objectAtIndex:i];
bitmap = [imageDecoder decodeFromFile:componentName];
if(bitmap){
image = [[NSImage alloc] init];
[image addRepresentation:bitmap];
[cell setImage:image];
[image release];
}
[cell setRepresentedObject:[thumbNames objectAtIndex:i]];
}
[thumbView sizeToCells];
[[thumbView window] display];
numberOfThumbnails = n;
return YES;
} else
return NO;
}
- (BOOL)_internalSaveGigernail:(NSString *)filename
{
NSString *fileComponentName;
NSData *tiffData;
NSFileManager *fm = [NSFileManager defaultManager];
BOOL isDir;
NSMutableString *plistString;
NSArray *cells;
NSButtonCell *cell;
unsigned int i;
if(!numberOfThumbnails)
return NO;
if([fm fileExistsAtPath:filename isDirectory:&isDir]){
if(![fm removeFileAtPath:filename handler:nil]){
NSLog(@"Could not remove old directory %@", filename);
NSBeep();
return NO;
}
}
if(![fm createDirectoryAtPath:filename attributes:nil]){
NSLog(@"Could not create directory %@", filename);
NSBeep();
return NO;
}
plistString = [[NSMutableString alloc] init];
[plistString appendString:@"(\n"];
cells = [thumbView cells];
for(i = 0; i < numberOfThumbnails; i++){
cell = [cells objectAtIndex:i];
fileComponentName = [NSString stringWithFormat:@"%@/t%d.tiff", filename, i];
tiffData = [[cell image] TIFFRepresentation];
if(![tiffData writeToFile:fileComponentName atomically:YES]){
NSLog(@"Could not write thumbnail %@", fileComponentName);
NSBeep();
}
[plistString appendFormat:@"\t\"%@\",\n", [cell representedObject]];
}
[plistString appendString:@")\n"];
fileComponentName = [NSString stringWithFormat:@"%@/Thumbs.plist", filename];
if(![plistString writeToFile:fileComponentName atomically:YES]){
NSLog(@"Could not write plist");
NSBeep();
}
[plistString release];
return YES;
}
- (BOOL)_internalSaveGigerwww:(NSString *)filename
{
NSString *fileComponentName;
NSData *tiffData, *gifData;
NSFileManager *fm = [NSFileManager defaultManager];
BOOL isDir;
NSMutableString *plistString;
NSArray *cells;
NSButtonCell *cell;
unsigned int i;
NSAutoreleasePool *pool;
NSPasteboard *filterPB;
if(!numberOfThumbnails)
return NO;
if([fm fileExistsAtPath:filename isDirectory:&isDir]){
if(![fm removeFileAtPath:filename handler:nil]){
NSLog(@"Could not remove old directory %@", filename);
NSBeep();
return NO;
}
}
if(![fm createDirectoryAtPath:filename attributes:nil]){
NSLog(@"Could not create directory %@", filename);
NSBeep();
return NO;
}
plistString = [[NSMutableString alloc] init];
[plistString appendString:@"(\n"];
cells = [thumbView cells];
for(i = 0; i < numberOfThumbnails; i++){
pool = [[NSAutoreleasePool alloc] init];
cell = [cells objectAtIndex:i];
fileComponentName = [NSString stringWithFormat:@"%@/t%d.gif", filename, i];
tiffData = [[cell image] TIFFRepresentation];
filterPB = [NSPasteboard pasteboardByFilteringData:tiffData ofType:NSTIFFPboardType];
gifData = [filterPB dataForType:@"image format gif"];
if(gifData && ![gifData writeToFile:fileComponentName atomically:YES]){
NSLog(@"Could not write thumbnail %@", fileComponentName);
NSBeep();
}
[plistString appendFormat:@"\t\"%@\",\n", [cell representedObject]];
[filterPB releaseGlobally];
[pool release];
}
[plistString appendString:@")\n"];
fileComponentName = [NSString stringWithFormat:@"%@/Thumbs.plist", filename];
if(![plistString writeToFile:fileComponentName atomically:YES]){
NSLog(@"Could not write plist");
NSBeep();
}
[plistString release];
return YES;
}
@end
@implementation AppController
- (oneway void)provideGlobalData
{
NSAutoreleasePool *subpool;
NSBitmapImageRep *bitmap;
[globalLock lock];
if(global.full){
NSZoneFree(NSDefaultMallocZone(),global.imagePlanes);
[global.bitmap release];
[global.image release];
[global.filename release];
global.full = NO;
}
if(isRunning && currentJob < [filenames count]){
global.filename = [[filenames objectAtIndex:currentJob] retain];
global.image = [[NSImage alloc] init];
bitmap = [imageDecoder decodeFromFile:global.filename];
if(!global.image || !bitmap){
[global.filename release];
global.full = NO;
[globalLock unlockWithCondition:HAS_DATA];
return;
}
[global.image addRepresentation:bitmap];
subpool = [[NSAutoreleasePool alloc] init];
global.bitmap = [[NSBitmapImageRep alloc] initWithData:[global.image TIFFRepresentation]];
[subpool release];
if(!global.bitmap){
[global.filename release];
[global.image release];
global.full = NO;
[globalLock unlockWithCondition:HAS_DATA];
return;
}
global.imageInfo.bitsPerPixel = [global.bitmap bitsPerPixel];
global.imageInfo.samplesPerPixel = [global.bitmap samplesPerPixel];
global.imageInfo.bitsPerSample = [global.bitmap bitsPerSample];
global.imageInfo.isPlanar = [global.bitmap isPlanar];
global.imageInfo.hasAlpha = [global.bitmap hasAlpha];
global.imageInfo.numberOfPlanes = [global.bitmap numberOfPlanes];
global.imageInfo.bytesPerPlane = [global.bitmap bytesPerPlane];
global.imageInfo.bytesPerRow = [global.bitmap bytesPerRow];
global.imageInfo.pixelsHigh = [global.bitmap pixelsHigh];
global.imageInfo.pixelsWide = [global.bitmap pixelsWide];
// normally it should be global.imageInfo.samplesPerPixel
// but due to a bug ?? it only works with something greater like 10
// global.imagePlanes = NSZoneMalloc(NSDefaultMallocZone(),
// global.imageInfo.samplesPerPixel * sizeof(unsigned char *));
global.imagePlanes = NSZoneMalloc(NSDefaultMallocZone(), 10 * sizeof(unsigned char *));
[global.bitmap getBitmapDataPlanes:global.imagePlanes];
global.imageData = [global.bitmap bitmapData];
global.full = YES;
}
[globalLock unlockWithCondition:HAS_DATA];
}
- (oneway void)displayGlobalData
{
NSButtonCell *cell;
NSWindow *window = [thumbView window];
NSImage *thumbnail, *thumbSharpenedNail;
NSBitmapImageRep *thumbBitmap, *thumbSharpenedBitmap;
[globalLock lock];
if(!global.full){
[globalLock unlockWithCondition:NO_DATA];
currentJob++;
if(isRunning && currentJob < [filenames count])
[thumber thumbWithSize:THUMBSIZE];
else {
job = NO;
isRunning = NO;
[statusTextField setStringValue:@""];
[window display];
[NSApp updateWindows];
}
return;
}
if(global.thumbnailPlanes){
thumbBitmap = [[NSBitmapImageRep alloc] initWithBitmapDataPlanes:global.thumbnailPlanes
pixelsWide:global.thumbnailInfo.pixelsWide
pixelsHigh:global.thumbnailInfo.pixelsHigh
bitsPerSample:global.thumbnailInfo.bitsPerSample
samplesPerPixel:global.thumbnailInfo.samplesPerPixel
hasAlpha:global.thumbnailInfo.hasAlpha
isPlanar:global.thumbnailInfo.isPlanar
colorSpaceName:[global.bitmap colorSpaceName]
bytesPerRow:global.thumbnailInfo.bytesPerRow
bitsPerPixel:global.thumbnailInfo.bitsPerPixel];
thumbSharpenedBitmap = [[NSBitmapImageRep alloc] initWithBitmapDataPlanes:global.thumbnailSharpenedPlanes
pixelsWide:global.thumbnailInfo.pixelsWide
pixelsHigh:global.thumbnailInfo.pixelsHigh
bitsPerSample:global.thumbnailInfo.bitsPerSample
samplesPerPixel:global.thumbnailInfo.samplesPerPixel
hasAlpha:global.thumbnailInfo.hasAlpha
isPlanar:global.thumbnailInfo.isPlanar
colorSpaceName:[global.bitmap colorSpaceName]
bytesPerRow:global.thumbnailInfo.bytesPerRow
bitsPerPixel:global.thumbnailInfo.bitsPerPixel];
thumbnail = [[NSImage alloc] init];
[thumbnail addRepresentation:thumbBitmap];
thumbSharpenedNail = [[NSImage alloc] init];
[thumbSharpenedNail addRepresentation:thumbSharpenedBitmap];
[thumbBitmap release];
[thumbSharpenedBitmap release];
[thumbnail lockFocus];
[thumbSharpenedNail compositeToPoint:NSZeroPoint operation:NSCompositeSourceOver];
[thumbnail unlockFocus];
[thumbSharpenedNail release];
} else
thumbnail = [global.image retain];
[thumbView addColumn];
cell = (NSButtonCell *)[[thumbView cells] lastObject];
[cell setImage:thumbnail];
[thumbnail release];
[cell setRepresentedObject:global.filename];
[globalLock unlockWithCondition:NO_DATA];
[thumbView sizeToCells];
[thumbView scrollCellToVisibleAtRow:0 column:[[thumbView cells] count] - 1];
[window display];
numberOfThumbnails++;
currentJob++;
if(isRunning && currentJob < [filenames count])
[thumber thumbWithSize:THUMBSIZE];
else {
job = NO;
isRunning = NO;
[statusTextField setStringValue:@""];
[window display];
[NSApp updateWindows];
}
}
- (BOOL)isRunning
{
return isRunning;
}
- (void)setThumber:(id)aThumber
{
if(thumber == aThumber)
return;
[thumber release];
thumber = [aThumber retain];
[thumber setProtocolForProxy:@protocol(ThumberMethods)];
}
- (void)stopThumbs:(id)sender
{
isRunning = NO;
}
- (void)addDirectory:(id)sender
{
NSWindow *window = [thumbView window];
NSString *filename, *dirname;
NSFileManager *fm = [NSFileManager defaultManager];
NSDirectoryEnumerator *dir;
NSArray *imageFileTypes = [imageDecoder imageUnfilteredFileTypes];
NSMutableArray *fn;
[openPanel setAllowsMultipleSelection:NO];
[openPanel setCanChooseDirectories:YES];
[openPanel setCanChooseFiles:NO];
[openPanel setTitle:@"Add Images from"];
if([openPanel runModal]){
[statusTextField setStringValue:@"Making thumbnails"];
[thumbView deselectAllCells];
[window display];
[window flushWindow];
PSWait();
dirname = [[openPanel filenames] objectAtIndex:0];
dir = [fm enumeratorAtPath:dirname];
if(filenames)
[filenames release];
fn = [[NSMutableArray alloc] init];
while(filename = [dir nextObject]){
if([imageFileTypes containsObject:[filename pathExtension]]){
filename = [NSString stringWithFormat:@"%@/%@",dirname,filename];
[fn addObject:filename];
}
}
filenames = [fn copy];
[fn release];
isRunning = YES;
job = YES;
currentJob = 0;
[thumber thumbWithSize:THUMBSIZE];
}
}
- (void)addImages:(id)sender
{
NSWindow *window = [thumbView window];
[openPanel setAllowsMultipleSelection:YES];
[openPanel setCanChooseDirectories:NO];
[openPanel setCanChooseFiles:YES];
[openPanel setTitle:@"Add Images"];
if([openPanel runModalForTypes:[imageDecoder imageUnfilteredFileTypes]]){
[statusTextField setStringValue:@"Making thumbnails"];
[thumbView deselectAllCells];
[window display];
[window flushWindow];
PSWait();
if(filenames)
[filenames release];
filenames = [[openPanel filenames] copy];
isRunning = YES;
job = YES;
currentJob = 0;
[thumber thumbWithSize:THUMBSIZE];
}
}
- (void)showImage:(id)sender
{
NSImage *image;
NSBitmapImageRep *bitmap;
NSButtonCell *cell;
NSString *filename;
cell = (NSButtonCell *)[thumbView selectedCell];
filename = (NSString *)[cell representedObject];
if(!filename){
NSBeep();
return;
}
bitmap = [imageDecoder decodeFromFile:filename];
if(bitmap){
image = [[NSImage alloc] init];
[image addRepresentation:bitmap];
} else {
NSBeep();
return;
}
[statusTextField setStringValue:filename];
[imageView setImage:image];
[imageView setImageName:filename];
[[imageView window] display];
[image release];
}
static BOOL alreadyAwake = NO;
- (void)awakeFromNib
{
NSScrollView *ms;
NSSize msSize;
if(alreadyAwake)
return;
alreadyAwake = YES;
numberOfThumbnails = 0;
[thumbView setAllowsEmptySelection:YES];
[thumbView removeRow:0];
[thumbView renewRows:1 columns:0];
[thumbView setCellSize:NSMakeSize(THUMBSIZE + 5, THUMBSIZE + 5)];
[thumbView setAutoresizingMask:0];
[thumbView sizeToCells];
[thumbView setDoubleAction:@selector(showImage:)];
[thumbView setTarget:self];
[thumbView setPostsFrameChangedNotifications:YES];
[thumbView deselectAllCells];
ms = [thumbView enclosingScrollView];
msSize = [NSScrollView frameSizeForContentSize:[thumbView frame].size
hasHorizontalScroller:[ms hasHorizontalScroller]
hasVerticalScroller:[ms hasVerticalScroller]
borderType:[ms borderType]];
[ms setFrameSize:msSize];
[self splitView:splitView resizeSubviewsWithOldSize:[splitView frame].size];
savePanel = [NSSavePanel savePanel];
openPanel = [NSOpenPanel openPanel];
}
- (void)applicationDidFinishLaunching:(NSNotification *)notification
{
NSPort *port1;
NSPort *port2;
NSArray *portArray;
isRunning = NO;
job = NO;
filenames = nil;
currentJob = 0;
global.full = NO;
globalLock = [[NSConditionLock alloc] initWithCondition:NO_DATA];
imageDecoder = [[MiscImageDecoderController alloc] init];
port1 = [NSPort port];
port2 = [NSPort port];
serverConnection = [[NSConnection alloc] initWithReceivePort:port1 sendPort:port2];
[serverConnection setRootObject:self];
portArray = [NSArray arrayWithObjects:port2, port1, nil];
[NSThread detachNewThreadSelector:@selector(connectWithPorts:)
toTarget:[Thumber class] withObject:portArray];
[[imageView window] makeKeyAndOrderFront:self];
}
- (BOOL)application:(NSApplication *)theApplication openFile:(NSString *)filename
{
NSString *extension;
extension = [filename pathExtension];
if([extension isEqualToString:@"giger"]){
return [self _internalOpenGiger:filename];
} else if([extension isEqualToString:@"gigernail"]){
return [self _internalOpenGigernail:filename];
} else if([extension isEqualToString:@"gigerwww"]){
return [self _internalOpenGigerwww:filename];
}
return NO;
}
- (void)saveDocument:(id)sender
{
NSString *filename;
NSString *extension;
if(!numberOfThumbnails)
return;
[savePanel setRequiredFileType:@"giger"];
[savePanel setTitle:@"Save"];
[savePanel setAccessoryView:saveFileTypeMatrix];
if([savePanel runModal]){
filename = [savePanel filename];
extension = [filename pathExtension];
if([extension isEqualToString:@"giger"])
[self _internalSaveGiger:filename];
else if([extension isEqualToString:@"gigernail"])
[self _internalSaveGigernail:filename];
else
[self _internalSaveGigerwww:filename];
}
}
- (void)saveChangeFileType:(id)sender
{
int tag = [sender selectedTag];
if(tag == 0)
[savePanel setRequiredFileType:@"giger"];
else if(tag == 1)
[savePanel setRequiredFileType:@"gigernail"];
else
[savePanel setRequiredFileType:@"gigerwww"];
}
- (void)openDocument:(id)sender
{
NSString *filename;
NSString *extension;
[openPanel setAllowsMultipleSelection:NO];
[openPanel setCanChooseDirectories:NO];
[openPanel setCanChooseFiles:YES];
[openPanel setTitle:@"Open"];
if([openPanel runModalForTypes:[NSArray arrayWithObjects:@"giger",@"gigernail",@"gigerwww",nil]]){
filename = [[openPanel filenames] objectAtIndex:0];
extension = [filename pathExtension];
if([extension isEqualToString:@"giger"])
[self _internalOpenGiger:filename];
else if([extension isEqualToString:@"gigernail"])
[self _internalOpenGigernail:filename];
else
[self _internalOpenGigerwww:filename];
}
}
- (void)showInfo:(id)sender
{
if(!infoPanel){
[NSBundle loadNibNamed:@"Info" owner:self];
[infoPanel setBecomesKeyOnlyIfNeeded:YES];
}
[infoPanel orderFront:self];
}
- (void)saveImage:(id)sender
{
NSString *filename;
NSData *tiffData;
if(![imageView image])
return;
[savePanel setRequiredFileType:@"tiff"];
[savePanel setTitle:@"Save Image"];
if([savePanel runModal]){
filename = [savePanel filename];
tiffData = [[imageView image] TIFFRepresentation];
if(![tiffData writeToFile:filename atomically:YES])
NSBeep();
}
}
- (void)openImage:(id)sender
{
NSString *filename;
NSImage *image;
NSBitmapImageRep *bitmap;
[openPanel setAllowsMultipleSelection:NO];
[openPanel setCanChooseDirectories:NO];
[openPanel setCanChooseFiles:YES];
[openPanel setTitle:@"Open Image"];
if([openPanel runModalForTypes:[imageDecoder imageUnfilteredFileTypes]]){
filename = [[openPanel filenames] objectAtIndex:0];
bitmap = [imageDecoder decodeFromFile:filename];
if(bitmap){
image = [[NSImage alloc] init];
[image addRepresentation:bitmap];
} else {
NSBeep();
return;
}
[statusTextField setStringValue:filename];
[imageView setImage:image];
[imageView setImageName:filename];
[[imageView window] display];
[image release];
}
}
- (void)remove:(id)sender
{
NSArray *selectedCells;
NSMutableArray *cells;
selectedCells = [thumbView selectedCells];
if([selectedCells count] == 0)
return;
cells = [[thumbView cells] mutableCopy];
[cells removeObjectsInArray:selectedCells];
[thumbView removeRow:0];
numberOfThumbnails = [cells count];
if(numberOfThumbnails == 0){
[thumbView renewRows:1 columns:0];
} else {
[thumbView renewRows:0 columns:[cells count]];
[thumbView addRowWithCells:cells];
}
[thumbView sizeToCells];
[[thumbView window] display];
}
- (void)newDocument:(id)sender;
{
if(numberOfThumbnails == 0)
return;
numberOfThumbnails = 0;
[thumbView removeRow:0];
[thumbView renewRows:1 columns:0];
[thumbView sizeToCells];
[[thumbView window] display];
}
- (void)splitView:(NSSplitView *)sender resizeSubviewsWithOldSize:(NSSize)oldSize
{
NSRect upper,lower;
NSView *upperView;
NSView *lowerView;
float delta, thumbHeight;
upperView = (NSView *)[[sender subviews] objectAtIndex:0];
lowerView = (NSView *)[[sender subviews] objectAtIndex:1];
upper = [upperView frame];
thumbHeight = upper.size.height;
[sender adjustSubviews];
upper = [upperView frame];
lower = [lowerView frame];
if(upper.size.height > thumbHeight){
delta = upper.size.height - thumbHeight;
upper.size.height = thumbHeight;
lower.size.height += delta;
[upperView setFrame:upper];
[lowerView setFrame:lower];
} else if (upper.size.height < thumbHeight){
delta = thumbHeight - upper.size.height;
upper.size.height = thumbHeight;
lower.size.height -= delta;
if(lower.size.height < 0)
lower.size.height = 0;
[upperView setFrame:upper];
[lowerView setFrame:lower];
}
}
- (void)splitView:(NSSplitView *)sender constrainMinCoordinate:(float *)min
maxCoordinate:(float *)max ofSubviewAt:(int)offset
{
NSScrollView *ms;
NSSize msSize;
if(offset == 0){
ms = [thumbView enclosingScrollView];
msSize = [NSScrollView frameSizeForContentSize:[thumbView frame].size
hasHorizontalScroller:[ms hasHorizontalScroller]
hasVerticalScroller:[ms hasVerticalScroller]
borderType:[ms borderType]];
*min = 0.0;
*max = msSize.height;
}
}
- (BOOL)validateMenuItem:(NSMenuItem *)anItem
{
NSString *menuTitle = [anItem title];
if([menuTitle isEqualToString:@"New"]){
if(isRunning || numberOfThumbnails == 0)
return NO;
else
return YES;
}
if([menuTitle isEqualToString:@"Open..."]){
if(isRunning)
return NO;
else
return YES;
}
if([menuTitle isEqualToString:@"Save..."]){
if(isRunning || numberOfThumbnails == 0)
return NO;
else
return YES;
}
if([menuTitle isEqualToString:@"Add Images..."]){
if(isRunning)
return NO;
else
return YES;
}
if([menuTitle isEqualToString:@"Add Directory..."]){
if(isRunning)
return NO;
else
return YES;
}
if([menuTitle isEqualToString:@"Remove"]){
if(isRunning || [[thumbView selectedCells] count] == 0)
return NO;
else
return YES;
}
if([menuTitle isEqualToString:@"Stop Thumbnails"]){
if(!isRunning)
return NO;
else
return YES;
}
if([menuTitle isEqualToString:@"Save Image..."]){
if(![imageView image])
return NO;
else
return YES;
}
return YES;
}
@end
These are the contents of the former NiCE NeXT User Group NeXTSTEP/OpenStep software archive, currently hosted by Netfuture.ch.