// BWBorkPadController.m -- the application controller for BorkPad

// Copyright (C) 2003 Borkware
// We use the BSD License.  Check out http://borkware.com/license

// include all the jazz we're using
#import "BWBorkPadController.h"

#import "BWPageControl.h"
#import "BWSwoopView.h"
#import "BWBorkPadModel.h"
#import "BWBorkPadPrefController.h"


// these are keys into the use defaults, for storing the font and size

static NSString *BWBorkPadDefaultsFontFamilyKey = @"FontFamily";
static NSString *BWBorkPadDefaultsFontSizeKey = @"FontSize";

// when doing the page-turning swoop animation, put it into a transparent
// window that hovers over the BorkPad window

static NSWindow *g_clearWindow;


@implementation BWBorkPadController


// hook up to the defauls system.  Since we're not actually storing
// and factory defaults, I'm not 100% sure this is needed

- (void) registerDefaultPreferences
{
    NSDictionary *dictionary;

    dictionary = [NSDictionary dictionary];

    [[NSUserDefaults standardUserDefaults] registerDefaults: dictionary];

} // registerDefaultPreferences



// get ourselves setup with non-UI setup

- (id) init
{
    if (self = [super init]) {
	[self registerDefaultPreferences];

	// this will automatically load the notes from the
	// user preferences
	notes = [[BWBorkPadModel alloc] init];

	// start off on page 1
	currentNote = 0;

	// set up listeners for notifications we're interested in

	[[NSNotificationCenter defaultCenter] 
	    addObserver: self
	    selector: @selector(swoopCompleted:)
	    name: BWSwoopViewCompletedNotification
	    object: nil];

	[[NSNotificationCenter defaultCenter]
	    addObserver: self
	    selector: @selector(fontChangeNotification:)
	    name: BWBorkPadPrefControllerFontChangeNotification
	    object: nil];
    }

    return (self);

} // init



// setup context help, where the user holds down the help key and gets
// some help that is between just a ToolTip thingie and having to lug
// out the HelpViewer.  Thanks to Vermont Recipes for this

- (void) setupContextHelp
{
    NSHelpManager *helpManager;

    helpManager = [NSHelpManager sharedHelpManager];

    [helpManager 
	setContextHelp: [[NSBundle mainBundle] 
			    contextHelpForKey: @"textviewContextHelp.rtfd"]
	forObject: textView];
    
    [helpManager
	setContextHelp: [[NSBundle mainBundle] 
			    contextHelpForKey: @"pagecontrolContextHelp.rtfd"]
	forObject: pageControl];

    [helpManager
	setContextHelp: [[NSBundle mainBundle] 
			    contextHelpForKey: @"pagecontrolContextHelp.rtfd"]
	forObject: gotoNoteShortcutButton];
    
} // setupContextHelp



// do any post-nib-loading initialization

- (void) awakeFromNib
{
    NSString *familyName;
    NSNumber *size;

    // get the font family and size from user defaults

    familyName = [[NSUserDefaults standardUserDefaults]
		     objectForKey: BWBorkPadDefaultsFontFamilyKey];
    size = [[NSUserDefaults standardUserDefaults]
	       objectForKey: BWBorkPadDefaultsFontSizeKey];

    // if they actually exist, adopt the look

    if (familyName && [size intValue] > 0) {
	NSFont *font;
	font = [[NSFontManager sharedFontManager] fontWithFamily: familyName
						  traits: 0
						  weight: 5
						  size: [size intValue]];
	[textView setFont: font];
    }

    // set the first note text
    [textView setString: [notes noteAtIndex: 0]];

    // save our window layout like a good citizen
    [window setFrameAutosaveName: @"BorkPad Note Window Location"];

    [self setupContextHelp];

} // awakeFromNib



// clean up our mess

- (void) dealloc
{
    [notes release];
    [prefController release];
    [findText release];

} // dealloc



