// BrowserPane.m
//
// Free software created 1 Feb 1992
// by Paul Burchard <burchard@math.utah.edu>.

#import "BrowserPane.h"
#import <objc/Storage.h>
#import <appkit/appkit.h>
#import <string.h>
#import <sys/types.h>
#import <sys/stat.h>
#import <sys/file.h>

@implementation BrowserPane


// Init, freeing, archiving.

- initFrame:(const NXRect *)frameRect
{
    return [self initFrame:frameRect cellClass:[TextFieldCell class]];
}

- initFrame:(const NXRect *)frameRect cellClass:factoryId
{
    id matrix;
    NXRect subframe;
    NXSize cellsize;
    
    [super initFrame:frameRect];
    [self setBorderType:NX_BEZEL];
    [self setBackgroundGray:NX_LTGRAY];
    [self setVertScrollerRequired:YES];
    [self setAutoresizeSubviews:YES];
    action = @selector(takeStringValueFrom:);
    doubleAction = @selector(takeStringValueFrom:);
    stringValue = [[Storage alloc] initCount:0 elementSize:sizeof(char) description:"c"];
    stringList = [[List alloc] initCount:0];
    separator = '\t';
    
    // Create Matrix of full-width Cells of class factoryId,
    // and place in our scrollView as its docView.
    [contentView getFrame:&subframe];
    matrix = [[Matrix alloc] initFrame:&subframe mode:NX_LISTMODE cellClass:factoryId numRows:0 numCols:1];
    if(!matrix) return nil;
    cellsize.width = cellsize.height = 0.0;
    [matrix setIntercell:&cellsize];
    [matrix getCellSize:&cellsize];
    cellsize.width = NX_WIDTH(&subframe);
    [matrix setCellSize:&cellsize];
    [matrix setAutosizeCells:YES];
    [matrix setAutoscroll:YES];
    [[matrix setAction:@selector(sendAction)] setTarget:self];
    [matrix setDoubleAction:@selector(sendDoubleAction)];
    [self setDocView:matrix];
    [matrix sizeToCells];
    [self setAutodisplay:YES];
    [self setNeedsDisplay:YES];
    
    return self;
}

- resizeSubviews:(const NXSize *)oldSize
{
    NXRect contentFrame, docFrame;

    [super resizeSubviews:oldSize];
    
    // Keep matrix at full width of contentView when size changes.
    [contentView getFrame:&contentFrame];
    [contentView convertRect:&contentFrame toView:contentView];//paranoia
    [[self docView] getFrame:&docFrame];
    [[self docView] sizeTo:NX_WIDTH(&contentFrame) :NX_HEIGHT(&docFrame)];
    return self;
}

- read:(NXTypedStream *)stream
{
    [super read:stream];
    stringValue = NXReadObject(stream);
    stringList = NXReadObject(stream);
    delegate = NXReadObject(stream);
    target = NXReadObject(stream);
    // Assumes BOOL is size of char.
    NXReadTypes(stream, "::ccccc", &action, &doubleAction,
    	&isAlphabetized, &isAbbreviated, &isEditable, &isDisabledOnEntry,
	&separator);
    return self;
}

- write:(NXTypedStream *)stream
{
    [super write:stream];
    NXWriteObject(stream, stringValue);
    NXWriteObject(stream, stringList);
    NXWriteObjectReference(stream, delegate);
    NXWriteObjectReference(stream, target);
    // Assumes BOOL is size of char.
    NXWriteTypes(stream, "::ccccc", &action, &doubleAction,
    	&isAlphabetized, &isAbbreviated, &isEditable, &isDisabledOnEntry,
	&separator);
    return self;
}

- free
{
    [stringValue free];
    [[stringList freeObjects] free];
    return [super free];
}


// Adding and removing entries.

