MBCGameInfo.mm   [plain text]


/*
	File:		MBCGameInfo.mm
	Contains:	Managing information about the current game
	Version:	1.0
	Copyright:	 2003 by Apple Computer, Inc., all rights reserved.

	File Ownership:

		DRI:				Matthias Neeracher    x43683

	Writers:

		(MN)	Matthias Neeracher

	Change History (most recent first):

		$Log: MBCGameInfo.mm,v $
		Revision 1.13  2009/04/22 23:19:47  neerache
		<rdar://problem/6815838> Chess crashes when editing Game Info from Game Log window
		
		Revision 1.12  2008/10/24 22:45:45  neerache
		<rdar://problem/5844722> Chess: black may illegally move first in new game
		
		Revision 1.11  2008/08/19 20:24:28  neerache
		<rdar://problem/6159904> 10A149: Chess Crashes using Foundation-706
		
		Revision 1.10  2008/04/22 18:40:55  neerache
		Merge late Leopard changes into trunk
		
		Revision 1.9.2.1  2007/05/18 20:36:37  neerache
		Properly hook up board to game info <rdar://problem/3852844>
		
		Revision 1.9  2007/03/02 07:40:46  neerache
		Revise document handling & saving <rdar://problems/3776337&4186113>
		
		Revision 1.8  2007/01/17 05:40:45  neerache
		Defer title updates <rdar://problem/3852824>
		
		Revision 1.7  2007/01/16 08:28:45  neerache
		Don't mess with nil strings
		
		Revision 1.6  2006/05/19 21:09:32  neerache
		Fix 64 bit compilation errors
		
		Revision 1.5  2003/07/25 22:05:21  neerache
		Dismiss edit window properly (RADAR 3343292)
		
		Revision 1.4  2003/07/03 05:35:28  neerache
		Add tooltips, tweak game info window
		
		Revision 1.3  2003/06/12 07:27:10  neerache
		Reorganize preferences window, Add simpler title style
		
		Revision 1.2  2003/05/27 03:13:57  neerache
		Rework game loading/saving code
		
		Revision 1.1  2003/05/24 20:29:25  neerache
		Add Game Info Window
		
*/

#import "MBCGameInfo.h"
#import "MBCController.h"
#import "MBCPlayer.h"

#include <sys/types.h>
#include <regex.h>
#include <algorithm>

static NSTextTab * MakeTab(NSTextTabType type, float location)
{
	return [[[NSTextTab alloc] initWithType:type location:location] 
			   autorelease];
}

@implementation MBCGameInfo

NSString * kMBCGameCity			= @"MBCGameCity";
NSString * kMBCGameCountry		= @"MBCGameCountry";
NSString * kMBCHumanFirst		= @"MBCHumanFirst";
NSString * kMBCHumanLast		= @"MBCHumanLast";
NSString * kMBCGameEvent		= @"MBCGameEvent";
NSString * kMBCShowMoveInTitle 	= @"MBCShowMoveInTitle";

+ (void) parseName:(NSString *)fullName intoFirst:(NSString **)firstName
			  last:(NSString **)lastName
{
	// 
	// Get name as UTF8. If the name is longer than 99 bytes, the last
	// character might be bad if it's non-ASCII. What sane person would
	// have such a long name anyway?
	//
	char	   n[100];
	strlcpy(n, [fullName UTF8String], 100);
	
	char * first 	= n+strspn(n, " \t"); 	// Beginning of first name
	char * last;
	char * nb1  	= NULL;			 		// Beginning of last word
	char * ne1  	= NULL;			 		// End of last word
	char * nb2  	= NULL;			 		// Beginning of last but one word
	char * ne2  	= NULL;			 		// End of last but two word
	char * ne3  	= NULL;            		// End of last but three word

	nb1	  = first;
	ne1   = nb1+strcspn(nb1, " \t");

	for (char * n; (n = ne1+strspn(ne1, " \t")) && *n; ) {
		ne3	= ne2; 
		nb2 = nb1;
		ne2 = ne1;
		nb1	= n;
		ne1 = nb1+strcspn(nb1, " \t");
	}

	if (ne3 && *nb2 >= 'a' && *nb2 <= 'z') { 
		//
		// Name has at least 3 words and last but one is 
		// lowercase, as in Ludwig van Beethoven
		//
		last = nb2;
		*ne3 = 0;
	} else if (ne2) {
		//
		// Name has at least two words. If 3 or more, last but one is 
		// uppercase as in John Wayne Miller
		//
		last = nb1;
		*ne2 = 0;
	} else {		// Name is single word
		last = ne1;
		*ne1 = 0;
	}
	*firstName	= [NSString stringWithUTF8String:first];
	*lastName	= [NSString stringWithUTF8String:last];
}

