/* 
 * Help.m, a help object to manage and display RTF help files.
 * The help object owns its own nib section "Help.nib" which has a
 * Help panel - with an NXBrowser to display the help topics, and
 * a scrolling text view to display the help files.  The help files are
 * all stored as RTF text files in a directory called Help within the
 * app wrapper.  At init time, the Help object loads the browser with 
 * names of all the files found in the Help directory.  When a name is
 * chosen from the browser, the help object opens a stream on that file
 * and read the rich text into the text object. The help object also
 * responds to request for context-sensitive help, by trying to find an
 * appropriate help file for the view that was moused down in.  See the
 * helpForObject: method for more detailed explanation of that.
 * This object is a useful addition to any program, because all users
 * appreciate help!
 *
 * Author: Julie Zelenski, NeXT Developer Support
 * You may freely copy, distribute and reuse the code in this example.  
 * NeXT disclaims any warranty of any kind, expressed or implied, as to 
 * its fitness for any particular use.
 */

#import <appkit/appkit.h>
#import "Help.h"
#import <sys/dir.h> //for getdirentries()
#import <libc.h>     


@implementation Help:Object


- init 
/* For newly created help object, loads the nib section with the Help
 * panel which has the topics browser and a scrolling text view for 
 * displaying help files.
 */
{
    [super init];
    [[NXBundle mainBundle] getPath:helpDirectory forResource:"HelpFiles" ofType:NULL];
    sprintf(noHelpFile,"%s/%s",helpDirectory,"No Help.rtf");
    // Note that as a side-effect of loading Help.nib, the helpPanel outlet
    // gets set to point to the window.
    if (![NXApp loadNibSection:"Help.nib" owner:self]) {
	NXLogError ("Could not load Help.nib");
    }
    return self;
}

- setHelpBrowser:anObject;
/* Sets the helpBrowser outlet, and calls on the browser to load up.
 */
{
    helpBrowser = anObject;
    [helpBrowser setDelegate:self];
    [helpBrowser loadColumnZero];
    return self;
}

/* TARGET/ACTION METHODS */

- generalHelp:sender;
/* This is the target/action method for the "Help" menu item.  This method 
 * will show the "general help" file.
 */
{
    [self showHelpFile:"General Help"];
    return self;
}

- browserHit:sender
/* This is the target/action method from the help topics browser.  When
 * a help topic is selected, this method will show the help file for that
 * topic.
 */
{   
    [self showHelpFile:[[[sender matrixInColumn:0] selectedCell] stringValue]];
    return self;
}


- print:sender;
/* This method is called by the Print menu cell in the main menu.  It will 
 * print the current help file.
 */
{
    [[helpScrollView docView] printPSCode:sender];
    return self;
}


/* HELP METHODS */

- helpForWindow:window;
/* If this window is the main menu, it will ask to display the help file
 * for "Main Menu." (I didn't want to call the Main Menu help file "BusyBox 
 * Menu")  Else it gets the help file for <window title> <window name>
 * "Info Panel" or "Document Menu" for example.
 */
{
    char filename[MAXPATHLEN];

    if (window == [NXApp mainMenu])
        sprintf(filename,"Main Menu");
    else 
        sprintf(filename,"%s %s",[window title],[window name]);
    [self showHelpFile:filename];
    return self;
}

- helpForView:view atPoint:(NXPoint *)aPt;
/* Gives help for the specified view, which was hit with a Control-mouseDown 
 * at aPt.  Gives feedback to the user which view was hit by framing the
 * bounds of the view with a gray rectangle.  This is done with instance
 * drawing and erased after the helpfile is read and displayed.  If the view
 * is a matrix, the method makes the effort to find the particular cell which
 * was hit and to only frame that cell.
 * This method calls on the method helpForObject which will figure out which
 * help file to display.
 */
{  
    int row,column;
    NXRect b;
    NXPoint p;
    id cell = nil;

    [view getBounds:&b];
    if ([view isKindOf:[Matrix class]]) {
        p = *aPt;
	[view convertPoint:&p fromView:nil];
	if (cell = [view getRow:&row andCol:&column forPoint:&p])
	    [view getCellFrame:&b at:row :column];
    }
    [view lockFocus];
    PSnewinstance();
    PSsetinstance(YES);
    PSsetgray(NX_DKGRAY);
    NXFrameRectWithWidth(&b,1.0);
    [[view window] flushWindow];
    PSsetinstance(NO);
    if (cell) 
    	[self helpForObject:cell];
    else if ([[view window] contentView] == view) 
    	[self helpForWindow:[view window]];
    else
    	[self helpForObject:view];
    PSnewinstance();
    [view unlockFocus];
    return self;
}