// Internal use only.
- (int)indexAddEntryStorage:stringStorage
{
    int row, nrows, ncols, len;
    id matrix;
    char *newName, *abbrevName = 0;
    const char *name;
    
    // Don't enter again if already in here.
    if(!stringStorage) return (-1);
    if(!(newName = [stringStorage elementAt:0])) return (-1);
    if((row=[self indexOfEntry:newName]) >= 0)
    	{ [stringStorage free]; return row; }
	
    // Get abbreviation if necessary.
    if(isAbbreviated)
    {
	len = strlen(newName);
	[stringStorage setNumSlots:2*(len+1)];
	abbrevName = (char *)[stringStorage elementAt:(len+1)];
	newName = (char *)[stringStorage elementAt:0];
	if(!abbrevName || !newName) return (-1);
	*abbrevName = 0;
	[self abbreviate:newName to:abbrevName];
    }
    
    // Find slot for new entry (sort by abbreviated forms).
    matrix = [self docView];
    [matrix getNumRows:&nrows numCols:&ncols];
    if(isAlphabetized) for(row=0; row<nrows; row++)
    {
    	name = [[matrix cellAt:row :0] stringValue];
	if(isAbbreviated && [self isFirst:abbrevName second:name]) break;
	if(!isAbbreviated && [self isFirst:newName second:name]) break;
    }
    else row = nrows;
    
    // Insert new entry into matrix and string list, disabling if req'd.
    [matrix insertRowAt:row];
    [matrix sizeToCells];
    [[matrix cellAt:row :0]
    	setStringValue:(isAbbreviated ? abbrevName : newName)];
    [stringList insertObject:stringStorage at:row];
    if(isDisabledOnEntry) [self setEntryEnabled:NO at:row];
    return row;    
}

- (int)indexAddEntry:(const char *)aString
{
    int row, len;
    id stringStorage;
    
    if(!aString) return (-1);
    len = strlen(aString);
    stringStorage = [[Storage alloc] initCount:(len+1) elementSize:sizeof(char) description:"c"];
    [stringStorage setNumSlots:(len+1)];
    strcpy((char *)[stringStorage elementAt:0], aString);
    row = [self indexAddEntryStorage:stringStorage];
    if(row < 0) { [stringStorage free]; return (-1); }
    return row;
}

- addEntry:(const char *)aString
{
    if([self indexAddEntry:aString] < 0) return nil;
    else return self;
}

- (const char *)entryAt:(int)row
{
    return (const char *)[[stringList objectAt:row] elementAt:0];
}

- cellAt:(int)row
{
    return [[self docView] cellAt:row :0];
}

- (int)indexOfEntry:(const char *)aString
{
    int row, nrows, ncols;
    id matrix;
    
    if(!aString) return (-1);
    matrix = [self docView];
    [matrix getNumRows:&nrows numCols:&ncols];
    for(row=0; row<nrows; row++)
	if(strcmp([self entryAt:row], aString) == 0) break;
    if(row >= nrows) return (-1);
    return row;
}

- (unsigned)count
{
    int nrows = (-1), ncols = (-1);
    
    [[self docView] getNumRows:&nrows numCols:&ncols];
    if(nrows < 0) return 0;
    else return nrows;
}

- removeEntryAt:(int)row
{
    int nrows, ncols;
    id matrix;
    
    // Removing a selected entry clears the selection.
    matrix = [self docView];
    if([self isEntrySelectedAt:row]) [self clearSelection];

    // Remove entry and resize Matrix.
    [matrix getNumRows:&nrows numCols:&ncols];
    if(row<0 || row>=nrows) return nil;
    [matrix removeRowAt:row andFree:YES];
    [matrix sizeToCells];
    [[stringList removeObjectAt:row] free];
    return self;
}

- removeSelection
{
    int row, nrows, ncols;
    id matrix;
    
    // Remove selected entries (starting from last) and resize Matrix.
    matrix = [self docView];
    [matrix getNumRows:&nrows numCols:&ncols];
    for(row=nrows-1; row>=0; row--) if([self isEntrySelectedAt:row])
    {
	[matrix removeRowAt:row andFree:YES];
	[[stringList removeObjectAt:row] free];
    }
    [matrix sizeToCells];
    [self clearSelection];
    return self;
}

- clear
{
    int row, nrows, ncols;
    id matrix;
    
    [self clearSelection];
    matrix = [self docView];
    [matrix getNumRows:&nrows numCols:&ncols];
    for(row=nrows-1; row>=0; row--)
    {
    	[matrix removeRowAt:row andFree:YES];
	[[stringList removeObjectAt:row] free];
    }
    [matrix sizeToCells];
    return self;
}