+ (void)initialize
{
	//
	// Parse the user name into last and first name
	//
	NSString * humanFirst;
	NSString * humanLast;

	[MBCGameInfo parseName:NSFullUserName() 
				 intoFirst:&humanFirst last:&humanLast];

	// 
	// Get the city we might be in. This technique may not necessarily
	// work in future revisions of Mac OS X, I suppose.
	//
	// PGN wants IOC codes for countries, which we're too lazy to convert.
	//
	NSArray *			cityInfo = 
		(NSArray *)CFPreferencesCopyValue((CFStringRef)
					@"com.apple.TimeZonePref.Last_Selected_City",
					kCFPreferencesAnyApplication, kCFPreferencesAnyUser,
					kCFPreferencesCurrentHost);
	NSString *			city 	= cityInfo ? [cityInfo objectAtIndex:7] : @"?";
	NSString *			country	= cityInfo ? [cityInfo objectAtIndex:8] : @"?";
	
	//
	// PGN wants ISO Latin 1, so we fall back to the non-Localized Name 
	// for non-Latin places.
	//
	if (![city canBeConvertedToEncoding:NSISOLatin1StringEncoding])
		city	= [cityInfo objectAtIndex:5];
	if (![country canBeConvertedToEncoding:NSISOLatin1StringEncoding])
		country	= [cityInfo objectAtIndex:6];

	if (cityInfo)
		[cityInfo release];

	NSString * event = 
		[NSLocalizedString(@"casual_game", @"casual game") retain];

	NSDictionary * defaults = 
		[NSDictionary 
			dictionaryWithObjectsAndKeys:
				humanFirst, kMBCHumanFirst,
				 humanLast, kMBCHumanLast,
			          city, kMBCGameCity,
   			       country, kMBCGameCountry,
			         event, kMBCGameEvent,
			nil];
	[[NSUserDefaults standardUserDefaults] registerDefaults: defaults];
}


- (id) init
{
	[[NSNotificationCenter defaultCenter] 
		addObserver:self
		selector:@selector(updateMoves:)
		name:MBCEndMoveNotification
		object:nil];
	[[NSNotificationCenter defaultCenter] 
		addObserver:self
		selector:@selector(takeback:)
		name:MBCTakebackNotification
		object:nil];
	[[NSNotificationCenter defaultCenter] 
		addObserver:self
		selector:@selector(updateMoves:)
		name:MBCIllegalMoveNotification
		object:nil];
	[[NSNotificationCenter defaultCenter] 
		addObserver:self
		selector:@selector(gameEnd:)
		name:MBCGameEndNotification
		object:nil];

	fOutcome	= nil;
	fWhiteName	= nil;
	fBlackName	= nil;
	fSetInfo	= false;

	return self;
}

- (void)awakeFromNib
{
	NSUserDefaults *defaults 	= [NSUserDefaults standardUserDefaults];

	[fShowMoveInTitle setIntValue:[defaults boolForKey:kMBCShowMoveInTitle]];

	fRows	= 0;
	fBoard	= [[MBCController controller] board];
}

- (void) updateMoves:(NSNotification *)notification
{
	[fMoveList reloadData];
	[fMoveList setNeedsDisplay:YES];
	fTitleNeedsUpdate = true;

	int rows = [self numberOfRowsInTableView:fMoveList]; 
	if (rows != fRows) {
		fRows = rows;
		[fMoveList selectRow:rows-1 byExtendingSelection:NO];
		[fMoveList scrollRowToVisible:rows-1];
	}
}

- (void) takeback:(NSNotification *)notification
{
	[fOutcome release];
	fOutcome 	= nil;
	fResult		= [@"*" retain];
	[self updateMoves:notification];
}