- helpForObject:object;
/* The method tries to cons together a file name that represents help
 * for the given object.  It makes no assumptions about the tags or
 * titles of the views, rather it employs a general strategy.  For many
 * objects, the name is simply used ("ScrollView","TextField"). For Cells,
 * it strips Cell from the name, for our purposes, TextFieldCell is
 * the same as a TextField.  For Buttons (and ButtonCells), it uses icon + name
 * (radio Button, popUp Button).  For MenuCells, it figures out where it is
 * submenu or a item.  Items display help for their parent menu, (using 
 * helpForWindow: method), submenus use title + menu (Format Menu, Find Menu).
 * What is neat about this general scheme is that all views, windows, menu
 * respond to the control-click, not just the ones I created. Bring up the 
 * Print panel and control-click on the Resolution popup, and you will see 
 * the help file for popups!  Control-click on the Save Panel, you get help for 
 * Save Panel!  This is probably not the way that a real application would 
 * implement help.  It happens to work for mine, because I want the same
 * help file to be displayed for every popup list, the same file for every 
 * button.  In your app, different button have various functions, and you 
 * would want to display different help files for different buttons.  You 
 * will probably devise a scheme using unique tags or titles, a more specific
 * way to determine what help file to display.
 */
{
    char filename[MAXPATHLEN];
    char *suffix;
    int len;

    sprintf(filename,"%s",[object name]);
    if ([object isKindOf:[Button class]] || [object isKindOf:[Cell class]]) {
	 if ([object icon]) 
	     sprintf(filename,"%s %s",[object icon],[object name]);
	 if ([object isKindOf:[Cell class]]) {
	     len = strlen(filename);
	     suffix = filename + (len-4)*sizeof(char);
	     if (strcmp("Cell",suffix)==0) {
		 filename[len-4] = '\0';
	    }
	}
    }
    if ([object isKindOf:[MenuCell class]]) {
        if ([object icon] && (strcmp([object icon],"menuArrow")==0))
	    sprintf(filename,"%s %s",[object title],"Menu");
	else {
	    return [self helpForWindow:[[object controlView] window]];
	}
    }
    [self showHelpFile:filename];
    return self;
}

- showHelpFile:(const char*)filename;
/* Tries to open a stream for the specified RTF text file in the Help 
 * directory so the text object can readRichText.  Also selects the
 * filename in the browser of help topics.  If the filename doesn't exist,
 * it will select and display the "no help" file.  It also brings the
 * help panel to the front.
 */
{
   NXStream *stream;
   char helpFile[MAXPATHLEN];
   static NXPoint origin = {0.0,0.0};

    if (![self browser:helpBrowser selectCell:filename inColumn:0])
        [self browser:helpBrowser selectCell:"No Help" inColumn:0];
    sprintf(helpFile,"%s/%s.rtf",helpDirectory,filename);
    if ((stream = NXMapFile(helpFile,NX_READONLY)) == NULL)
        stream = NXMapFile(noHelpFile,NX_READONLY);
    if (stream != NULL) {
    	[helpPanel disableFlushWindow];
        [[helpScrollView docView] readRichText:stream]; 
	[[helpScrollView docView] scrollPoint:&origin];
	[[helpPanel reenableFlushWindow] flushWindow];
    	NXCloseMemory(stream,NX_FREEBUFFER);
    }
    [helpPanel orderFront:self];
    return self;
}


/* BROWSER DELEGATE METHODS */