- addFiles:(const char *)dir suffix:(const char *)sfx;
{
    char command[2048], *fgets(), *name;
    FILE *pipe;
    struct stat statbuf;
	
    sprintf(command, "/bin/ls -1d 2>/dev/null %s/*", dir);
    if(sfx != NULL) { strcat(command, "."); strcat(command, sfx); }
    pipe = popen(command, "r");
    while(fgets(command, 2048, pipe) != NULL)
    {
	command[strlen (command) - 1] = '\0';
	stat(command, &statbuf);
	if((name = strrchr(command, '/')) != NULL) name++;
	else name = command;
	if(!(statbuf.st_mode & S_IFDIR)) [self addEntry:name];
    }
    pclose(pipe);
    return self;
}


// Adding and selecting entries via stringValue.

- (const char *)grabStringFrom:sender
{
    if([sender respondsTo:@selector(stringValue)])
        return [sender stringValue];
    else if([sender respondsTo:@selector(stringValueAt:)])
        return [sender stringValueAt:0];
    else return (const char *)0;
}

- (const char *)stringValue
{
    if([stringValue count] <= 0) return 0;
    return (const char *)[stringValue elementAt:0];
}

- setStringValue:(const char *)aString
{
    return [self setStringValue:aString append:NO];
}

- setStringValue:(const char *)aString append:(BOOL)yn
{
    int row, len;
    const char *nextString;
    char *endOfEntry;
    id nextEntry;

    // Clear selection first if not appending to stringValue.
    if(!yn || !aString) [self clearSelection];
    if(!aString) return self;
    
    // Add TAB-separated items to selection.
    // If not already in matrix and list, add them.
    for(nextString=aString; nextString;
    	nextString=strchr(nextString, separator))
    {
    	// Create Storage string to hold unabbrev next entry.
    	if(*nextString == separator) nextString++;
	len = strlen(nextString);
	nextEntry = [[Storage alloc] initCount:(len+1) elementSize:sizeof(char) description:"c"];
	[nextEntry setNumSlots:(len+1)];
	strcpy((char *)[nextEntry elementAt:0], nextString);
	endOfEntry = strchr((char *)[nextEntry elementAt:0], separator);
	if(endOfEntry) *endOfEntry = 0;
	
	// Add to matrix and list if necessary, disabled if req'd.
	row = [self indexAddEntryStorage:nextEntry];
	if(row < 0) { [nextEntry free]; return nil; }
	if(isDisabledOnEntry) [self setEntryEnabled:NO at:row];
	
	// Add entry to selection.
	if(!isDisabledOnEntry) [self selectEntryAt:row append:YES];
    }
    [self update];
    return self;
}

- takeStringValueFrom:sender
{
    id oldTarget = nil;
    id rtn;
    
    // If sender is target, don't send action (to avoid circularity).
    if(sender == target) { oldTarget = target; [self setTarget:nil]; }
    rtn = [self setStringValue:[self grabStringFrom:sender] append:NO];
    if(oldTarget) [self setTarget:oldTarget];
    
    // Notify delegate of user-induced change in list.
    // (Actually, due to non-duplication no change may have taken place.)
    if([delegate respondsTo:@selector(textDidChange:)])
    	[delegate textDidChange:self];
    return rtn;
}

- appendStringValueFrom:sender
{
    id oldTarget = nil;
    id rtn;
    
    // If sender is target, don't send action (to avoid circularity).
    if(sender == target) { oldTarget = target; [self setTarget:nil]; }
    rtn = [self setStringValue:[self grabStringFrom:sender] append:YES];
    if(oldTarget) [self setTarget:oldTarget];
    
    // Notify delegate of user-induced change in list.
    // (Actually, due to non-duplication no change may have taken place.)
    if([delegate respondsTo:@selector(textDidChange:)])
    	[delegate textDidChange:self];
    return rtn;
}

- (char)separator
{
    return separator;
}

- setSeparator:(char)c
{
    if(!c) return nil;
    separator = c;
    return self;
}


// Selecting.

