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 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; } /* * 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) - selectedClient; - (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 { /* * Find the possibly new position in the list of the selected client, * BEFORE redisplaying the browser from the list... */ int row = [clientList indexOf:[self selectedClient]]; [browser loadColumnZero]; [[browser matrixInColumn:0] selectCellAt:row :0]; [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; } - 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]; 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 { if ( teNum ) [self stopClock]; 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; } /* * 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; } /* * 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]; [[ClientInspector sharedInstance] display]; [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 display]; } return self; } @implementation Controller(PRIVATE) - 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.