//=============================================================================
//
//		Copyright (C) 1995-1997 by Paul S. McCarthy and Eric Sunshine.
//				Written by Paul S. McCarthy and Eric Sunshine.
//							All Rights Reserved.
//
//		This notice may not be removed from this source code.
//
//		This object is included in the MiscKit by permission from the authors
//		and its use is governed by the MiscKit license, found in the file
//		"License.rtf" in the MiscKit distribution.	Please refer to that file
//		for a list of all applicable permissions and restrictions.
//
//=============================================================================
//-----------------------------------------------------------------------------
// DirWindow.m
//
//		Manages window which displays directory listing.
//
//-----------------------------------------------------------------------------
//-----------------------------------------------------------------------------
// $Id: DirWindow.m,v 1.4 97/07/08 11:53:33 sunshine Exp $
// $Log:		DirWindow.m,v $
// Revision 1.4	 97/07/08  11:53:33	 sunshine
// v31: Worked around Cell's ClipView cache bug.
// 
// Revision 1.3	 97/06/10  05:14:32	 sunshine
// v30: Synchronized with ScrollDir v29.4 for OPENSTEP.
// Reorganized to mirror structure of LazyScrollDir counterpart.
// Now uses -setTitleAsFilename:.  Fixed bug: Wasn't taking directory
// sticky-bit into account when determining if file could be renamed.
// No longer misleadingly stat()'s file which soft-link points at.
// Ditched Message slot.  Upgraded naming.	Hard-links are now right-aligned.
// 
// Revision 1.2	 97/02/05  08:15:52	 sunshine
// v29: Resync'd with LazyScrollDir (v13).
// Added 'writable' flag which prevents cmd-r from working on read-only
// directories.
//-----------------------------------------------------------------------------
#import "DirWindow.h"
#import "Defaults.h"
#import "NameCache.h"
#import <misckit/MiscExporter.h>
#import <misckit/MiscIntList.h>
#import <misckit/MiscTableScroll.h>
#import <misckit/MiscTableCell.h>
#import <appkit/Application.h>
#import <appkit/Button.h>
#import <appkit/ButtonCell.h>
#import <appkit/NXImage.h>
#import <appkit/Panel.h>
#import <appkit/Text.h>
#import <appkit/TextField.h>
#import <appkit/workspaceRequest.h>
#import <objc/NXBundle.h>
#import <assert.h>
#import <errno.h>
#import <libc.h>				// getwd()
#import <string.h>
#import <unistd.h>				// getcwd()
#import <mach/mach_init.h>		// vm_page_size
#import <sys/dir.h>

#ifdef _POSIX_SOURCE
# define GET_CURR_DIR(_BF_,_SZ_) getcwd( _BF_, _SZ_ )
# define GRP_TYPE gid_t
# define MAX_GRPS NGROUPS_MAX
#else
# define GET_CURR_DIR(_BF_,_SZ_) getwd( _BF_ )
# define GRP_TYPE int
# define MAX_GRPS NGROUPS
#endif

enum
	{
	ICON_SLOT,
	NAME_SLOT,
	LOCK_SLOT,
	SIZE_SLOT,
	MODIFIED_SLOT,
	PERMS_SLOT,
	OWNER_SLOT,
	GROUP_SLOT,
	HARDLINKS_SLOT,
	SOFTLINK_SLOT,

	MAX_SLOT
	};

static int const CASCADE_MAX = 10;
static int CASCADE_COUNTER = 0;
static NXCoord CASCADE_ORIGIN_X;
static NXCoord CASCADE_ORIGIN_Y;
static NXCoord CASCADE_DELTA_X;
static NXCoord CASCADE_DELTA_Y;

static BOOL DEFAULT_AUTO_SORT;
static BOOL DEFAULT_SHOW_HIDDEN;
static BOOL DEFAULT_HIGHLIGHT_DIRS;
static BOOL DEFAULT_DRAG_UNSCALED;
static NXColor DEFAULT_COLOR;
static NXSize DEFAULT_WIN_SIZE;
static Font* DEFAULT_FONT;
static List* OPEN_DIRS = 0;
static NXImage* LOCKED_ICON = 0;
static NXImage* UNLOCKED_ICON = 0;
static OwnerCache* OWNERS = 0;
static GroupCache* GROUPS = 0;

static char const COLOR_DEF[] = "DirColor";
static char const SIZE_DEF[] = "DirSize";
static char const FONT_DEF[] = "DirFont";
static char const SORT_DEF[] = "AutoSort";
static char const HIDDEN_DEF[] = "ShowHidden";
static char const HLIGHT_DEF[] = "HighlightDirs";
static char const UNSCALED_DEF[] = "DragUnscaled";
static char const COL_SIZES_DEF[] = "ColSizes";
static char const COL_ORDER_DEF[] = "ColOrder";

static char const LOCKED_ICON_S[] = "Lock.secure";
static char const UNLOCKED_ICON_S[] = "Lock.insecure";

typedef struct
	{
	char const* shortName;
	char const* longName;
	char softLink[ FILENAME_MAX + 1 ];
	struct stat status;
	BOOL isDirectory;
	BOOL parentWritable;
	BOOL parentSticky;
	uid_t effectiveUid;
	} DirEntry;


