// // Pennyworth_Punch_Clock_AppDelegate.m // Pennyworth Punch Clock // // Created by Chris Karr on 6/30/08. // Copyright Northwestern University 2008 . All rights reserved. // #import "AppDelegate.h" #import "AppleScriptStream.h" @implementation AppDelegate @synthesize lastSlices; @synthesize selectedSliceTag; - (IBAction) closeDatePicker:(id) sender { [self willChangeValueForKey:@"sliceFilter"]; [NSApp endSheet:datePanel]; [datePanel orderOut:sender]; [self didChangeValueForKey:@"sliceFilter"]; } - (void) activateStatusMenu { NSStatusBar * bar = [NSStatusBar systemStatusBar]; theItem = [bar statusItemWithLength:NSVariableStatusItemLength]; [theItem retain]; [theItem setTitle:nil]; [theItem setImage:[NSImage imageNamed:@"ppc-status"]]; [theItem setHighlightMode:YES]; [theItem setMenu:statusMenu]; } - (void) initPennyworth { NSString * detectAs = @"tell application \"System Events\" to return count (every process whose name is \"Pennyworth\")"; NSAppleScript * detectScript = [[NSAppleScript alloc] initWithSource:detectAs]; NSAppleEventDescriptor * detectDesc = [detectScript executeAndReturnError:nil]; if ([[detectDesc stringValue] intValue] == 0) return; NSArray * keys = [NSArray arrayWithObjects:@"Location", @"Social Context", @"Activity", nil]; for (NSString * key in keys) { NSString * as = [NSString stringWithFormat:@"tell application \"Pennyworth\" to return value of prediction named \"%@\"", key, nil]; NSAppleScript * script = [[NSAppleScript alloc] initWithSource:as]; NSAppleEventDescriptor * desc = [script executeAndReturnError:nil]; NSString * prediction = [desc stringValue]; [script release]; NSMutableDictionary * note = [NSMutableDictionary dictionary]; [note setValue:key forKey:KEY]; [note setValue:prediction forKey:PREDICTION]; [[NSNotificationCenter defaultCenter] postNotificationName:PREDICTION_FETCHED object:self userInfo:note]; } } - (void) hotKeyReleased:(NDHotKeyEvent *) aHotKey { [NSApp activateIgnoringOtherApps:YES]; [updatePanel makeKeyAndOrderFront:nil]; } - (void) awakeFromNib { [NDHotKeyEvent setSignature:'PPCc']; hotKey = [[NDHotKeyEvent hotKeyWithKeyCode:35 character:112 modifierFlags:(NSControlKeyMask|NSCommandKeyMask)] retain]; [hotKey setTarget:self selectorReleased:@selector(hotKeyReleased:) selectorPressed:@selector(hotKeyReleased:)]; [hotKey setEnabled:YES]; self.lastSlices = [NSMutableDictionary dictionary]; self.selectedSliceTag = [NSNumber numberWithInteger:0]; [[NSDistributedNotificationCenter defaultCenter] addObserver:self selector:@selector(update:) name:PREDICTION_FETCHED object:nil]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(update:) name:PREDICTION_FETCHED object:nil]; [[NSUserDefaults standardUserDefaults] addObserver:self forKeyPath:START_AT_LOGIN options:0 context:NULL]; NSSortDescriptor * descriptor = [[NSSortDescriptor alloc] initWithKey:@"startDate" ascending:NO]; [slicesController setSortDescriptors:[NSArray arrayWithObject:descriptor]]; [descriptor release]; descriptor = [[NSSortDescriptor alloc] initWithKey:@"name" ascending:YES]; [streamsController setSortDescriptors:[NSArray arrayWithObject:descriptor]]; [descriptor release]; [self initPennyworth]; [self activateStatusMenu]; NSDateFormatter * formatter = [[NSDateFormatter alloc] initWithDateFormat:@"%1I:%M:%S %p (%B %e, %Y)" allowNaturalLanguage:YES]; [startDateField setFormatter:formatter]; [formatter release]; formatter = [[NSDateFormatter alloc] initWithDateFormat:@"%1I:%M:%S %p (%B %e, %Y)" allowNaturalLanguage:YES]; [endDateField setFormatter:formatter]; [formatter release]; } - (void) startAtLogin { BOOL enabled = [[NSUserDefaults standardUserDefaults] boolForKey:START_AT_LOGIN]; OSStatus status; CFArrayRef loginItems = NULL; NSURL * url = [NSURL fileURLWithPath:[[NSBundle mainBundle] bundlePath]]; int existingLoginItemIndex = -1; status = LIAECopyLoginItems (&loginItems); if (status == noErr) { NSEnumerator * enumerator = [(NSArray *) loginItems objectEnumerator]; NSDictionary * loginItemDict; while ((loginItemDict = [enumerator nextObject])) { if ([[loginItemDict objectForKey:(NSString *) kLIAEURL] isEqual:url]) { existingLoginItemIndex = [(NSArray *) loginItems indexOfObjectIdenticalTo:loginItemDict]; break; } } } if (enabled && (existingLoginItemIndex == -1)) LIAEAddURLAtEnd ((CFURLRef) url, false); else if (!enabled && (existingLoginItemIndex != -1)) LIAERemove (existingLoginItemIndex); if (loginItems) CFRelease (loginItems); } - (void) observeValueForKeyPath: (NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { if ([keyPath isEqual:START_AT_LOGIN]) [self startAtLogin]; } - (NSNumber *) getSliceWindowTag { return self.selectedSliceTag; } - (void) setSliceWindowTag:(NSNumber *) tag { [self willChangeValueForKey:@"sliceFilter"]; self.selectedSliceTag = tag; [self didChangeValueForKey:@"sliceFilter"]; if ([tag integerValue] == 99) { [NSApp beginSheet:datePanel modalForWindow:window modalDelegate:nil didEndSelector:nil contextInfo:NULL]; } } - (IBAction) invokeDatePanel:(id) sender { [self setSliceWindowTag:[NSNumber numberWithInteger:99]]; } - (NSPredicate *) getSliceFilter { NSInteger tag = [self.selectedSliceTag integerValue]; NSCalendar * calendar = [NSCalendar currentCalendar]; NSDate * date = nil; NSDateComponents * dateComps = [[NSDateComponents alloc] init]; NSDate * now = [NSDate date]; if (tag == 0) { [dateComps setMinute:-30]; date = [calendar dateByAddingComponents:dateComps toDate:now options:0]; } else if (tag == 1) { [dateComps setHour:-1]; date = [calendar dateByAddingComponents:dateComps toDate:now options:0]; } else if (tag == 2) { [dateComps setDay:-1]; date = [calendar dateByAddingComponents:dateComps toDate:now options:0]; } else if (tag == 3) { [dateComps setWeek:-1]; date = [calendar dateByAddingComponents:dateComps toDate:now options:0]; } else if (tag == 4) { [dateComps setMonth:-1]; date = [calendar dateByAddingComponents:dateComps toDate:now options:0]; } else if (tag == 99) { [dateComps release]; NSUserDefaults * defaults = [NSUserDefaults standardUserDefaults]; NSDate * start = [defaults objectForKey:@"Custom_Start"]; NSDate * end = [defaults objectForKey:@"Custom_End"]; if (end == nil) end = [NSDate date]; if (start == nil) start = [NSDate distantPast]; NSPredicate * predicate = [NSPredicate predicateWithFormat:@"(%K > %@ && %K < %@) || (%K > %@ && %K < %@) || (%K < %@ && %K > %@)", @"startDate", start, @"startDate", end, @"endDate", start, @"endDate", end, @"startDate", start, @"endDate", end, nil]; return predicate; } [dateComps release]; if (date == nil) return nil; else return [NSPredicate predicateWithFormat:@"((%K > %@) || (%K > %@) || (%K == NIL))", @"startDate", date, @"endDate", date, @"endDate", nil]; } - (void) setSliceFilter:(NSPredicate *) predicate { } - (void) update:(NSNotification *) theNote { NSDictionary * userInfo = [theNote userInfo]; NSString * key = [userInfo valueForKey:KEY]; NSString * predictionString = [userInfo valueForKey:PREDICTION]; NSArray * predictions = nil; if ([predictionString rangeOfString:@";"].location != NSNotFound) predictions = [[predictionString stringByReplacingOccurrencesOfString:@"; " withString:@";"] componentsSeparatedByString:@";"]; else predictions = [NSArray arrayWithObject:predictionString]; NSDate * now = [NSDate date]; if (key != nil && predictions != nil) { NSFetchRequest * request = [[NSFetchRequest alloc] init]; [request setEntity:[NSEntityDescription entityForName:@"TimeStream" inManagedObjectContext:[self managedObjectContext]]]; [request setPredicate:[NSPredicate predicateWithFormat:@"name MATCHES %@", key, nil]]; NSManagedObject * timeStream = nil; NSError * error = nil; NSArray * timeStreams = [[self managedObjectContext] executeFetchRequest:request error:&error]; for (NSManagedObject * stream in timeStreams) timeStream = stream; if (timeStream == nil) { timeStream = [NSEntityDescription insertNewObjectForEntityForName:@"TimeStream" inManagedObjectContext:[self managedObjectContext]]; [timeStream setValue:key forKey:@"name"]; [timeStream setValue:[NSNumber numberWithBool:NO] forKey:@"userEditable"]; } NSMutableSet * slices = [timeStream mutableSetValueForKey:@"slices"]; NSMutableArray * activeSlices = [NSMutableArray array]; for (NSString * prediction in predictions) { BOOL newSlice = YES; for (NSManagedObject * slice in slices) { if ([slice valueForKey:@"endDate"] == nil && [prediction isEqualToString:[slice valueForKey:@"name"]]) { newSlice = NO; [activeSlices addObject:slice]; } } if (newSlice && [prediction length] > 0) { NSManagedObject * slice = [NSEntityDescription insertNewObjectForEntityForName:@"Slice" inManagedObjectContext:[self managedObjectContext]]; [slice setValue:prediction forKey:@"name"]; [slice setValue:now forKey:@"startDate"]; [slices addObject:slice]; [activeSlices addObject:slice]; } } for (NSManagedObject * slice in slices) { if ([slice valueForKey:@"endDate"] == nil) { NSString * sliceName = [slice valueForKey:@"name"]; if (![predictions containsObject:sliceName]) [slice setValue:now forKey:@"endDate"]; } } [self.lastSlices setValue:activeSlices forKey:key]; NSMutableSet * values = [timeStream mutableSetValueForKey:@"values"]; BOOL hasValue = NO; for (NSString * prediction in predictions) { for (NSManagedObject * value in values) { NSString * valueName = [value valueForKey:@"name"]; if ([valueName isEqualToString:prediction]) hasValue = YES; } if (!hasValue) { NSManagedObject * value = [NSEntityDescription insertNewObjectForEntityForName:@"Value" inManagedObjectContext:[self managedObjectContext]]; [value setValue:prediction forKey:@"name"]; [values addObject:value]; } } } } /** Returns the support folder for the application, used to store the Core Data store file. This code uses a folder named "Pennyworth_Punch_Clock" for the content, either in the NSApplicationSupportDirectory location or (if the former cannot be found), the system's temporary directory. */ - (NSString *)applicationSupportFolder { NSArray * paths = NSSearchPathForDirectoriesInDomains (NSApplicationSupportDirectory, NSUserDomainMask, YES); NSString * basePath = ([paths count] > 0) ? [paths objectAtIndex:0] : NSTemporaryDirectory (); return [basePath stringByAppendingPathComponent:@"Pennyworth Punch Clock"]; } /** Creates, retains, and returns the managed object model for the application by merging all of the models found in the application bundle. */ - (NSManagedObjectModel *) managedObjectModel { if (managedObjectModel != nil) return managedObjectModel; managedObjectModel = [[NSManagedObjectModel mergedModelFromBundles:nil] retain]; return managedObjectModel; } /** Returns the persistent store coordinator for the application. This implementation will create and return a coordinator, having added the store for the application to it. (The folder for the store is created, if necessary.) */ - (NSPersistentStoreCoordinator *) persistentStoreCoordinator { if (persistentStoreCoordinator != nil) return persistentStoreCoordinator; NSFileManager * fileManager; NSString * applicationSupportFolder = nil; NSURL * url; NSError * error; fileManager = [NSFileManager defaultManager]; applicationSupportFolder = [self applicationSupportFolder]; if ( ![fileManager fileExistsAtPath:applicationSupportFolder isDirectory:NULL] ) { [fileManager createDirectoryAtPath:applicationSupportFolder attributes:nil]; } url = [NSURL fileURLWithPath:[applicationSupportFolder stringByAppendingPathComponent:@"Pennyworth_Punch_Clock.xml"]]; persistentStoreCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:[self managedObjectModel]]; if (![persistentStoreCoordinator addPersistentStoreWithType:NSXMLStoreType configuration:nil URL:url options:nil error:&error]) [[NSApplication sharedApplication] presentError:error]; return persistentStoreCoordinator; } /** Returns the managed object context for the application (which is already bound to the persistent store coordinator for the application.) */ - (NSManagedObjectContext *) managedObjectContext { if (managedObjectContext != nil) return managedObjectContext; NSPersistentStoreCoordinator *coordinator = [self persistentStoreCoordinator]; if (coordinator != nil) { managedObjectContext = [[NSManagedObjectContext alloc] init]; [managedObjectContext setPersistentStoreCoordinator:coordinator]; } return managedObjectContext; } /** Returns the NSUndoManager for the application. In this case, the manager returned is that of the managed object context for the application. */ - (NSUndoManager *) windowWillReturnUndoManager:(NSWindow *) window { return [[self managedObjectContext] undoManager]; } /** Performs the save action for the application, which is to send the save: message to the application's managed object context. Any encountered errors are presented to the user. */ - (IBAction) saveAction:(id) sender { NSError * error = nil; if (![[self managedObjectContext] save:&error]) [[NSApplication sharedApplication] presentError:error]; } /** Implementation of the applicationShouldTerminate: method, used here to handle the saving of changes in the application managed object context before the application terminates. */ - (NSApplicationTerminateReply) applicationShouldTerminate:(NSApplication *) sender { [NSApp activateIgnoringOtherApps:YES]; if (NSAlertDefaultReturn == NSRunAlertPanel (@"Quit Pennyworth Punch Clock?", @"Do you want to stop running Pennyworth Punch Clock?", @"No", @"Yes", nil)) return NO; NSDate * now = [NSDate date]; for (NSString * key in [self.lastSlices allKeys]) { for (NSManagedObject * slice in [self.lastSlices valueForKey:key]) [slice setValue:now forKey:@"endDate"]; } NSError * error; int reply = NSTerminateNow; if (managedObjectContext != nil) { if ([managedObjectContext commitEditing]) { if ([managedObjectContext hasChanges] && ![managedObjectContext save:&error]) { // This error handling simply presents error information in a panel with an // "Ok" button, which does not include any attempt at error recovery (meaning, // attempting to fix the error.) As a result, this implementation will // present the information to the user and then follow up with a panel asking // if the user wishes to "Quit Anyway", without saving the changes. // Typically, this process should be altered to include application-specific // recovery steps. BOOL errorResult = [[NSApplication sharedApplication] presentError:error]; if (errorResult == YES) reply = NSTerminateCancel; else { int alertReturn = NSRunAlertPanel(nil, @"Could not save changes while quitting. Quit anyway?" , @"Quit anyway", @"Cancel", nil); if (alertReturn == NSAlertAlternateReturn) reply = NSTerminateCancel; } } } else reply = NSTerminateCancel; } [[NSFileManager defaultManager] removeItemAtPath:TMP_DIR error:NULL]; return reply; } /** Implementation of dealloc, to release the retained variables. */ - (void) dealloc { [managedObjectContext release], managedObjectContext = nil; [persistentStoreCoordinator release], persistentStoreCoordinator = nil; [managedObjectModel release], managedObjectModel = nil; [super dealloc]; } - (IBAction) toggleLog:(id) sender { [NSApp activateIgnoringOtherApps:YES]; [window makeKeyAndOrderFront:sender]; [mainTabs selectTabViewItemAtIndex:0]; } - (IBAction) toggleReports:(id) sender { [NSApp activateIgnoringOtherApps:YES]; [window makeKeyAndOrderFront:sender]; [mainTabs selectTabViewItemAtIndex:1]; } - (IBAction) togglePreferences:(id) sender { [NSApp activateIgnoringOtherApps:YES]; [window makeKeyAndOrderFront:sender]; [mainTabs selectTabViewItemAtIndex:2]; } - (BOOL) application:(NSApplication *) sender delegateHandlesKey:(NSString *) key { if ([key isEqualToString:@"streams"]) return YES; return NO; } - (NSArray *) getStreams { NSMutableArray * asStreams = [NSMutableArray array]; for (NSManagedObject * stream in [streamsController arrangedObjects]) { AppleScriptStream * asStream = [[AppleScriptStream alloc] init]; asStream.name = [stream valueForKey:@"name"]; asStream.object = stream; [asStreams addObject:asStream]; [asStream release]; } return asStreams; } - (NSNumber *) getSelectedSliceTag { return self.selectedSliceTag; } - (BOOL) applicationShouldHandleReopen:(NSApplication *) theApplication hasVisibleWindows:(BOOL) flag { [self toggleLog:self]; return NO; } - (NSDictionary *) getActiveState { NSPredicate * predicate = [NSPredicate predicateWithFormat:@"(%K == NIL)", @"endDate", nil]; NSFetchRequest * request = [[NSFetchRequest alloc] init]; [request setEntity:[NSEntityDescription entityForName:@"Slice" inManagedObjectContext:[self managedObjectContext]]]; [request setPredicate:predicate]; NSError * error = nil; NSArray * slices = [[self managedObjectContext] executeFetchRequest:request error:&error]; NSMutableDictionary * state = [NSMutableDictionary dictionary]; for (NSManagedObject * slice in slices) { NSString * streamName = [[slice valueForKey:@"stream"] valueForKey:@"name"]; if ([state valueForKey:streamName] == nil) [state setValue:[NSMutableArray array] forKey:streamName]; [[state valueForKey:streamName] addObject:[slice valueForKey:@"name"]]; } [request release]; return state; } @end