This is Controller.m in view mode; [Download] [Up]
/* * Main controller for Stopwatch app. * * For legal stuff see the file COPYRIGHT */ #import <stdio.h> #import <ansi/string.h> /* for import feature only (?) */ #import <bsd/sys/param.h> /* for MAXPATHLEN */ #import <appkit/NXCType.h> #import "Controller.h" #import "StopWatch.h" #import "InfoPanel.h" #import "ClientInfo.h" #import "ClientInspector.h" #import "SessionEditor.h" #import "AppIconView.h" #import "createPath.h" #import "Preferences.h" #define PRIORITY NX_MODALRESPTHRESHOLD #define ARCHIVE_FILE "client.data" #define TEMPLATE_DIR "Templates" #define MAXCLIENTLEN 80 #define SELECTED_CLIENT "SelectedClient" #define VERSION 2 /* the current file version that gets written */ int FileVersion; /* the version of the file being read */ void freeAndCopy( char **ptr, const char *str ) { if ( *ptr ) free( *ptr ); *ptr = NXCopyStringBuffer(str); } const char * currentDate() { time_t now; struct tm *tm; static char buf[10]; time(&now); tm = localtime(&now); sprintf( buf, "%02d/%02d/%02d", tm->tm_mon + 1, tm->tm_mday, tm->tm_year ); return buf; } const char * currentTime() { time_t now; struct tm *tm; static char buf[10]; time(&now); tm = localtime(&now); sprintf( buf, "%02d:%02d", tm->tm_hour, tm->tm_min ); return buf; } #define MAX_COMMA_STR 50 /* max digits we can handle */ void commafy( int value, char *result ) { char *ptr, reverse[MAX_COMMA_STR + 1]; int digits = 0, signum = 1; if ( value < 0 ) { signum = -1; /* save the sign (and the whales) */ value = -value; /* make it positive for conversion */ } ptr = reverse; do { if ( ++digits > 3 ) { digits = 1; /* reset digit counter */ *ptr++ = ','; /* insert a comma */ } *ptr++ = '0' + value % 10; /* convert least significant digit to ascii */ value /= 10; /* and remove it from the number */ } while ( value != 0 ); /* until no digits remain */ if ( signum < 0 ) *ptr++ = '-'; /* insert the negative sign */ /* We already have a pointer to the end, so just copy backwards! */ while ( ptr-- != reverse ) *result++ = *ptr; *result = '\0'; } /* * Puts commas into a double by convering to an int, after * adjusting for rounding and calling commafy() */ void commafyDouble( double value, char *result ) { int signum = 1, cents; char buf[MAX_COMMA_STR + 1]; if ( value < 0 ) { signum = -1; value *= -1; } value *= 100; value += 0.5; /* Adjust for rounding */ cents = (int)value % 100; /* save the pennies for later */ value /= 100; /* chop off last two places */ commafy( (int)(signum < 0 ? -value : value), buf ); sprintf( result, "%s.%02d", buf, cents ); } /* * To avoid having to exec /bin/cp. */ int copyFile( const char *src, const char *dst ) { FILE *in, *out; int count; char buf[BUFSIZ]; if ( ! (in = fopen( src, "r" ) ) ) { fprintf( stderr, "Can't open `%s' for reading.\n", src ); return 0; } if ( ! (out = fopen( dst, "w" ) ) ) { fprintf( stderr, "Can't open `%s' for writing.\n", dst ); fclose(in); return 0; } while ( (count = fread( buf, sizeof(char), sizeof(buf), in )) > 0 ) fwrite( buf, sizeof(char), count, out ); fclose(in); fclose(out); return 1; } @interface Controller(PRIVATE) - (void)selectBrowserRow:(int)row; - (ClientInfo *)findClient:(const char *)name; - selectedClient; - (int)selectedRow; - (int)compare:obj1 :obj2; /* comparison method for SortList */ - initInvoice; - (void)addSession:(const char *)startDate time:(const char *)startTime duration:(int)minutes description:(const char *)desc; - (void)checkStartButton; @end @implementation Controller DPSTimedEntryProc showElapsedTime(DPSTimedEntry teNum, double now, char *data) { [(id)data showElapsedTime]; return (void *)NULL; } - (void) removeTimedEntry { if ( teNum ) { DPSRemoveTimedEntry(teNum); teNum = 0 ; } } - (void) addTimedEntry { [self removeTimedEntry]; /* in case there is one */ /* Set it up so that the clock updates every minute */ teNum = DPSAddTimedEntry( (double)60.0, (DPSTimedEntryProc)showElapsedTime, (void *)self, PRIORITY ); } - free { [self removeTimedEntry]; [stopwatch free]; [infoPanel free]; return [super free]; } - awakeFromNib { [window setFrameAutosaveName:"Stopwatch"]; /* Make the browser's font match the startButton (can't do this in IB) */ [[[browser matrixInColumn:0] prototype] setFont:[startButton font]]; return self; } - add:sender { [[ClientInspector sharedInstance] add:sender]; return self; } - modify:sender { [[ClientInspector sharedInstance] modify:sender]; return self; } - delete:sender { [[ClientInspector sharedInstance] delete:sender]; return self; } - undelete:sender { [[ClientInspector sharedInstance] undelete:sender]; return self; } - (void)enableAdd:(BOOL)flag { [addMenuItem setEnabled:flag]; } - (void)enableModify:(BOOL)flag { [modifyMenuItem setEnabled:flag]; } - (void)enableDelete:(BOOL)flag { [deleteButton setEnabled:flag]; } - (void)enableUndelete:(BOOL)flag { [undeleteButton setEnabled:flag]; } /* * Redisplay from the data in the clientList. Try to re-select the * same item afterwards. */ - (void)decacheBrowser { [browser loadColumnZero]; [self selectBrowserRow:[clientList indexOf:activeClient]]; [self checkStartButton]; } - (NXTypedStream *)openArchive:(int)mode { NXTypedStream *stream ; if ( (stream = NXOpenTypedStreamForFile( filename, mode )) == NULL ) { NXRunAlertPanel( [NXApp appName], "Unable to open client data file: `%s'", "Create it when needed", NULL, NULL, filename ); return nil; } return stream; } /* * Read in the client info from the typestream file */ - (int)loadClientInfo { NXTypedStream *stream ; if ( (stream = [self openArchive:NX_READONLY]) == nil ) return 0; NXReadType( stream, "i", &FileVersion ); [clientList read:stream]; [clientList sort]; NXCloseTypedStream(stream) ; return 1; } - (int)saveClientInfoToStream:(NXTypedStream *)stream { int version = VERSION; NXWriteType( stream, "i", &version ); [clientList write:stream]; return 1; } - (int)saveClientInfo { NXTypedStream *stream ; char backup[FILENAME_MAX + 1]; /* * If this is the first write, move the old filename to * filename~ to serve as a backup. */ if ( didBackup == NO ) { sprintf( backup, "%s~", filename ); rename( filename, backup ); didBackup = YES; } if ( (stream = [self openArchive:NX_WRITEONLY]) == nil ) return 0; [self saveClientInfoToStream:stream]; NXCloseTypedStream(stream); return 1; } /* * Edit the selected invoicing template by messaging to the * workspace to open the corresponding file. Sender is the * Matrix containing the menu of template names. */ - editTemplate:sender { id cell = [sender cellAt:[sender selectedRow] :0]; [self initInvoice]; [invoice editTemplate:[cell title]]; return self; } - preferences:sender { [preferences display]; return self; } - saveAs:sender { SavePanel *savePanel = [SavePanel new]; NXTypedStream *stream; const char *path; if ( [savePanel runModalForDirectory:dirname file:""] == 0 ) return nil; path = [savePanel filename]; if ( (stream = NXOpenTypedStreamForFile( path, NX_WRITEONLY )) == NULL ) { NXRunAlertPanel( [NXApp appName], "Unable to open file for writing: `%s'", "What the...?", NULL, NULL, path ); return nil; } [self saveClientInfoToStream:stream]; NXCloseTypedStream(stream); return self; } - clientList { return clientList; } /* * Select the client who was saved in the defaults db on our last exit. */ - (void)selectPreviousClient { const char *name = NXGetDefaultValue( [NXApp appName], SELECTED_CLIENT ); ClientInfo *client; if ( name && *name ) { client = [self findClient:name]; [self selectBrowserRow:[clientList indexOf:client]]; [self selectClient:self]; } } - appDidInit:sender { NXRect rect = {{0.0, 0.0}, {64.0, 64.0}}; if ( createPath( dirname, DIRMODE ) != PathCreationOk ) { NXRunAlertPanel( [NXApp appName], "Cannot create path `%s'", "Damned UNIX!", NULL, NULL, dirname ); [NXApp terminate:sender]; } preferences = [Preferences new]; [self loadClientInfo]; [self decacheBrowser]; [self selectPreviousClient]; if ( [preferences hideOnAutoLaunch] ) [NXApp hide:self]; else [window makeKeyAndOrderFront:self]; /* make view that tracks elapsedTime be the appIcon window's contentView */ appIconView = [[AppIconView alloc] initFrame:&rect sourceView:elapsedTimeField]; [[[NXApp appIcon] setContentView:appIconView] free]; [browser setDoubleAction:@selector(inspect:)]; [browser setTarget:self]; /* If there are no clients defined yet, disable the start buttons */ [self checkStartButton]; return self; } /* * If we logout, or there's a powerOff, make sure the time gets saved. */ - app:sender powerOffIn:(int)ms andSave:(int)aFlag { return [self appWillTerminate:sender]; } - appDidUnhide:sender { [window makeKeyAndOrderFront:self]; return self; } - appWillTerminate:sender { ClientInfo *client = [self selectedClient]; if ( teNum ) [self stopClock]; NXWriteDefault([NXApp appName], SELECTED_CLIENT, client ? [client shortName] : ""); return self; } - init { char path[FILENAME_MAX + 1]; [super init]; stopwatch = [[StopWatch alloc] init]; clientList = [[SortList alloc] init]; [clientList setAutoSort:YES]; [clientList setDelegate:self]; sprintf( path, "%s/Library/%s", NXHomeDirectory(), [NXApp appName] ); dirname = NXCopyStringBuffer(path); sprintf( path, "%s/%s", dirname, ARCHIVE_FILE ); filename = NXCopyStringBuffer(path); return self; } - (const char *) description { return [description stringValue]; } /* * Called once per minute by the timed entry routine while the clock is running. */ - showElapsedTime { [elapsedTimeField setStringValue:[stopwatch elapsedTime]]; [appIconView display]; return self; } /* * Respond to the user's selection of a client */ - selectClient:sender { /* Assume that this means we should stop the previous client */ if ( [stopwatch running] == YES ) [startButton performClick:sender]; activeClient = [self selectedClient]; [description setStringValue:[activeClient lastDescription]]; [description selectText:sender]; return self; } /* * The start button highlights, but we need to force the title to "Stop". * Setting the Alternate Title didn't seem to do the right thing in IB. */ - startClock { id font = [elapsedTimeField font]; [elapsedTimeField setFont:[[FontManager new] convertWeight:YES of:font]]; [self addTimedEntry]; [startButton setTitle:"Stop"]; [startMenuItem setTitle:"Stop"]; [stopwatch startWatch]; [self showElapsedTime]; activeClient = [self selectedClient]; return self; } /* * The mirror image of the above routine */ - stopClock { id font = [elapsedTimeField font]; [elapsedTimeField setFont:[[FontManager new] convertWeight:NO of:font]]; [self removeTimedEntry]; [startButton setTitle:"Start"]; [startMenuItem setTitle:"Start"]; [stopwatch stopWatch]; [self showElapsedTime]; [self addSession:[stopwatch startDateString] time:[stopwatch startTimeString] duration:[stopwatch elapsedMinutes] description:[self description]]; activeClient = nil; return self; } /* * Called whenever the startButton is pressed. */ - buttonHandler:sender { if ( [startButton state] == 1 ) [self startClock]; else [self stopClock]; return self; } - showInfo:sender { [[InfoPanel new] showInfo]; return self; } /* * Inspect the currently selected client */ - inspect:sender { Matrix *matrix = [browser matrixInColumn:0]; ClientInspector *inspector = [ClientInspector sharedInstance]; [inspector selectClientAt:[matrix selectedRow]]; [inspector display]; return self; } - inspectSessions:sender { [[ClientInspector sharedInstance] showHours:sender]; return self; } - inspectExpenses:sender { [[ClientInspector sharedInstance] showExpenses:sender]; return self; } - inspectClients:sender { [[ClientInspector sharedInstance] showClient:sender]; return self; } - generateDetail:sender { [self initInvoice]; [invoice generate:clientList]; return self; } /* * Compact consecutive sessions with identical descriptions into * a single session with the same total time. */ - compactClients:sender { int i, count = [clientList count]; for ( i = 0; i < count; i++ ) [[clientList objectAt:i] compactSessions]; /* If it's showing, redisplay it, otherwise do nothing */ [[ClientInspector sharedInstance] decacheView]; [self saveClientInfo]; return self; } /* * This needs to be cleaned up... */ - import:sender { FILE *fp; const char *pathname; char buf[512], *tok; char shortName[80], startDate[80], startTime[80], minutes[80], desc[256]; id openPanel = [OpenPanel new]; ClientInspector *inspector = [ClientInspector sharedInstance]; char delimiter[2], endDelimiters[10]; if ( [openPanel runModal] == 0 ) return nil; pathname = [openPanel filename]; if ( ! (fp = fopen( pathname, "r" ) ) ) { NXRunAlertPanel( [NXApp appName], "Unable to open import file: `%s'", "Eat me!", NULL, NULL, pathname ); return self; } sprintf( delimiter, "%c", DELIMITER ); sprintf( endDelimiters, "%c\n", DELIMITER ); while ( fgets(buf, sizeof(buf), fp) ) { ClientInfo *info; Session *session; tok = strtok( buf, delimiter ); strcpy( shortName, tok ) ; if ( ! (info = [self findClient:shortName]) ) { NXRunAlertPanel( [NXApp appName], "Ignoring unknown client: `%s'", "Who needs 'em?", NULL, NULL, shortName ); continue; } tok = strtok( NULL, delimiter ); strcpy( startDate, tok ); tok = strtok( NULL, delimiter ); strcpy( startTime, tok ); tok = strtok( NULL, delimiter ); strcpy( minutes, tok ); tok = strtok( NULL, endDelimiters ); /* throw out newline too. */ strcpy( desc, tok ); session = [[Session alloc] init:startDate time:startTime duration:atoi(minutes) description:desc]; [info addSession:session]; [inspector updatedInfo:info]; } fclose(fp); [self saveClientInfo]; return self; } - export:sender { FILE *fp; const char *pathname; int i, count = [clientList count]; id savePanel = [SavePanel new]; if ( [savePanel runModal] == 0 ) return nil; pathname = [savePanel filename]; if ( ! (fp = fopen( pathname, "w" ) ) ) { NXRunAlertPanel( [NXApp appName], "Unable to open export file: `%s'", "I'll be darned!", NULL, NULL, pathname ); return self; } for ( i = 0; i < count; i++ ) [[clientList objectAt:i] exportToFile:fp]; fclose(fp); return self; } /* * Clear out all session information from all clients. */ - closeMonth:sender { ClientInspector *inspector = [ClientInspector sharedInstance]; /* Give the user a chance to change their mind... */ if ( NXRunAlertPanel( [NXApp appName], "Delete all session and expense data?", "Delete all data", "Hell no!", NULL ) == NX_ALERTDEFAULT ) { [clientList makeObjectsPerform:@selector(deleteSessionsAndExpenses)]; [inspector closeMonth]; [self saveClientInfo]; /* write the newly empty file */ [inspector decacheView]; } return self; } @implementation Controller(PRIVATE) - (void)selectBrowserRow:(int)row { Matrix *matrix = [browser matrixInColumn:0]; if ( row < 0 ) row = 0; [matrix selectCellAt:row :0]; [matrix scrollCellToVisible:row :0]; } /* * Find a client by short name */ - (ClientInfo *)findClient:(const char *)name { int i, count = [clientList count]; for ( i = 0; i < count; i++ ) { ClientInfo *info; info = [clientList objectAt:i]; if ( strcmp( name, [info shortName] ) == 0 ) return info ; } return nil; } - initInvoice { char path[FILENAME_MAX + 1]; if ( invoice == nil ) { sprintf( path, "%s/%s", dirname, TEMPLATE_DIR ); invoice = [[Invoice alloc] initTemplateDir:path]; } return invoice; } - (int)selectedRow { return [[browser matrixInColumn:0] selectedRow]; } - selectedClient { return [clientList objectAt:[self selectedRow]]; } /* * Make sure the start buttons are disabled if there are * no clients defined, and enabled if there are. */ - (void)checkStartButton { BOOL flag = ( [clientList count] ? YES : NO ); [startMenuItem setEnabled:flag]; [startButton setEnabled:flag]; /* the same goes for these */ [sessionMenuItem setEnabled:flag]; [expenseMenuItem setEnabled:flag]; /* * The only reasonable thing to do if there are no * clients is to create some! */ if ( flag == NO ) [self inspectClients:nil]; } /* * Create a new session object and add it to the proper client's * ClientInfo list. Tell the browser what happened so it can update. */ - (void)addSession:(const char *)startDate time:(const char *)startTime duration:(int)minutes description:(const char *)desc { Session *session = [[Session alloc] init:startDate time:startTime duration:minutes description:desc]; [activeClient addSession:session]; [[ClientInspector sharedInstance] updatedInfo:activeClient]; [self saveClientInfo]; } /* * Compare two ClientInfo objects. Sort alpha by long name. */ - (int)compare:obj1 :obj2 { return strcmp( [obj1 clientName], [obj2 clientName] ); } /* * Delegated method of NXBrowser. This should be consolidated into a single * object. Right now this method appears (almost) identically in the Controller * and the ClientMgr... (Here we use the shortName instead of the full one.) */ - (int) browser:sender fillMatrix:matrix inColumn:(int)column { int i, count = [clientList count]; for ( i = 0; i < count; i++ ) { const char *name; id cell; [matrix addRow]; name = [[clientList objectAt:i] shortName]; cell = [matrix cellAt:i :0]; /* 1 dimen. matrix: always use col 0! */ [cell setStringValue:name]; [cell setLoaded:YES]; [cell setLeaf:YES]; } return count ; } @end
These are the contents of the former NiCE NeXT User Group NeXTSTEP/OpenStep software archive, currently hosted by Netfuture.ch.