//-----------------------------------------------------------------------------
// NOTE: USE_PRIVATE_ZONE
//		When set to '1', this program will place each window in a separate
//		NXZone and destroy the zone when the window is closed.	However,
//		the NEXTSTEP 3.3 Cell class caches ClipViews which it uses for editing
//		scrollable cells.  Unfortunately the caching code is buggy.	 It
//		blindly allocates ClipViews from transient zones such as ours then
//		caches and tries to re-use the ClipViews even after the originating
//		zones are defunct.	Hence the program crashes.	Verification of this
//		bug was obtained via disassembly of relevant portions of the AppKit.
//		The OPENSTEP AppKit does not have this problem.	 Disassembly of the
//		relevant OPENSTEP AppKit code reveals that the NSClipViews are
//		allocated from a zone which is guranteed to stay around for the life
//		of the application, NSDefaultMallocZone().
//-----------------------------------------------------------------------------
#define USE_PRIVATE_ZONE 0

#if USE_PRIVATE_ZONE
# define EMPLOY_ZONE()	NXCreateZone( vm_page_size, vm_page_size, YES )
# define RETIRE_ZONE(Z) NXDestroyZone(Z)
#else
# define EMPLOY_ZONE()	NXDefaultMallocZone()
# define RETIRE_ZONE(Z) (void)(Z)
#endif


//-----------------------------------------------------------------------------
// normalize_path
//-----------------------------------------------------------------------------
static void normalize_path( char* buff, size_t sz )
	{
	char save_dir[ FILENAME_MAX + 1 ];
	char tmp[ FILENAME_MAX + 1 ];

	if (buff != 0 && sz > 0)
		{
		if (GET_CURR_DIR( save_dir, sizeof(save_dir) ) != 0)
			{
			if (chdir( buff ) == 0)
				{
				if (GET_CURR_DIR( tmp, sizeof(tmp) ) != 0)
					{
					int const tmp_len = strlen( tmp );
					if (tmp_len < sz)
						strcpy( buff, tmp );
					else
						{
						strncpy( buff, tmp, sz );
						buff[ sz - 1 ] = '\0';
						}
					}
				chdir( save_dir );
				}
			}
		}
	}


//-----------------------------------------------------------------------------
// dir_writable
//-----------------------------------------------------------------------------
static BOOL dir_writable( char const* path )
	{
	BOOL rc = NO;
	struct stat st;
	if (stat( path, &st ) == 0)
		{
		unsigned int mode = st.st_mode;
		if ((mode & 0002) != 0 ||								// "other"
		   ((mode & 0200) != 0 && st.st_uid == geteuid()))		// "owner"
			rc = YES;
		else if ((mode & 0020) != 0)							// "group"
			{
			gid_t const gid = getegid();
			if (st.st_gid == gid)
				rc = YES;
			else
				{
				GRP_TYPE groups[ MAX_GRPS ];
				int n = getgroups( MAX_GRPS, groups );
				while (n-- > 0)
					if (gid == groups[n])
						{ rc = YES; break; }
				}
			}
		}
	return rc;
	}


//-----------------------------------------------------------------------------
// dir_sticky
//-----------------------------------------------------------------------------
static BOOL dir_sticky( char const* path )
	{
	struct stat st;
	if (stat( path, &st ) == 0)
		return ((st.st_mode & 01000) != 0);
	return NO;
	}


//-----------------------------------------------------------------------------
// str_date
//-----------------------------------------------------------------------------
static char const* str_date( time_t t )
	{
	#define BUFF_SIZE 64
	static char buff[ BUFF_SIZE ];
	strftime( buff, BUFF_SIZE, "%m/%d/%y %H:%M", localtime(&t) );
	return buff;
	}


//-----------------------------------------------------------------------------
// str_perms
//-----------------------------------------------------------------------------
static char const* str_perms( unsigned int mode )
	{
	static char buff[ 16 ];
	int ftype;
	int c;
	char* s = buff;

	ftype = (mode & S_IFMT);
	switch (ftype)
		{
		case S_IFDIR:	c = 'd'; break;
		case S_IFCHR:	c = 'c'; break;
		case S_IFBLK:	c = 'b'; break;
		case S_IFREG:	c = '-'; break;
		case S_IFLNK:	c = 'l'; break;
		case S_IFSOCK:	c = 's'; break;
		default:		c = '-'; break;
		}
	*s++ = c;
	*s++ = (mode & 0400) ? 'r' : '-';
	*s++ = (mode & 0200) ? 'w' : '-';
	*s++ = (mode & S_ISUID) ? 's' : (mode & 0100) ? 'x' : '-';
	*s++ = (mode & 0040) ? 'r' : '-';
	*s++ = (mode & 0020) ? 'w' : '-';
	*s++ = (mode & S_ISGID) ? 's' : (mode & 0010) ? 'x' : '-';
	*s++ = (mode & 0004) ? 'r' : '-';
	*s++ = (mode & 0002) ? 'w' : '-';
	*s++ = (mode & S_ISVTX) ? 't' : (mode & 0001) ? 'x' : '-';
	*s = '\0';

	return buff;
	}


//-----------------------------------------------------------------------------
// fmt_icon
//-----------------------------------------------------------------------------
static void fmt_icon( MiscTableScroll* ts, DirEntry const* de, id cell )
	{
	NXCoord h,w,s;
	NXSize sz;
	NXImage* i = [[Application workspace] getIconForFile:de->longName];

	w = [ts colSize:ICON_SLOT];			if (w == 0) w = 18;
	h = [ts uniformSizeRows];			if (h == 0) h = 18;
	s = (w < h ? w : h) - 1.0;
	sz.width = s;
	sz.height = s;

	[cell setTag:(int)i];		// Unscaled image.
	i = [i copy];
	[i setScalable:YES];
	[i setSize:&sz];
	[cell setImage:i];			// Scaled image.
	}