- selectEntryAt:(int)row append:(BOOL)yn
{
    int nrows, ncols, oldlen;
    const char *entryString;
    id matrix;
    
    matrix = [self docView];
    [matrix getNumRows:&nrows numCols:&ncols];
    if(row<0 || row>=nrows) return nil;
    if(![self isEntryEnabledAt:row]) return nil;
    entryString = (char *)[[stringList objectAt:row] elementAt:0];
    
    if(yn)
    {
    	// Append to selection.
	// Selection indicated by cell state and highlighting.
	[matrix lockFocus];
	[matrix highlightCellAt:row :0 lit:YES];
	[matrix setState:1 at:row :0];
	[matrix unlockFocus];
	if([stringValue count] <= 0) oldlen = 0;
	else oldlen = strlen((char *)[stringValue elementAt:0]);
	[stringValue setNumSlots:(oldlen + 1 + strlen(entryString) + 1)];
	sprintf((char *)[stringValue elementAt:0] + oldlen, 
	    (oldlen>0 ? "\t%s" : "%s"), entryString);
    }
    else
    {
    	// Create new selection.
	// Selection indicated by cell state and highlighting.
	[self clearSelection];
	[matrix lockFocus];
	[matrix highlightCellAt:row :0 lit:YES];
	[matrix setState:1 at:row :0];
	[matrix unlockFocus];
	[stringValue setNumSlots:(strlen(entryString)+1)];
	strcpy((char *)[stringValue elementAt:0], entryString);
    }
    
    // Notify target of new selection.
    if(action) [target perform:action with:self];
    return self;
}

- (BOOL)isEntrySelectedAt:(int)row
{
    int nrows, ncols;
    id matrix;
    
    // Selection indicated by cell state and highlighting.
    // Make sure state is consistent with highlighting and enabling.
    matrix = [self docView];
    [matrix getNumRows:&nrows numCols:&ncols];
    if(row<0 || row>=nrows) return NO;
    if(![self isEntryEnabledAt:row] || ![[matrix cellAt:row :0] isHighlighted])
    {
	[matrix setState:0 at:row :0];
	return NO;
    }
    [matrix setState:1 at:row :0];
    return YES;
}

- clearSelection
{
    int row, nrows, ncols;
    id matrix;
    
    matrix = [self docView];
    [matrix selectCellAt:(-1) :(-1)];
    [matrix getNumRows:&nrows numCols:&ncols];
    [matrix lockFocus];
    for(row=0; row<nrows; row++)
    {
    	// Selection indicated by cell state and highlighting.
    	[matrix highlightCellAt:row :0 lit:NO];
	[matrix setState:0 at:row :0];
    }
    [matrix unlockFocus];
    [stringValue setNumSlots:0];
    if(action) [target perform:action with:self];
    return self;
}


// Target and action.

// For internal use only.
- (BOOL)appendSelStringFrom:sender
{
    int row, oldlen;
    const char *nextString;
    
    // Find (enabled) cell and its unabbreviated stringValue.
    if((row=[[[self docView] cellList] indexOf:sender]) == NX_NOT_IN_LIST)
    	return NO;
    if(![self isEntryEnabledAt:row]) return YES; // continue looping thru cells
    nextString = (char *)[[stringList objectAt:row] elementAt:0];
	
    // Selection indicated by cell state and highlighting.
    // Make sure state is consistent with highlighting.
    if(![sender isHighlighted])
    {
	[sender setState:0];
	return YES; // continue looping thru cells
    }
    [sender setState:1];

    // Append sender's unabbrev stringValue onto ours (with TAB separation).
    if([stringValue count] <= 0) oldlen = 0;
    else oldlen = strlen((char *)[stringValue elementAt:0]);
    [stringValue setNumSlots:(oldlen + 1 + strlen(nextString) + 1)];
    sprintf((char *)[stringValue elementAt:0] + oldlen, 
	(oldlen>0 ? "\t%s" : "%s"), nextString);
    return YES; // continue looping thru cells
}

// For internal use only.
- recomputeStringValueFromSelection
{
    [stringValue setNumSlots:0];
    [[self docView] sendAction:@selector(appendSelStringFrom:) to:self forAllCells:NO];
    return self;
}

- sendAction
{
    // Sent by newly selected cells in matrix.
    // So recompute stringValue and make self firstResponder.
    [self recomputeStringValueFromSelection];
    [window makeFirstResponder:self];
    if(!action) return self;
    return [target perform:action with:self];
}