- (void) gameEnd:(NSNotification *)notification
{
	MBCMove *    move 	= reinterpret_cast<MBCMove *>([notification object]);

	switch (move->fCommand) {
	case kCmdWhiteWins:
		fResult		= @"1-0";
		fOutcome	= NSLocalizedString(@"white_win_msg", @"White wins");
		break;
	case kCmdBlackWins:
		fResult		= @"0-1";
		fOutcome 	= NSLocalizedString(@"black_win_msg", @"Black wins");
		break;
	case kCmdDraw:
		fResult		= @"1/2-1/2";
		fOutcome 	= NSLocalizedString(@"draw_msg", @"Draw");
		break;
	default:
		return;
	}
	[fResult retain];
	[fOutcome retain];
	[self updateTitle:nil];
}

- (NSDictionary *)getInfo
{
	NSUserDefaults *defaults 	= [NSUserDefaults standardUserDefaults];

	return [NSDictionary dictionaryWithObjectsAndKeys:
							 fWhiteName, @"White",
						 fBlackName, @"Black",
						 fStartDate, @"StartDate",
						 fStartTime, @"StartTime",
						 fResult, 	 @"Result",
						 [defaults stringForKey:kMBCGameCity], 	  @"City",
						 [defaults stringForKey:kMBCGameCountry], @"Country",
						 [defaults stringForKey:kMBCGameEvent],   @"Event",
						 nil];
}

- (void)setInfo:(NSDictionary *)dict
{
	NSUserDefaults *defaults 	= [NSUserDefaults standardUserDefaults];
	NSString * 		human		=
		[NSString stringWithFormat:@"%@ %@",
				  [defaults stringForKey:kMBCHumanFirst],
				  [defaults stringForKey:kMBCHumanLast]];
	NSString * 		engine 		= @"Computer";

	fSetInfo	= true;
	[fWhiteName release];
	[fBlackName release];
	if (NSString * white = [dict objectForKey:@"White"])
		fWhiteName 	= [white retain];
	else if ([[dict objectForKey:@"WhiteType"] isEqual:kMBCHumanPlayer])
		fWhiteName 	= [human retain];
	else
		fWhiteName 	= [engine retain];
	if (NSString * black = [dict objectForKey:@"Black"])
		fBlackName 	= [black retain];
	else if ([[dict objectForKey:@"BlackType"] isEqual:kMBCHumanPlayer])
		fBlackName 	= [human retain];
	else
		fBlackName 	= [engine retain];
	fStartDate = [[dict objectForKey:@"StartDate"] retain];
	fStartTime = [[dict objectForKey:@"StartTime"] retain];
	fResult	   = [[dict objectForKey:@"Result"] retain];
	if (NSString * city = [dict objectForKey:@"City"])
		[defaults setObject:city forKey:kMBCGameCity];
	if (NSString * country = [dict objectForKey:@"Country"])
		[defaults setObject:country forKey:kMBCGameCountry];
	if (NSString * event = [dict objectForKey:@"Event"])
		[defaults setObject:event forKey:kMBCGameEvent];
		
	[fOutcome release];
	fOutcome = nil;
	if ([fResult isEqual:@"1-0"])
		fOutcome	= NSLocalizedString(@"white_win_msg", @"White wins");
	if ([fResult isEqual:@"0-1"])
		fOutcome 	= NSLocalizedString(@"black_win_msg", @"Black wins");
	else if ([fResult isEqual:@"1/2-1/2"]) 
		fOutcome 	= NSLocalizedString(@"draw_msg", @"Draw");
	[fOutcome retain];
}

- (void) startGame:(MBCVariant)variant playing:(MBCSide)sideToPlay
{
	[NSObject cancelPreviousPerformRequestsWithTarget:self];
	if (!fSetInfo) {
		NSUserDefaults *defaults 	= [NSUserDefaults standardUserDefaults];
		NSString * 		human		=
			[NSString stringWithFormat:@"%@ %@",
					  [defaults stringForKey:kMBCHumanFirst],
					  [defaults stringForKey:kMBCHumanLast]];
		NSString * 		engine 		= @"Computer";

		[fWhiteName release];
		[fBlackName release];
		switch (fHuman = fSideToPlay = sideToPlay) {
		case kWhiteSide:
			fWhiteName 	= [human retain];
			fBlackName 	= [engine retain];
			break;
		case kBlackSide:
			fWhiteName 	= [engine retain];
			fBlackName 	= [human retain];
			break;
		case kBothSides:
			fWhiteName 	= [human retain];
			fBlackName 	= [human retain];
			fHuman	   	= kWhiteSide;
			break;
		case kNeitherSide:
			fWhiteName 	= [engine retain];
			fBlackName 	= [engine retain];
			break;
		}
		NSDate * now	= [NSDate date];
		fStartDate		= [[now descriptionWithCalendarFormat:@"%Y.%m.%d"
								timeZone:nil locale:nil] retain];
		fStartTime		= [[now descriptionWithCalendarFormat:@"%H:%M:%S"
								timeZone:nil locale:nil] retain];
		fResult			= [@"*" retain];
		[fOutcome release];
		fOutcome = nil;
	} else
		fSetInfo	= false;

	[self updateMoves:nil];
	[self updateTitle:nil];
}