//-----------------------------------------------------------------------------
// fmt_lock
//-----------------------------------------------------------------------------
static void fmt_lock( MiscTableScroll* ts, DirEntry const* de, id cell )
	{
	BOOL const unlocked = (de->parentWritable &&
		(!de->parentSticky || de->status.st_uid == de->effectiveUid));
	BOOL const flag = (unlocked && strcmp( de->shortName, ".." ) != 0);
	[cell setState:flag];
	[cell setEnabled:flag];
	}


//-----------------------------------------------------------------------------
// fmt_name
//-----------------------------------------------------------------------------
static void fmt_name( MiscTableScroll* ts, DirEntry const* de, id cell )
	{
	[cell setStringValue:de->shortName];
	}


//-----------------------------------------------------------------------------
// fmt_size
//-----------------------------------------------------------------------------
static void fmt_size( MiscTableScroll* ts, DirEntry const* de, id cell )
	{
	[cell setIntValue:de->status.st_size];
	}


//-----------------------------------------------------------------------------
// fmt_modified
//-----------------------------------------------------------------------------
static void fmt_modified( MiscTableScroll* ts, DirEntry const* de, id cell )
	{
	[cell setStringValue:str_date( de->status.st_mtime )];
	[cell setTag:de->status.st_mtime];
	}


//-----------------------------------------------------------------------------
// fmt_perms
//-----------------------------------------------------------------------------
static void fmt_perms( MiscTableScroll* ts, DirEntry const* de, id cell )
	{
	[cell setStringValue:str_perms( de->status.st_mode )];
	[cell setTag:de->isDirectory];
	}


//-----------------------------------------------------------------------------
// fmt_owner
//-----------------------------------------------------------------------------
static void fmt_owner( MiscTableScroll* ts, DirEntry const* de, id cell )
	{
	[cell setStringValueNoCopy:[OWNERS lookup:de->status.st_uid]];
	}


//-----------------------------------------------------------------------------
// fmt_group
//-----------------------------------------------------------------------------
static void fmt_group( MiscTableScroll* ts, DirEntry const* de, id cell )
	{
	[cell setStringValueNoCopy:[GROUPS lookup:de->status.st_gid]];
	}


//-----------------------------------------------------------------------------
// fmt_hardlinks
//-----------------------------------------------------------------------------
static void fmt_hardlinks( MiscTableScroll* ts, DirEntry const* de, id cell )
	{
	[cell setIntValue:de->status.st_nlink];
	}


//-----------------------------------------------------------------------------
// fmt_softlink
//-----------------------------------------------------------------------------
static void fmt_softlink( MiscTableScroll* ts, DirEntry const* de, id cell )
	{
	[cell setStringValue:de->softLink];
	}


//-----------------------------------------------------------------------------
// FORMAT_FUNC
//-----------------------------------------------------------------------------

typedef void (*FormatFunc)( MiscTableScroll*, DirEntry const*, id );

static FormatFunc FORMAT_FUNC[ MAX_SLOT ] =
		{
		fmt_icon,				// ICON_SLOT,
		fmt_name,				// NAME_SLOT,
		fmt_lock,				// LOCK_SLOT,
		fmt_size,				// SIZE_SLOT,
		fmt_modified,			// MODIFIED_SLOT,
		fmt_perms,				// PERMS_SLOT,
		fmt_owner,				// OWNER_SLOT,
		fmt_group,				// GROUP_SLOT,
		fmt_hardlinks,			// HARDLINKS_SLOT,
		fmt_softlink,			// SOFTLINK_SLOT,
		};


//-----------------------------------------------------------------------------
// format_cell
//-----------------------------------------------------------------------------
static inline void format_cell(
		MiscTableScroll* ts,
		DirEntry const* de,
		id cell,
		unsigned int col )
	{
	FORMAT_FUNC[ col ]( ts, de, cell );
	}


//=============================================================================
// IMPLEMENTATION
//=============================================================================
@implementation DirWindow

//-----------------------------------------------------------------------------
// + initCascader
//-----------------------------------------------------------------------------
+ (void)initCascader
	{
	NXSize s; [NXApp getScreenSize:&s];
	CASCADE_ORIGIN_X = s.width / 4;
	CASCADE_ORIGIN_Y = s.height - 40;
	CASCADE_DELTA_X = 20;
	CASCADE_DELTA_Y = 20;
	}


//-----------------------------------------------------------------------------
// + initialize
//-----------------------------------------------------------------------------
+ (id)initialize
	{
	if (self == [DirWindow class])
		{
		[self initCascader];
		OPEN_DIRS = [[List alloc] init];
		LOCKED_ICON	  = [NXImage findImageNamed:LOCKED_ICON_S];
		UNLOCKED_ICON = [NXImage findImageNamed:UNLOCKED_ICON_S];
		OWNERS = [OwnerCache commonInstance];
		GROUPS = [GroupCache commonInstance];
		DEFAULT_COLOR = [Defaults getColor:COLOR_DEF fallback:NX_COLORLTGRAY];
		DEFAULT_AUTO_SORT = [Defaults getBool:SORT_DEF fallback:YES];
		DEFAULT_SHOW_HIDDEN = [Defaults getBool:HIDDEN_DEF fallback:NO];
		DEFAULT_HIGHLIGHT_DIRS = [Defaults getBool:HLIGHT_DEF fallback:NO];
		DEFAULT_DRAG_UNSCALED = [Defaults getBool:UNSCALED_DEF fallback:YES];
		}
	return self;
	}


//-----------------------------------------------------------------------------
// - cascade
//-----------------------------------------------------------------------------
- (void)cascade
	{
	NXCoord top,left;
	left = CASCADE_ORIGIN_X + (CASCADE_DELTA_X * CASCADE_COUNTER);
	top	 = CASCADE_ORIGIN_Y - (CASCADE_DELTA_Y * CASCADE_COUNTER);
	[window moveTopLeftTo:left:top];
	if (++CASCADE_COUNTER >= CASCADE_MAX)
		CASCADE_COUNTER = 0;
	}