#define CHUNK 127
static char **addFile(const char *file, int length, char **list, int count)
/* Adds the specified filename to the list of filenames.  It allocates 
 * more memory in chunks as needed.
 */
{
    char *suffix;
    
    if (!list) list = (char **)malloc(CHUNK*sizeof(char *));
    if (suffix = rindex(file,'.')) 
        *suffix  = '\0'; 	/* strip rtf suffix */
    list[count] = (char *)malloc((length+1)*sizeof(char));
    strcpy(list[count], file);
    count++;
    if (!(count% CHUNK)) {
	list = (char **)realloc(list,(((count/CHUNK)+1)*CHUNK)*sizeof(char *));
    }
    list[count] = NULL;
    return list;
}

static void freeList(char **list)
/* Frees the array of filenames
 */
 {
    char **strings;

    if (list) {
	strings = list;
	while (*strings) free(*strings++);
	free(list);
    }
}

static BOOL isOk(const char *s)
/* checks to make sure the filename is not NULL and to verify that it is
 * not a "dot"--hidden file.
 */
{
    return (!s[0] || s[0] == '.') ? NO : YES;
}

static int caseInsensitiveCompare(const void *arg1, const void *arg2)
/* Compares the two arguments without regard for case using strcasecmp().
*/
{
    char *string1, *string2;

    string1 = *((char **)arg1);
    string2 = *((char **)arg2);
    return strcasecmp(string1,string2);
}

static char **fileList;

- (int)browser:sender fillMatrix:matrix inColumn:(int)column
/* This delegate method goes out to the help directory and gets a list
 * of all the files in that directory.  It creates a list of file names
 * for the static variable fileList, and will load the filenames into the 
 * browser on demand (lazy loading).
 */
{
    long basep;
    char *buf;
    struct direct *dp;
    char **list = NULL;
    int cc, fd, fileCount = 0;
    char dirbuf[8192];

    if ((fd = open(helpDirectory, O_RDONLY, 0644)) > 0) {
	cc = getdirentries(fd, (buf = dirbuf), 8192, &basep);
	while (cc) {
	    dp = (struct direct *)buf;
	    if (isOk(dp->d_name)) {
		list = addFile(dp->d_name, dp->d_namlen, list, fileCount++);
	    }
	    buf += dp->d_reclen;
	    if (buf >= dirbuf + cc) {
		cc = getdirentries(fd, (buf = dirbuf), 8192, &basep);
	    }
	}
	close(fd);
	if (list) qsort(list,fileCount,sizeof(char *),caseInsensitiveCompare);
    }
    freeList(fileList);
    fileList = list;
    return fileCount;
}

- browser:sender loadCell:cell atRow:(int)row inColumn:(int)column
/* This delegate method loads the cell for a given row.  The stringValue
 * for that row comes from the fileList.
 */
{
    if (fileList) {
	[cell setStringValueNoCopy:fileList[row]];
	[cell setLeaf:YES];
    }
    return self;
}


- (BOOL)browser:sender selectCell:(const char *)title inColumn:(int)column
/* This delegate method selects the cell with the given title.  If it finds
 * a cell with that title, it verifies that it has a file entry in the 
 * fileList, forces the loading of the cell, selects it (highlights) and
 * scrolls the browser so the cell is visible.  It returns a boolean value
 * which indicates whether the cell was found.
 */
{
    int row;
    id matrix;

    if (title) {
	matrix = [sender matrixInColumn:column];
	if (!fileList) return NO;
	for (row = [matrix cellCount]-1; row >= 0; row--) {
	    if (fileList[row] && !strcmp(title, fileList[row])) {
		[sender getLoadedCellAtRow:row inColumn:column];
		[matrix selectCellAt:row :0];
		[matrix scrollCellToVisible:row :0];
		return YES;
	    }
	}
    }
    return NO;
}


/* WINDOW DELEGATE METHODS */

- windowWillResize:sender toSize:(NXSize *)frameSize;
/* This method constrains the Help Panel to a reasonable minimum size
 * when the user resizes the panel.
 */
{
    frameSize->width = MAX(frameSize->width,400.0);
    frameSize->height = MAX(frameSize->height,350.0);
    return self;
}


@end