// the user is moving on to another note (or something).  Take any edits
// from the text view and stick them back into the model

- (void) commitCurrentNote
{
    NSString *string;

    string = [[NSString alloc] initWithString:[textView string]];

    // this does a save to disk for us
    [notes replaceNoteAtIndex: currentNote  withString: string];

    // clear the undo actions, otherwise the textView gets really confused
    // when moving to another note
    [[textView undoManager] removeAllActions];

} // commmitCurrentNote



// switch to a note.  The 'andCommit' flag tells us whether to save
// a note before moving on.  It's just a slight optimiztion to keep ourselves
// from committing changes to a a note that's being deleted

- (void) moveToNote: (int) index  andCommit: (BOOL) commit
{
    // make sure the index is in range
    if ((index >= 0) && (index < [notes count])) {

	if (commit) {
	    // just in case, save any changes
	    // if this note hasn't changed, depend on the BWBorkPad class
	    // to not do unnecessary saving
	    [self commitCurrentNote];
	}

	// move to the indicated note
	currentNote = index;

	// update the UI
	[pageControl setPageNumber: index + 1];
	[textView setString: [notes noteAtIndex: currentNote]];

	// turn on the text field.  It probably got turned off
	// by the swooping effect
	[window makeFirstResponder: textView];

	// scroll to the bottom
	NSRange range = { [[textView string] length], 0 };
	[textView setSelectedRange: range];
	[textView scrollRangeToVisible: range];
    }

} // moveToNote



// notification method from the BWSwoopView that we're done swooping.
// Move to the note that we recorded earlier in 'postSwoopDelta'

- (void) swoopCompleted: (NSNotification *) notification
{
    [self moveToNote: currentNote + postSwoopDelta  andCommit: YES];

    postSwoopDelta = 0; // no longer swooping

    [g_clearWindow close];

} // swoopCompleted



// start the swooping happening.
// the detla is the number of pages to move forward or backward from
// the current item.

- (void) startSwoopWithDelta: (int) delta  swoopForwards: (BOOL) forwards
{
    if (delta != 0) {

	// do any disk access before starting the swoop animation
	[self commitCurrentNote];

	// make the clear window
	g_clearWindow = [[NSWindow alloc]
			  initWithContentRect: [window frame]
			  styleMask: NSBorderlessWindowMask
			  backing: NSBackingStoreBuffered
			  defer: NO];
	[g_clearWindow setLevel: NSPopUpMenuWindowLevel];

	// make it see-through
	[g_clearWindow setOpaque: NO];
	[g_clearWindow setBackgroundColor: [NSColor clearColor]];

	// record how far to move once the swoop's done
	postSwoopDelta = delta;

	// turn off the text field
	[window makeFirstResponder: nil];

	// figure out how big to make the swoop view
	NSRect rect;
	rect = [[window contentView] bounds];

	// avoid the 'pieces of paper' pinstripes at the bottom
	rect.size.height -= 8;
	rect.origin.y += 8;
	rect.size.width -= 16; // avoid scroll bar

	// (finally!) make the view that will animate the swoop effect
	BWSwoopView *view;
	view = [[BWSwoopView alloc] initWithFrame: rect
				    swoopForwards: forwards];

	[g_clearWindow setContentView: view];

	[view setNeedsDisplay: YES];
	[view release];

	// show the window
	[g_clearWindow orderFront: nil];

	// sure is a lot of work for a cheesy little effect!
    }

} // startSwoopWithDelta



/*
 * ----- Delegate Methods
 */

// a BWPageControl delegate method, triggered when the user wants to page
// forward (to a higher numbered page)

- (void) pageForward
{
    // if we're swooping, ignore any control notifications

    if (postSwoopDelta == 0) { // don't page if were swooping
	if (currentNote < ([notes count] - 1)) {
	    [self startSwoopWithDelta: 1  swoopForwards: YES];
	}
    }

} // pageForward



// a BWPageControl delegate method, triggered when the user wants to page
// backward (to a lower numbered page)

- (void) pageBackward
{
    if (postSwoopDelta == 0) { // don't page if we're swooping
	if (currentNote > 0) {
	    [self startSwoopWithDelta: -1  swoopForwards: NO];
	}
    }

} // pageBackward


// an NSApplication delegate, to know when the app is quitting.  Use the opportunity
// to save notes

- (NSApplicationTerminateReply) applicationShouldTerminate: (NSApplication *) app
{
    [self commitCurrentNote];

    if (![notes saveNotes]) {
	NSLog (@"%@",
	       NSLocalizedString (@"couldn't save notes!  ack!",
				  @"can't save nslog"));
	return (NSTerminateNow);
    } else {
	return (NSTerminateNow);
    }

} // applicationShouldTerminate



// the user closed the BorkPad window.  We intentionally only support
// one window, so this is a way of the user telling us to "go away"

- (BOOL) windowShouldClose: (id) sender
{
    // if we're getting this, the user closed the pad window
    [self commitCurrentNote];
    [NSApp stop: nil];

    return (YES);

} // windowShouldClose



/*
 * ----- Notification methods
 */


// the pref controller sends this out when the user has selected a different
// font face or size.  Update the textview accordingly

- (void) fontChangeNotification: (NSNotification *) notification
{
    NSDictionary *info;
    NSFont *font;
    NSNumber *boolnumber;

    info = [notification userInfo];
    font = [info objectForKey:BWBorkPadPrefFontKey];
    boolnumber = [info objectForKey: BWBorkPadPrefPermanentKey];

    [textView setFont: font];

    // make this permanent? or are they just test-driving
    if ([boolnumber boolValue]) {

	// make permanent
	[[NSUserDefaults standardUserDefaults]
	    setObject: [font familyName]
	    forKey: BWBorkPadDefaultsFontFamilyKey];

	[[NSUserDefaults standardUserDefaults]
	    setObject: [NSNumber numberWithInt: (int)[font pointSize]]
	    forKey: BWBorkPadDefaultsFontSizeKey];
	
	[[NSUserDefaults standardUserDefaults] 
	    synchronize];
    }

} // fontChangeNotification



/*
 * ----- Action methods
 */


// add a new (blank) note and move to it

- (IBAction) newNote: (id) sender
{
    // stick at end, with a swoosh
    [notes addNote];

    [self startSwoopWithDelta: [notes count] - currentNote - 1
	  swoopForwards: YES];

} // newNote



// open the preferences window

- (IBAction) openPrefs: (id) sender
{
    if (prefController == nil) {
	prefController = [[BWBorkPadPrefController alloc] init];
    }

    // tell it what we're showing now
    [prefController setFont: [textView font]];

    // show that pupper
    [prefController showWindow: nil];

} // openPrefs



// sheet notification for deleteNote:

- (void) deleteSheetDidEnd: (NSWindow *) sheet  
		returnCode: (int) returnCode
	       contextInfo: (void *) contextInfo
{
    if (returnCode == NSAlertDefaultReturn) {
	[notes deleteNoteAtIndex: currentNote];

	// must have at least one note, so create a new empty one
	if ([notes count] == 0) {
	    [notes addNote];
	}

	// make sure the currentNote is still in a sensible range
	currentNote = MIN(currentNote - 1, [notes count] - 1);
	currentNote = MAX(currentNote, 0);

	// definitely don't want to commit what's in the textfield
	// back into the note that now lives at currentNote

	[self moveToNote: currentNote  andCommit: NO];

    } else if (returnCode == NSAlertAlternateReturn) {
	// no boom today
    }

} // deleteEndSelector



// action for 'delete note'.  Put up a sheet confirming the deletion

- (IBAction) deleteNote: (id) sender
{
    NSBeginAlertSheet (NSLocalizedString(@"Really delete this note?", 
					 @"note deletion confirmation"),
		       NSLocalizedString(@"Delete", 
					 @"note delete button label"),
		       NSLocalizedString(@"Cancel", 
					 @"cancel note deletion label"),
		       nil, // other button
		       window,
		       self,
		       @selector(deleteSheetDidEnd:returnCode:contextInfo:),
		       NULL, // didDismissSelector
		       NULL, // contextInfo
		       NSLocalizedString(@"removal explanation",
					 @"removing not undoable"));
    
} // deleteNote



// provide a save option so folks can save their note's data.
// more of a paranoia thing

- (IBAction) save: (id) sender
{
    [self commitCurrentNote];
    [notes saveNotes];
} // save



// erase the text view's selection

- (IBAction) clear: (id) sender
{
    NSRange selectedRange;
    
    selectedRange = [textView selectedRange];

    [textView delete: nil];

} // clear



/*
 * ----- Go To Note handling
 */

// the action that triggers the goto note.  This brings down a sheet
// to ask the user what note to go to.

- (IBAction) gotoNote: (id) sender
{
    if (gotoNoteSheet == nil) {
	[NSBundle loadNibNamed: @"GotoNote.nib"  owner: self];

	// the 'ok' button is disabled if the entered page number is invalid
	// or blank.  Start it off disabled since it's blank.  There is a textfield
	// delegate that enables and disables the button as the user manipulates
	// text there
	[gotoNoteOk setEnabled: NO];
    }

    // populate one of the static text fields saying how many
    // notes we have
    [gotoNoteNoteCount setStringValue:
			   [NSString stringWithFormat:
					 NSLocalizedString (@"There are %d notes", 
							    @"gotoNote note count"),
				     [notes count]]];
    // start the sheet
    [NSApp beginSheet: gotoNoteSheet
	   modalForWindow: window
	   modalDelegate: self
	   didEndSelector: nil // actual work is done in some button actions
	   contextInfo: nil];

} // gotoNote



// action hooked up to the 'goto note' sheet.  Dismiss the sheet and not 
// go anywhere

- (IBAction) cancelGotoSheet: (id) sender
{
    [NSApp endSheet: gotoNoteSheet];
    [gotoNoteSheet close];

} // cancelGotoSheet



// action hooked up to the 'goto note' sheet.
// the user entered in a numbe that passed text field validation
// switch to that note

- (IBAction) acceptGotoSheet: (id) sender
{
    [self moveToNote: [gotoNoteField intValue] - 1
	  andCommit: YES];

    [NSApp endSheet: gotoNoteSheet];

    [gotoNoteSheet close];

} // acceptGotoSheet



/*
 * ----- 'Find' sheet handling
 */

// start a find action.  Actually, it just drops a sheet that the user can
// fill in.  There are IBActions for the buttons on that sheet

- (IBAction) find: (id) sender
{
    if (findSheet == nil) {
	[NSBundle loadNibNamed: @"Find.nib"  owner:self];

	// disable the OK button to start out with.  a delegate on the textfield
	// will enable it when there's text to find
	[findOk setEnabled: NO];
    }

    // start the text
    [NSApp beginSheet: findSheet
	   modalForWindow: window
	   modalDelegate: self
	   didEndSelector: nil // actual work is done in some button actions
	   contextInfo: nil];

} //  find



// do another find based on information already supplied by the user

- (IBAction) findAgain: (id) sender
{
    // if the user changed notes, clear out the lastFind so that we'll
    // start from the top of the current note

    if (lastFind.noteNumber != currentNote) {
	lastFind.noteNumber = currentNote;
	lastFind.foundRange = NSMakeRange(0,0);
    } else {
	lastFind.foundRange = [textView selectedRange];
    }

    lastFind = [notes findString: findText
		      startingAt: lastFind
		      justOneNote: !findInAllNotes];
    
    // if the search isn't empty
    // then go to the note and highlight the range

    if (lastFind.foundRange.location == NSNotFound) {
	NSBeep ();  // agent '\007'
	
    } else {
	[self moveToNote: lastFind.noteNumber  andCommit: YES];
	[textView setSelectedRange: lastFind.foundRange];
	[textView scrollRangeToVisible: lastFind.foundRange];
    }

} //  findAgain



// action hooked up to the 'find' sheet.  Dismiss it without doing any searches

- (IBAction) cancelFindSheet: (id) sender
{
    [NSApp endSheet: findSheet];
    [findSheet close];
    
} // cancelFindSheet



// action hooked up to the 'find' sheet OK button

- (IBAction) acceptFindSheet: (id) sender
{
    // get the various stuffage the user entered

    // like the text to look for
    [findText release];
    findText = [[findField stringValue] copy];

    // and whether we should look in all notes.
    // the logic here is based on the expectation that the 'all notes'
    // radio button has a non-zero tag, and 'just in this note' has a
    // zero tag
    findInAllNotes = [[findRadioMatrix selectedCell] tag];

    // close the sheet before finding
    [NSApp endSheet: findSheet];
    [findSheet close];

    // start from the top.  
    lastFind = BWZeroResult;
    // force findAgain to start at the top of the current note
    lastFind.noteNumber = -1; 

    // if find in just this sheet, set the noteNumber to the current one
    [self findAgain: nil];

} // acceptFindSheet



/*
 * ----- random stuff
 */

// text field delegate from the 'goto note' sheet and the 'find' sheet

- (void) controlTextDidChange: (NSNotification *) notification
{
    // enable the OK button if the entered number is in a valid
    // range for the goto note sheet.  disable it otherwise
    if ([notification object] == gotoNoteField) {
	NSString *currentString;
	int noteNumber;

	currentString 
	    = [[[notification userInfo] objectForKey: @"NSFieldEditor"] string];

	noteNumber = [currentString intValue];

	if (noteNumber == 0 || noteNumber > [notes count]) {
	    [gotoNoteOk setEnabled: NO];
	} else {
	    [gotoNoteOk setEnabled: YES];
	}

    // disable the OK button if the 'find' text field is empty.  enable it otherwise
    } else if ([notification object] == findField) {
	NSString *currentString;

	currentString 
	    = [[[notification userInfo] objectForKey: @"NSFieldEditor"] string];

	if ([currentString length] == 0) {
	    [findOk setEnabled: NO];
	} else {
	    [findOk setEnabled: YES];
	}
    }

} // controlTextDidChange



// enable / disable some items based on the current state of the world

- (BOOL) validateMenuItem: (id <NSMenuItem>) menuItem
{
    int result = YES;

    // don't allow deletion if we only have one note
    if ([menuItem action] == @selector(deleteNote:)) {
	if ([notes count] == 1) {
	    result = NO; // can't delete the last note
	}
	
    // can't goto a note if we only have one note
    } else if ([menuItem action] == @selector(gotoNote:)) {
	if ([notes count] == 1) {
	    result = NO; // can't go to a different not if only one
	}

    // can't find again if we haven't done a Find the first time
    } else if ([menuItem action] == @selector(findAgain:)) {
	if (findText == nil) {
	    result = NO;
	}

    // can't clear if there's no selection in the text view
    } else if ([menuItem action] == @selector(clear:)) {
	NSRange selectedRange;
	selectedRange = [textView selectedRange];
	if (selectedRange.length == 0) {
	    result = NO;
	}
    
    // don't let folks add new notes if a swoop is in action
    } else if ([menuItem action] == @selector(newNote:)) {
	if (postSwoopDelta != 0) {
	    result = NO;
	}
    }
    
    return (result);

} // validateMenuItem


@end // BWBorkPadController

/* $Id: BWBorkPadController.m,v 1.18 2003/01/03 21:29:56 markd Exp $ */