//-----------------------------------------------------------------------------
// - isDir:
//-----------------------------------------------------------------------------
- (BOOL)isDir:(int)r
	{
	return [scroll tagAt:r:PERMS_SLOT];
	}


//-----------------------------------------------------------------------------
// - updateButtons
//-----------------------------------------------------------------------------
- (void)updateButtons
	{
	BOOL const enable = [scroll numSelectedRows] == 1 &&
						[self isDir:[scroll selectedRow]];
	if (enable != [cdButton isEnabled])
		[cdButton setEnabled:enable];
	}


//-----------------------------------------------------------------------------
// - setRow:useOwner:color:
//-----------------------------------------------------------------------------
- (void)setRow:(int)r useOwner:(BOOL)useOwner color:(NXColor)color
	{
	int i;
	for (i = MAX_SLOT; i-- >= 0; )
		{
		id cell = [scroll cellAt:r:i];
		if (useOwner &&
			[cell respondsTo:@selector(setUseOwnerBackgroundColor:)])
			{
			[cell setUseOwnerBackgroundColor:YES];
			if ([cell respondsTo:@selector(setOwnerBackgroundColor:)])
				[cell setOwnerBackgroundColor:color];
			}
		else if ([cell respondsTo:@selector(setBackgroundColor:)])
			[cell setBackgroundColor:color];
		}
	}


//-----------------------------------------------------------------------------
// - highlight:row:
//-----------------------------------------------------------------------------
- (void)highlight:(BOOL)flag row:(int)r
	{
	if (flag)
		[self setRow:r useOwner:NO color:NX_COLORCYAN];
	else
		[self setRow:r useOwner:YES color:DEFAULT_COLOR];
	}


//-----------------------------------------------------------------------------
// - highlightDirs:
//-----------------------------------------------------------------------------
- (void)highlightDirs:(BOOL)flag
	{
	int i;
	for (i = [scroll numRows]; i-- > 0; )
		if ([self isDir:i])
			[self highlight:flag row:i];
	}


//-----------------------------------------------------------------------------
// - freeImages
//-----------------------------------------------------------------------------
- (void)freeImages
	{
	int i;
	for (i = [scroll numRows]; i-- > 0; )
		{
		id cell = [scroll cellAt:i:ICON_SLOT];
		id image = [cell image];		// Scaled image.
		[cell setImage:0];
		[image free];
		[(id)[cell tag] free];			// Unscaled image.
		}
	}


//-----------------------------------------------------------------------------
// - tableScroll:fontChangedFrom:to:
//-----------------------------------------------------------------------------
- (id)tableScroll:(MiscTableScroll*)ts
		fontChangedFrom:(Font*)oldFont
		to:(Font*)newFont
	{
	DEFAULT_FONT = newFont;
	[Defaults set:FONT_DEF font:DEFAULT_FONT];
	return self;
	}


//-----------------------------------------------------------------------------
// - tableScroll:border:slotResized:
//-----------------------------------------------------------------------------
- (id)tableScroll:(MiscTableScroll*)ts
		border:(MiscBorderType)b
		slotResized:(int)n
	{
	char* s;
	assert( b == MISC_COL_BORDER );
	s = [ts colSizesAsString:0 size:0 canExpand:YES];
	[Defaults set:COL_SIZES_DEF str:s];
	free(s);
	return self;
	}


//-----------------------------------------------------------------------------
// saveSlotOrder:border:
//-----------------------------------------------------------------------------
- (void)saveSlotOrder:(MiscTableScroll*)ts border:(MiscBorderType)b
	{
	if (b == MISC_COL_BORDER)
		{
		char* s = [ts colOrderAsString:0 size:0 canExpand:YES];
		[Defaults set:COL_ORDER_DEF str:s];
		free(s);
		}
	}


//-----------------------------------------------------------------------------
// - tableScroll:border:slotDraggedFrom:to:
//-----------------------------------------------------------------------------
- (id)tableScroll:(MiscTableScroll*)ts
		border:(MiscBorderType)b
		slotDraggedFrom:(int)fromPos
		to:(int)toPos
	{
	[self saveSlotOrder:ts border:b];
	return self;
	}


//-----------------------------------------------------------------------------
// - tableScroll:border:slotSortReversed:
//-----------------------------------------------------------------------------
- (id)tableScroll:(MiscTableScroll*)ts
		border:(MiscBorderType)b
		slotSortReversed:(int)n
	{
	[self saveSlotOrder:ts border:b];
	return self;
	}


//-----------------------------------------------------------------------------
// - tableScroll:canEdit:at::
//-----------------------------------------------------------------------------
- (BOOL)tableScroll:(MiscTableScroll*)ts
		canEdit:(NXEvent const*)ev
		at:(int)row :(int)col
	{
	return ((ev == 0 || ev->data.mouse.click == 2) &&
			col == NAME_SLOT && [[ts cellAt:row:LOCK_SLOT] state] != 0);
	}

 
//-----------------------------------------------------------------------------
// - tableScroll:preparePasteboard:forDragOperationAt::
//-----------------------------------------------------------------------------
- (void)tableScroll:(MiscTableScroll*)s
		preparePasteboard:(Pasteboard*)pb 
		forDragOperationAt:(int)r :(int)c
	{
	char const* const name = [[s cellAt:r:NAME_SLOT] stringValue];
	char buff[ FILENAME_MAX * 2 + 1 ];
	strcat( strcat( strcpy( buff, path ), "/" ), name );

	[pb declareTypes:&NXFilenamePboardType num:1 owner:0];
	[pb writeType:NXFilenamePboardType data:buff length:strlen(buff)];
	}


//-----------------------------------------------------------------------------
// - tableScroll:allowDragOperationAt::
//-----------------------------------------------------------------------------
- (BOOL)tableScroll:(MiscTableScroll*)s allowDragOperationAt:(int)r :(int)c
	{
	return (c == ICON_SLOT);
	}


//-----------------------------------------------------------------------------
// - tableScroll:imageForDragOperationAt::
//-----------------------------------------------------------------------------
- (NXImage*)tableScroll:(MiscTableScroll*)s
		imageForDragOperationAt:(int)r :(int)c
	{
	return (dragUnscaled ? (NXImage*)[[s cellAt:r:c] tag] : 0);
	}


//-----------------------------------------------------------------------------
// - addFile:
//-----------------------------------------------------------------------------
- (void)addFile:(DirEntry const*)de
	{
	int r,c;

	[scroll addRow];
	r = [scroll numRows] - 1;
	for (c = 0; c < MAX_SLOT; c++)
		format_cell( scroll, de, [scroll cellAt:r:c], c );

	if (highlightDirs && de->isDirectory)
		[self highlight:YES row:r];
	}


//-----------------------------------------------------------------------------
// setupDirFlag:
//-----------------------------------------------------------------------------
- (void)setupDirFlag:(DirEntry*)de
	{
	unsigned int mode = de->status.st_mode;

	if ((mode & S_IFMT) == S_IFLNK)		// soft-link?
		{
		int len = readlink( de->longName, de->softLink, FILENAME_MAX );
		if (len >= 0)
			{
			struct stat st;
			de->softLink[ len ] = '\0';
			if (stat( de->longName, &st ) == 0)
				mode = st.st_mode;		// mode of file linked to
			}
		}

	de->isDirectory = ((mode & S_IFMT) == S_IFDIR);
	}


//-----------------------------------------------------------------------------
// - includeFile:length: -- exclude ".", include ".."
//-----------------------------------------------------------------------------
- (BOOL)includeFile:(char const*)name length:(int)len
	{
	BOOL const dotName = (name[0] == '.');
	return ((len > 1 || !dotName) &&			// exclude "."
				(showHidden || !dotName || (len == 2 && name[1] == '.')));
	}


//-----------------------------------------------------------------------------
// - fillScroll
//-----------------------------------------------------------------------------
- (void)fillScroll
	{
	size_t totalBytes = 0;
	int dirlen;
	DIR* dirp;
	char namebuff[ FILENAME_MAX + 1 ];

	dirlen = strlen( path );
	strcpy( namebuff, path );
	if (dirlen == 0 || namebuff[ dirlen - 1 ] != '/')
		{
		namebuff[ dirlen++ ] = '/';
		namebuff[ dirlen ] = '\0';
		}

	[window disableDisplay];
	[self freeImages];
	[scroll empty];

	if ((dirp = opendir( path )) == 0)
		{
		NXRunAlertPanel( "Can't Read",
				"Cannot read directory, %s\n%d:%s", "OK", 0, 0,
				path, errno, strerror(errno) );
		}
	else
		{
		struct direct const* dp;
		uid_t const euid = geteuid();
		BOOL const sticky = dir_sticky( path );
		writable = dir_writable( path );

		while ((dp = readdir( dirp )) != 0)
			{
			if ([self includeFile:dp->d_name length:dp->d_namlen])
				{
				DirEntry de;
				memcpy( namebuff + dirlen, dp->d_name, dp->d_namlen + 1 );
				de.shortName = dp->d_name;
				de.longName = namebuff;
				de.softLink[0] = '\0';
				de.parentWritable = writable;
				de.parentSticky = sticky;
				de.effectiveUid = euid;
				if (lstat( namebuff, &de.status ) == 0)
					{
					totalBytes += de.status.st_size;
					[self setupDirFlag:&de];
					[self addFile:&de];
					}
				}
			}
		closedir( dirp );
		}

	if ([scroll autoSortRows])
		[scroll sortRows];
	[scroll sizeToCells];

	sprintf( namebuff, "%d files   %lu bytes", [scroll numRows], totalBytes );
	[countField setStringValue:namebuff];

	[self updateButtons];
	[[window reenableDisplay] display];
	}


//-----------------------------------------------------------------------------
// - setPath:
//-----------------------------------------------------------------------------
- (void)setPath:(char const*)dirname
	{
	NXZone* const z = [self zone];
	int dirlen;
	if (path != 0) NXZoneFree( z, path );
	if (dirname == 0) dirname = NXHomeDirectory();
	if (dirname == 0) dirname = "/";
	dirlen = strlen( dirname ) + 1;
	path = (char*) NXZoneMalloc( z, dirlen );
	memcpy( path, dirname, dirlen );
	[window setTitleAsFilename:path];
	}


//-----------------------------------------------------------------------------
// - load:
//-----------------------------------------------------------------------------
- (void)load:(char const*)dirname
	{
	[self setPath:dirname];
	[self fillScroll];
	}


//-----------------------------------------------------------------------------
// - save:
//-----------------------------------------------------------------------------
- (id)save:(id)sender
	{
	[[MiscExporter commonInstance] exportTableScroll:scroll];
	return self;
	}


//-----------------------------------------------------------------------------
// - print:
//-----------------------------------------------------------------------------
- (id)print:(id)sender
	{
	[scroll printPSCode:self];
	return self;
	}