- sendDoubleAction
{
    char *fileName, *nxt;
    int ok;
    
    // Sent by newly selected cell in matrix.
    // So recompute stringValue and make self firstResponder.
    [self recomputeStringValueFromSelection];
    [window makeFirstResponder:self];
    
    // Double-click means open files (if delegate exists and opens files).
    // Open files one by one (there may be many if msg sent by outside obj).
    if([stringValue count]>0 && [delegate respondsTo:@selector(openFile:ok:)])
    {
	fileName = (char *)[stringValue elementAt:0];
	nxt = strchr(fileName, separator);
	while(fileName)
	{
    	    if(nxt) *nxt = 0;
	    [delegate openFile:fileName ok:&ok];
	    if(nxt)
	    {
		*nxt = separator;
	    	fileName = nxt+1;
		nxt = strchr(fileName, separator);
	    }
	    else fileName = nxt = 0;
	}
    }
    
    // Send doubleAction to target.
    if(!doubleAction) return self;
    return [target perform:doubleAction with:self];
}

- setTarget:anObject
{
    target = anObject;
    [[self docView] setTarget:self];
    return self;
}

- setAction:(SEL)aSelector
{
    action = aSelector;
    [[self docView] setAction:@selector(sendAction)];
    return self;
}

- setDoubleAction:(SEL)aSelector
{
    doubleAction = aSelector;
    [[self docView] setDoubleAction:@selector(sendDoubleAction)];
    return self;
}

- target
{
    return target;
}

- (SEL)action
{
    return action;
}

- (SEL)doubleAction
{
    return doubleAction;
}


// Editing.

- setEditable:(BOOL)yn
{
    isEditable = yn;
    return self;
}

- (BOOL)isEditable
{
    return isEditable;
}

- (BOOL)acceptsFirstResponder
{
    return YES;
}

- becomeFirstResponder
{
    [self setBackgroundGray:NX_WHITE];
    [self update];
    return self;
}

- resignFirstResponder
{
    [self setBackgroundGray:NX_LTGRAY];
    [self update];
    return self;
}

- keyDown:(NXEvent *)theEvent
{
    if(theEvent->type!=NX_KEYDOWN) return self;
    if(!isEditable) return nil;
    switch(theEvent->data.key.charCode)
    {
    case NX_DELETE:
    	[self delete:self];
	break;
    default:
    	return nil;
    }    	
    return self;
}

- mouseDown:(NXEvent *)theEvent
{
    // In case matrix is empty or does not fill view, capture mouse clicks.
    [super mouseDown:theEvent];
    if([window firstResponder] != self) [window makeFirstResponder:self];
    return self;
}

- delete:sender
{
    char *fileName, *nxt;
    int ok;
    
    if(!isEditable) return self;

    // Let delegate delete selected "files" if it can.
    if([stringValue count]>0
    	&& [delegate respondsTo:@selector(removeFile:ok:)])
    {
	fileName = (char *)[stringValue elementAt:0];
	nxt = strchr(fileName, separator);
	while(fileName)
	{
    	    if(nxt) *nxt = 0;
	    [delegate removeFile:fileName ok:&ok];
	    if(nxt)
	    {
		*nxt = separator;
	    	fileName = nxt+1;
		nxt = strchr(fileName, separator);
	    }
	    else fileName = nxt = 0;
	}
    }

    // Now remove selected names from list.
    [self removeSelection];
    [self update];
    
    // Notify delegate of user-induced change in list.
    if([delegate respondsTo:@selector(textDidChange:)])
    	[delegate textDidChange:self];
    return self;
}

- cut:sender
{
    const char *sval;
    
    if(!isEditable) return self;
    
    // Copy out string value...
    if(!(sval = [self stringValue])) return self;
    [[Pasteboard new] declareTypes:&NXAsciiPboard num:1 owner:self];
    [[Pasteboard new] writeType:NXAsciiPboard data:sval length:strlen(sval)];
    
    // Delete selection (and associated "files" if any).
    return [self delete:sender];
}

- copy:sender
{
    char *fileName, *nxt;
    int ok;
    const char *sval;
    
    if(!(sval = [self stringValue])) return self;

    // Let delegate "prepare" selected "files" if it can.
    if([delegate respondsTo:@selector(prepFile:ok:)])
    {
	fileName = sval;
	nxt = strchr(fileName, separator);
	while(fileName)
	{
    	    if(nxt) *nxt = 0;
	    [delegate prepFile:fileName ok:&ok];
	    if(nxt)
	    {
		*nxt = separator;
	    	fileName = nxt+1;
		nxt = strchr(fileName, separator);
	    }
	    else fileName = nxt = 0;
	}
    }

    // Copy out string value.
    [[Pasteboard new] declareTypes:&NXAsciiPboard num:1 owner:self];
    [[Pasteboard new] writeType:NXAsciiPboard data:sval length:strlen(sval)];
    [self update];
    return self;
}