- (IBAction) editInfo:(id)sender
{
	NSUserDefaults * 	defaults 	= [NSUserDefaults standardUserDefaults];
	[fWhite setStringValue:fWhiteName];
	[fBlack setStringValue:fBlackName];
	[fCity setStringValue:[defaults stringForKey:kMBCGameCity]];
	[fCountry setStringValue:[defaults stringForKey:kMBCGameCountry]];
	[fEvent setStringValue:[defaults stringForKey:kMBCGameEvent]];

	switch (fSideToPlay) {
	case kWhiteSide:
		[fWhite setEditable:YES];
		[fBlack setSelectable:NO];
		break;
	case kBlackSide:
		[fWhite setSelectable:NO];
		[fBlack setEditable:YES];
		break;
	case kBothSides:
		[fWhite setEditable:YES];
		[fBlack setEditable:YES];
		break;
	case kNeitherSide:
		[fWhite setSelectable:NO];
		[fBlack setSelectable:NO];
		break;
	}

	[NSApp beginSheet:fEditSheet
		   modalForWindow:fInfoWindow
		   modalDelegate:nil
		   didEndSelector:nil
		   contextInfo:nil];
    [NSApp runModalForWindow:fEditSheet];
	[NSApp endSheet:fEditSheet];
	[fEditSheet orderOut:self];
	[fInfoWindow makeKeyAndOrderFront:self];
}

- (IBAction) cancelInfo:(id)sender
{
	[NSApp stopModal];
}

- (IBAction) updateInfo:(id)sender
{
	NSUserDefaults * 	defaults 	= [NSUserDefaults standardUserDefaults];
	NSString *			human;

	fWhiteName = [[fWhite stringValue] retain];
	fBlackName = [[fBlack stringValue] retain];

	if (fHuman == kWhiteSide) 
		human = fWhiteName;
	else if (fHuman == kBlackSide) 
		human = fBlackName;
	else 
		human = nil; // Both or neither are humans, no default to learn here

	if (human) {
		NSString *	humanFirst;
		NSString * 	humanLast;

		[MBCGameInfo parseName:human
					 intoFirst:&humanFirst last:&humanLast];
		[defaults setObject:humanFirst forKey:kMBCHumanFirst];
		[defaults setObject:humanLast forKey:kMBCHumanLast];
	}

	[defaults setObject:[fCity stringValue] forKey:kMBCGameCity];
	[defaults setObject:[fCountry stringValue] forKey:kMBCGameCountry];
	[defaults setObject:[fEvent stringValue] forKey:kMBCGameEvent];

	[self updateTitle:nil];
	[NSApp stopModal];
}

- (int)numberOfRowsInTableView:(NSTableView *)aTableView
{
	return ([fBoard numMoves]+1) / 2;
}

- (id)tableView:(NSTableView *)v objectValueForTableColumn:(NSTableColumn *)col row:(int)row
{
	//
	// Defer title update until table gets redrawn
	//
	if (fTitleNeedsUpdate) {
		[self performSelector:@selector(updateTitle:) withObject:nil
			  afterDelay:0.01];
		fTitleNeedsUpdate = false;
	}

	NSString * 		ident 	= [col identifier];
	if ([ident isEqual:@"Move"])
		return [NSString stringWithFormat:@"%d.", row+1];
	
	NSString * move = [[fBoard move: row*2+[ident isEqual:@"Black"]] 
						  localizedText:YES];
	if (!move)
		return nil;
	NSMutableParagraphStyle * style = 
		[[[NSParagraphStyle defaultParagraphStyle] mutableCopy] autorelease];
	float tab   = [col width] / 14.0f;
	[style setTabStops:
			   [NSArray arrayWithObjects:
							[move characterAtIndex:1]=='\t' 
						? MakeTab(NSRightTabStopType, 1.0f) 
						: MakeTab(NSRightTabStopType, 5.0f*tab),
						MakeTab(NSCenterTabStopType, 6.0f*tab),
						MakeTab(NSLeftTabStopType, 7.0f*tab),
						nil]];
	return [[[NSAttributedString alloc]
				initWithString:move attributes:
					[NSDictionary dictionaryWithObject:style
								  forKey:NSParagraphStyleAttributeName]]
			   autorelease];
}