//-----------------------------------------------------------------------------
// - open:
//-----------------------------------------------------------------------------
- (id)open:(id)sender
	{
	if ([scroll hasRowSelection])
		{
		int i;
		int len;
		char buff[ FILENAME_MAX + 1 ];
		MiscIntList* list = [[MiscIntList allocFromZone:[self zone]] init];

		strcpy( buff, path );
		len = strlen( buff );
		if (len == 0 || buff[len - 1] != '/')
			{
			buff[ len++ ] = '/';
			buff[ len ] = '\0';
			}
			
		[scroll selectedRows:list];
		for (i = [list count]; i-- > 0; )
			{
			int row = [list intAt:i];
			strcpy( buff + len, [[scroll cellAt:row:NAME_SLOT] stringValue] );
			if ([self isDir:row])
				[[self class] launchDir:buff];
			else
				[[Application workspace] openFile:buff];
			}
		[list free];
		}
	return self;
	}


//-----------------------------------------------------------------------------
// - destroy:
//-----------------------------------------------------------------------------
- (id)destroy:(id)sender
	{
	if (writable && [scroll hasRowSelection])
		{
		size_t totalLen = 0;
		char* fileList;
		int i;
		NXZone* const z = [self zone];
		MiscIntList* list = [[MiscIntList allocFromZone:z] init];

		[scroll selectedRows:list];

		for (i = [list count]; i-- > 0; )
			{
			int const row = [list intAt:i];
			char const* const s = [[scroll cellAt:row:NAME_SLOT] stringValue];
			if (s != 0)
				totalLen += strlen(s) + 1;
			}

		fileList = (char*) NXZoneMalloc( z, totalLen + 1 );
		
		totalLen = 0;
		for (i = [list count]; i-- > 0; )
			{
			int row = [list intAt:i];
			char const* const s = [[scroll cellAt:row:NAME_SLOT] stringValue];
			if (s != 0)
				{
				int const len = strlen(s);
				strcpy( fileList + totalLen, s );
				totalLen += len;
				fileList[ totalLen++ ] = '\t';
				}
			}
		fileList[ totalLen ] = '\0';

		[[Application workspace]
				performFileOperation:WSM_DESTROY_OPERATION
						source:path
						destination:""
						files:fileList
						options:""];

		NXZoneFree( z, fileList );
		[list free];
		[self fillScroll];
		}
	return self;
	}


//-----------------------------------------------------------------------------
// - rename:to:msg:sys:
//-----------------------------------------------------------------------------
- (BOOL)rename:(char const*)oldName
		to:(char const*)newName
		msg:(char const**)msg
		sys:(char const**)sys
	{
	BOOL ok = NO;
	char oldPath[ MAXPATHLEN ];
	char newPath[ MAXPATHLEN ];
	struct stat st;
	int pathLen;
	int rc;

	pathLen = strlen( path );
	assert( pathLen > 0 );
	memcpy( oldPath, path, pathLen );
	memcpy( newPath, path, pathLen );
	if (oldPath[ pathLen - 1 ] != '/')
		{
		oldPath[ pathLen ] = '/';
		newPath[ pathLen ] = '/';
		pathLen++;
		}

	strcpy( oldPath + pathLen, oldName );
	strcpy( newPath + pathLen, newName );

	rc = stat( newPath, &st );
	if (rc == 0)
		*msg = "Filename in use.";
	else if (errno != ENOENT)
		{
		*msg = "stat(): ";
		*sys = strerror( errno );
		}
	else // (rc != 0 && errno == ENOENT)
		{
		rc = rename( oldPath, newPath );
		if (rc == 0)
			ok = YES;
		else
			{
			*msg = "rename(): ";
			*sys = strerror( errno );
			}
		}

	return ok;
	}


//-----------------------------------------------------------------------------
// - textWillEnd:
//-----------------------------------------------------------------------------
- (BOOL)textWillEnd:(id)sender
	{
	BOOL reject = YES;
	int r,c,len;
	char const* errMsg;
	char const* sysErr;
	char const* oldName;
	char* newName;

	r = [scroll clickedRow];
	c = [scroll clickedCol];
	assert( c == NAME_SLOT );

	oldName = [[scroll cellAt:r:c] stringValue];
	if (oldName == 0) oldName = "";

	len = [sender textLength];
	if (len == 0)
		errMsg = "Filename cannot be the empty string.";
	else if (len + 1 + strlen(path) >= MAXPATHLEN)
		errMsg = "Filename too long.";
	else
		{
		newName = (char*) malloc( len + 1 );
		assert( newName != 0 );
		[sender getSubstring:newName start:0 length:len + 1];

		if (strcmp( newName, oldName ) == 0)
			reject = NO;		// Unchanged, silently ignore.
		else if (strcmp( newName, ".." ) == 0 ||
				 strcmp( newName, "." ) == 0 ||
				 strchr( newName, '/' ) != 0)
			errMsg = "Illegal filename.";
		else
			reject = ![self rename:oldName to:newName
							msg:&errMsg sys:&sysErr];

		free( newName );
		}

	if (reject)
		{
		if (sysErr == 0) sysErr = "";
		NXRunAlertPanel( "Error", "%s%s", "OK", 0, 0, errMsg, sysErr );
		[sender setText:oldName];
		}

	return reject;
	}


//-----------------------------------------------------------------------------
// - refreshPressed:
//-----------------------------------------------------------------------------
- (id)refreshPressed:(id)sender
	{
	[scroll abortEditing];
	[self fillScroll];
	return self;
	}


//-----------------------------------------------------------------------------
// - cdPressed:
//-----------------------------------------------------------------------------
- (id)cdPressed:(id)sender
	{
	[scroll abortEditing];
	if ([scroll numSelectedRows] == 1)
		{
		MiscCoord_P const row = [scroll selectedRow];
		if ([self isDir:row])
			{
			char buff[ FILENAME_MAX * 2 + 1 ];
			strcat( strcat( strcpy( buff, path ), "/" ),
				[[scroll cellAt:row:NAME_SLOT] stringValue] );
			normalize_path( buff, sizeof(buff) );
			[self load:buff];
			}
		}
	return self;
	}


//-----------------------------------------------------------------------------
// - autoSortClick:
//-----------------------------------------------------------------------------
- (id)autoSortClick:(id)sender
	{
	BOOL const switchState = [autoSortSwitch state];
	[scroll abortEditing];
	if (autoSort != switchState)
		{
		DEFAULT_AUTO_SORT = autoSort = switchState;
		[Defaults set:SORT_DEF bool:DEFAULT_AUTO_SORT];
		[scroll setAutoSortRows:switchState];
		if (switchState)
			[scroll sortRows];
		}
	return self;
	}


//-----------------------------------------------------------------------------
// - hiddenFilesClick:
//-----------------------------------------------------------------------------
- (id)hiddenFilesClick:(id)sender
	{
	BOOL const switchState = [hiddenFilesSwitch state];
	[scroll abortEditing];
	if (showHidden != switchState)
		{
		DEFAULT_SHOW_HIDDEN = showHidden = switchState;
		[Defaults set:HIDDEN_DEF bool:DEFAULT_SHOW_HIDDEN];
		[self fillScroll];
		}
	return self;
	}


//-----------------------------------------------------------------------------
// - highlightClick:
//-----------------------------------------------------------------------------
- (id)highlightClick:(id)sender
	{
	BOOL const switchState = [highlightSwitch state];
	[scroll abortEditing];
	if (highlightDirs != switchState)
		{
		DEFAULT_HIGHLIGHT_DIRS = highlightDirs = switchState;
		[Defaults set:HLIGHT_DEF bool:DEFAULT_HIGHLIGHT_DIRS];
		[self highlightDirs:highlightDirs];
		[scroll display];
		}
	return self;
	}


//-----------------------------------------------------------------------------
// - dragUnscaledClick:
//-----------------------------------------------------------------------------
- (id)dragUnscaledClick:(id)sender
	{
	BOOL const switchState = [dragUnscaledSwitch state];
	if (dragUnscaled != switchState)
		{
		DEFAULT_DRAG_UNSCALED = dragUnscaled = switchState;
		[Defaults set:UNSCALED_DEF bool:DEFAULT_DRAG_UNSCALED];
		}
	return self;
	}


//-----------------------------------------------------------------------------
// - lockClick:
//-----------------------------------------------------------------------------
- (id)lockClick:(id)sender
	{
	int const row = [sender clickedRow];
	if ([sender autoSortRows])
		[sender sortRow:row];
	return self;
	}


//-----------------------------------------------------------------------------
// - didClick:
//-----------------------------------------------------------------------------
- (id)didClick:(id)sender
	{
	[self updateButtons];
	return self;
	}


//-----------------------------------------------------------------------------
// - didDoubleClick:
//-----------------------------------------------------------------------------
- (id)didDoubleClick:(id)sender
	{
	[self open:sender];
	return self;
	}


//-----------------------------------------------------------------------------
// - makeKeyAndOrderFront:
//-----------------------------------------------------------------------------
- (id)makeKeyAndOrderFront:(id)sender
	{
	[window makeKeyAndOrderFront:sender];
	return self;
	}


//-----------------------------------------------------------------------------
// - windowWillClose:
//-----------------------------------------------------------------------------
- (id)windowWillClose:(id)sender
	{
	[scroll abortEditing];
	[OPEN_DIRS removeObject:self];
	[NXApp delayedFree:self];
	return self;
	}


//-----------------------------------------------------------------------------
// - windowDidResize:
//-----------------------------------------------------------------------------
- (id)windowDidResize:(id)sender
	{
	NXRect r; [sender getFrame:&r];
	if (r.size.width  != DEFAULT_WIN_SIZE.width ||
		r.size.height != DEFAULT_WIN_SIZE.height)
		{
		DEFAULT_WIN_SIZE = r.size;
		[Defaults set:SIZE_DEF size:DEFAULT_WIN_SIZE];
		}
	return self;
	}


//-----------------------------------------------------------------------------
// - setDefaultColor:
//-----------------------------------------------------------------------------
- (void)setDefaultColor:(NXColor)c
	{
	DEFAULT_COLOR = c;
	[Defaults set:COLOR_DEF color:c];
	}


//-----------------------------------------------------------------------------
// - setColors:
//-----------------------------------------------------------------------------
- (void)setColors:(NXColor)c
	{
	[window disableDisplay];
	[window setBackgroundColor:c];
	[scroll setColor:c];
	[window reenableDisplay];
	}


//-----------------------------------------------------------------------------
// - draggingEntered:
//-----------------------------------------------------------------------------
- (NXDragOperation)draggingEntered:(id<NXDraggingInfo>)sender
	{
	return ([sender draggingSourceOperationMask] & NX_DragOperationGeneric);
	}


//-----------------------------------------------------------------------------
// - performDragOperation:
//-----------------------------------------------------------------------------
- (BOOL)performDragOperation:(id<NXDraggingInfo>)sender
	{
	[self setDefaultColor:
				NXReadColorFromPasteboard( [sender draggingPasteboard] )];
	[self setColors:DEFAULT_COLOR];
	[window display];
	return YES;
	}


//-----------------------------------------------------------------------------
// - initDefaults
//-----------------------------------------------------------------------------
- (void)initDefaults
	{
	static BOOL initialized = NO;
	if (!initialized)
		{
		NXRect r; [window getFrame:&r];
		DEFAULT_WIN_SIZE = r.size;
		DEFAULT_FONT = [Defaults getFont:FONT_DEF fallback:[scroll font]];
		initialized = YES;
		}
	}


//-----------------------------------------------------------------------------
// - loadDefaults
//-----------------------------------------------------------------------------
- (void)loadDefaults
	{
	NXRect r;
	char const* s;

	[window getFrame:&r];
	r.size = [Defaults getSize:SIZE_DEF fallback:DEFAULT_WIN_SIZE];
	[window placeWindow:&r];

	autoSort = DEFAULT_AUTO_SORT;
	showHidden = DEFAULT_SHOW_HIDDEN;
	highlightDirs = DEFAULT_HIGHLIGHT_DIRS;
	dragUnscaled = DEFAULT_DRAG_UNSCALED;

	[autoSortSwitch setState:autoSort];
	[hiddenFilesSwitch setState:showHidden];
	[highlightSwitch setState:highlightDirs];
	[dragUnscaledSwitch setState:dragUnscaled];

	[scroll setAutoSortRows:autoSort];
	[scroll setFont:DEFAULT_FONT];
	[self setColors:DEFAULT_COLOR];

	s = [Defaults getStr:COL_SIZES_DEF fallback:0];
	if (s)
		[scroll setColSizesFromString:s];

	s = [Defaults getStr:COL_ORDER_DEF fallback:0];
	if (s)
		[scroll setColOrderFromString:s];
	}


//-----------------------------------------------------------------------------
// - initLockSlot
//-----------------------------------------------------------------------------
- (void)initLockSlot
	{
	id proto = [scroll colCellPrototype:LOCK_SLOT];
	[proto setType:NX_SWITCH];
	[proto setIconPosition:NX_ICONONLY];
	[proto setTarget:self];
	[proto setAction:@selector(lockClick:)];
	[proto setImage:LOCKED_ICON];
	[proto setAltImage:UNLOCKED_ICON];
	}


//-----------------------------------------------------------------------------
// - initNameSlot
//-----------------------------------------------------------------------------
- (void)initNameSlot
	{
	id proto = [scroll colCellPrototype:NAME_SLOT];
	[proto setEditable:YES];
	[proto setScrollable:YES];
	}


//-----------------------------------------------------------------------------
// - initSlots
//-----------------------------------------------------------------------------
- (void)initSlots
	{
	[self initLockSlot];
	[self initNameSlot];
	[[scroll colCellPrototype:SIZE_SLOT] setAlignment:NX_RIGHTALIGNED];
	[[scroll colCellPrototype:HARDLINKS_SLOT] setAlignment:NX_RIGHTALIGNED];
	}


//-----------------------------------------------------------------------------
// - initWithDir:
//-----------------------------------------------------------------------------
- (id)initWithDir:(char const*)dirname
	{
	char buff[ FILENAME_MAX + 1 ];
	NXZone* const z = [self zone];

	[super init];
	path = 0;
	[[NXBundle bundleForClass:[self class]]
		getPath:buff forResource:[[self class] name] ofType:"nib"];
	[NXApp loadNibFile:buff owner:self withNames:NO fromZone:z];
	[window registerForDraggedTypes:&NXColorPboardType count:1];
	[self initSlots];
	[self initDefaults];
	[self loadDefaults];
	[self load:dirname];
	[OPEN_DIRS addObject:self];
	[self cascade];
	[window makeKeyAndOrderFront:self];
	return self;
	}


//-----------------------------------------------------------------------------
// - init
//-----------------------------------------------------------------------------
- (id)init
	{
	return [self initWithDir:NXHomeDirectory()];
	}


//-----------------------------------------------------------------------------
// - free
//-----------------------------------------------------------------------------
- (id)free
	{
	NXZone* const z = [self zone];
	[window setDelegate:0];
	[window close];
	[window disableDisplay];
	[self freeImages];
	[window free];
	if (path != 0)
		free( path );
	[super free];
	RETIRE_ZONE(z);
	return 0;
	}


//-----------------------------------------------------------------------------
// - path
//-----------------------------------------------------------------------------
- (char const*)path
	{
	return path;
	}


//-----------------------------------------------------------------------------
// + findDir:
//-----------------------------------------------------------------------------
+ (DirWindow*)findDir:(char const*)normalizedPath
	{
	if (normalizedPath != 0)
		{
		unsigned int i;
		unsigned int const lim = [OPEN_DIRS count];
		for (i = 0; i < lim; i++)
			{
			DirWindow* p = (DirWindow*) [OPEN_DIRS objectAt:i];
			char const* s = [p path];
			if (s != 0 && strcmp( s, normalizedPath ) == 0)
				return p;
			}
		}
	return 0;
	}


//-----------------------------------------------------------------------------
// + launchDir:
//-----------------------------------------------------------------------------
+ (id)launchDir:(char const*)dirname
	{
	DirWindow* p = 0;
	char buff[ FILENAME_MAX + 1 ];
	if (dirname == 0) dirname = NXHomeDirectory();
	if (dirname == 0) dirname = "/";
	strncpy( buff, dirname, sizeof(buff) );
	buff[ sizeof(buff) - 1 ] = '\0';
	normalize_path( buff, sizeof(buff) );
	if ((p = [self findDir:buff]) != 0)
		[p makeKeyAndOrderFront:self];
	else
		p = [[self allocFromZone:EMPLOY_ZONE()] initWithDir:buff];
	return p;
	}

@end