- paste:sender
{
    char *sval;
    int length;
    
    if(!isEditable) return self;
    [[Pasteboard new] readType:NXAsciiPboard data:&sval length:&length];
    [self setStringValue:sval]; //!!! is the data null-terminated?
    vm_deallocate(task_self(), (vm_address_t)sval, sizeof(char)*length);
    [self update];

    // Notify delegate of user-induced change in list.
    if([delegate respondsTo:@selector(textDidChange:)])
    	[delegate textDidChange:self];
    return self;
}

- selectAll:sender
{
    int row, nrows, ncols;
    id matrix;
    BOOL autodisplay;
    
    //!!! Hideous in action (autoDisplay isn't getting shut off?).
    matrix = [self docView];
    autodisplay = [matrix isAutodisplay];
    [matrix setAutodisplay:NO];
    [matrix getNumRows:&nrows numCols:&ncols];
    for(row=0; row<nrows; row++) [self selectEntryAt:row append:YES];
    [matrix setAutodisplay:autodisplay];
    [self update];
    return self;
}

- (int)openFile:(const char *)fileName ok:(int *)flag
{
    // Default does nothing.
    return 0;
}

- (int)prepFile:(const char *)fileName ok:(int *)flag
{
    // Default does nothing.
    return 0;
}

- (int)removeFile:(const char *)fileName ok:(int *)flag
{
    // Default does nothing.
    return 0;
}

- textDidChange:sender
{
    return self;
}


// String display control.

- setEntryEnabled:(BOOL)yn at:(int)row
{
    id matrix, cell;
    
    // Disabling a selected entry clears the selection.
    if(!yn && [self isEntrySelectedAt:row]) [self clearSelection];
    matrix = [self docView]; cell = [matrix cellAt:row :0];
    [cell setEnabled:yn];
    //!!! Note: textGray seems to be relative to the background??
    if(yn) [matrix drawCell:[cell setTextGray:NX_BLACK]];
    else [matrix drawCell:[cell setTextGray:NX_LTGRAY]];
    return self;
}

- (BOOL)isEntryEnabledAt:(int)row
{
    return [[[self docView] cellAt:row :0] isEnabled];
}

- setDisabledOnEntry:(BOOL)yn
{
    isDisabledOnEntry = yn;
    return self;
}

- (BOOL)isDisabledOnEntry
{
    return isDisabledOnEntry;
}

- setAlphabetized:(BOOL)yn
{
    //!!! Does not retroactively sort yet! Sorry!
    isAlphabetized = yn;
    return self;
}

- (BOOL)isAlphabetized
{
    return isAlphabetized;
}

- (BOOL)isFirst:(const char *)firstString second:(const char *)secondString
{
    // Standard NeXTstep string comparison.
    if(!secondString || !firstString) return NO;
    if(NXOrderStrings(secondString, firstString, NO, -1, NULL) > 0)
    	return YES;
    else return NO;
}

- setAbbreviated:(BOOL)yn
{
    int len, row, nrows, ncols;
    char *fullName, *abbrevName;
    id matrix, stringStorage;
    
    isAbbreviated = yn;
    matrix = [self docView];
    [matrix getNumRows:&nrows numCols:&ncols];
    for(row=0; row<nrows; row++)
    {
    	stringStorage = [stringList objectAt:row];
    	if(!(fullName = (char *)[stringStorage elementAt:0])) continue;
	len = strlen(fullName);
	[stringStorage setNumSlots:2*(len+1)];
	abbrevName = (char *)[stringStorage elementAt:(len+1)];
	fullName = (char *)[stringStorage elementAt:0];
	if(!abbrevName || !fullName) continue;
	*abbrevName = 0;
	[self abbreviate:fullName to:abbrevName];
	[[matrix cellAt:row :0] setStringValue:abbrevName];
    }
    [self update];
    return self;
}

- (BOOL)isAbbreviated
{
    return isAbbreviated;
}

- abbreviate:(const char *)srcString to:(char *)destString
{
    const char *abbr;
    
    // Show part after last '/'.
    if(!destString) return nil;
    if(!srcString) { *destString = 0; return self; }
    abbr = strrchr(srcString, '/');
    if(!abbr) abbr = srcString;
    else abbr++;
    strcpy(destString, abbr);
    return self;
}

@end