- (IBAction) updateTitle:(id)sender
{
	NSString * 		move;
	int		 		numMoves = [fBoard numMoves];		

	if (sender) {
		NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];

		[defaults setBool:
					  [fShowMoveInTitle intValue] forKey:kMBCShowMoveInTitle];
	}
	
	if (numMoves && [fShowMoveInTitle intValue])
		move = 	[NSString stringWithFormat:@"%d. %@%@",
						  (numMoves+1)/2, numMoves&1 ? @"":@"... ",
						  [[fBoard lastMove] localizedText:NO]];
	else if (!numMoves)
		move =	NSLocalizedString(@"new_msg", @"New Game");
	else if (numMoves & 1)
		move = 	NSLocalizedString(@"black_move_msg", @"Black to move");
	else
		move = 	NSLocalizedString(@"white_move_msg", @"White to move");

	NSString * title =
		[NSString stringWithFormat:@"%@ - %@   (%@)", 
				  fWhiteName, fBlackName, move];
	if (fOutcome)
		title = [NSString stringWithFormat:@"%@   %@", title, fOutcome];
	[fMainWindow setTitle:title];
	unichar emdash = 0x2014;
	[fMatchup setStringValue:
				  [NSString stringWithFormat:@"%@\n%@\n%@", 
							fWhiteName, 
							[NSString stringWithCharacters:&emdash length:1],
							fBlackName]];
	fTitleNeedsUpdate = false;
}

- (NSSize)windowWillResize:(NSWindow *)sender toSize:(NSSize)proposedFrameSize
{
	[fMoveList setNeedsDisplay:YES];

	return proposedFrameSize;
}

- (NSString *)pgnHeader
{
	NSUserDefaults *defaults 	= [NSUserDefaults standardUserDefaults];
	NSString * 		wf;
	NSString *		wl;
	NSString *		bf;
	NSString * 		bl;
	[MBCGameInfo parseName:fWhiteName intoFirst:&wf last:&wl];
	[MBCGameInfo parseName:fBlackName intoFirst:&bf last:&bl];
	NSString *		humanw	=
		[NSString stringWithFormat:@"%@, %@", wl, wf];
	NSString *		humanb	=
		[NSString stringWithFormat:@"%@, %@", bl, bf];
	NSString * 		engine 		= 
		[NSString stringWithFormat:@"Apple Chess %@",
				  [[NSBundle mainBundle] 
					  objectForInfoDictionaryKey:@"CFBundleVersion"]];
	NSString * white;
	NSString * black;
	switch (fSideToPlay) {
	case kWhiteSide:
		white 	= humanw;
		black 	= engine;
		break;
	case kBlackSide:
		white 	= engine;
		black	= humanb;
		break;
	case kBothSides:
		white	= humanw;
		black	= humanb;
		
		break;
	case kNeitherSide:
		white	= engine;
		black	= engine;
		break;
	}

	//
	// PGN uses a standard format that is NOT localized
	//
	NSString * format = 
		[NSString stringWithCString:
				  "[Event \"%@\"]\n"
				  "[Site \"%@, %@\"]\n"
				  "[Date \"%@\"]\n"
				  "[Round \"-\"]\n"
				  "[White \"%@\"]\n"
				  "[Black \"%@\"]\n"
				  "[Result \"%@\"]\n"
				  "[Time \"%@\"]\n"];
	return [NSString stringWithFormat:format,
					 [defaults stringForKey:kMBCGameEvent],
					 [defaults stringForKey:kMBCGameCity],
					 [defaults stringForKey:kMBCGameCountry],
					 fStartDate, white, black, fResult, fStartTime];
}

- (NSString *)pgnResult
{
	return fResult;
}

@end

// Local Variables:
// mode:ObjC
// End: