/* author: Onne Gorter This file is part of AntiRSI. AntiRSI is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. AntiRSI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with AntiRSI; if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ #import "AntiRSI.h" #include #include extern CFTimeInterval CGSSecondsSinceLastInputEvent(unsigned long eventType); static int badge_session_minutes = -1; @implementation AntiRSI - (void)computeResetSessionDate; { NSCalendarDate *now = [NSCalendarDate calendarDate]; NSCalendarDate *reset = [NSCalendarDate dateWithYear:[now yearOfCommonEra] month:[now monthOfYear] day:[now dayOfMonth] hour:[reset_session_timer_time hourOfDay] minute:[reset_session_timer_time minuteOfHour] second:0 timeZone:[NSTimeZone systemTimeZone]]; if ([now compare:reset] != NSOrderedAscending) reset = [reset dateByAddingYears:0 months:0 days:1 hours:0 minutes:0 seconds:0]; reset_session_date = [reset timeIntervalSinceReferenceDate]; } // bindings methods - (void)setMicro_pause_duration:(float)f { micro_pause_duration = round(f); if (s_taking_micro_pause == state) { [progress setMaxValue:micro_pause_duration]; [progress setDoubleValue:micro_pause_taking_t]; } } - (void)setMicro_pause_period:(float)f { micro_pause_period = 60 * round(f); } - (void)setWork_break_duration:(float)f { work_break_duration = 60 * round(f); if (s_taking_work_break == state) { [progress setMaxValue:work_break_duration / 60]; [progress setDoubleValue:work_break_taking_t / 60 - 0.5]; } } - (void)setWork_break_period:(float)f { work_break_period = 60 * round(f); } - (void)setReset_session_timer_time:(NSDate *)d; { if (reset_session_timer_time != nil) [reset_session_timer_time autorelease]; reset_session_timer_time = [[NSCalendarDate alloc] initWithTimeIntervalSinceReferenceDate:[d timeIntervalSinceReferenceDate]]; [reset_session_timer_time setTimeZone:[NSTimeZone timeZoneForSecondsFromGMT:0]]; [self computeResetSessionDate]; } - (void)installTimer:(double)interval { if (mtimer != nil) { [mtimer invalidate]; [mtimer autorelease]; } mtimer = [[NSTimer scheduledTimerWithTimeInterval:interval target:self selector:@selector(tick:) userInfo:nil repeats:YES] retain]; } - (void)setSample_interval:(NSString *)s { sample_interval = 1; if ([s isEqualToString:@"Super Smooth"]) sample_interval = 0.1; if ([s isEqualToString:@"Smooth"]) sample_interval = 0.33; if ([s isEqualToString:@"Normal"]) sample_interval = 1; if ([s isEqualToString:@"Low"]) sample_interval = 2; [self installTimer:sample_interval]; } - (void)setDraw_dock_image:(BOOL)b { draw_dock_image=b; if (!b) { [NSApp setApplicationIconImage:[NSImage imageNamed:@"AntiRSI"]]; } else { [self drawDockImage]; } } - (void)setBackground:(NSColor *)c { [background autorelease]; background=[c retain]; // make new darkbackground color float r,g,b,a; [background getRed:&r green:&g blue:&b alpha:&a]; [darkbackground autorelease]; darkbackground=[[NSColor colorWithCalibratedRed:r*0.35 green:g*0.35 blue:b*0.35 alpha:a+0.2] retain]; [self drawDockImage]; } - (void)setElapsed:(NSColor *)c { [elapsed autorelease]; elapsed=[c retain]; [dock_badge setBadgeColor:elapsed]; badge_session_minutes = -1; [self drawDockImage]; } - (void)setTaking:(NSColor *)c { [taking autorelease]; taking=[c retain]; [self drawDockImage]; } // end of bindings - (void)resetTimers; { // set timers to 0 micro_pause_t = 0; work_break_t = 0; micro_pause_taking_t = 0; work_break_taking_t = 0; work_break_taking_cached_t = 0; work_break_taking_cached_date = 0; session_t = 0; } - (void)awakeFromNib { // want transparancy [NSColor setIgnoresAlpha:NO]; // initial colors elapsed = [[NSColor colorWithCalibratedRed:0.3 green:0.3 blue:0.9 alpha:0.95] retain]; taking = [[NSColor colorWithCalibratedRed:0.3 green:0.9 blue:0.3 alpha:0.90] retain]; background = [NSColor colorWithCalibratedRed:0.9 green:0.9 blue:0.9 alpha:0.7]; //initial values micro_pause_period = 4*60; micro_pause_duration = 13; work_break_period = 50*60; work_break_duration = 8*60; sample_interval = 1; // set current state state = s_normal; [self resetTimers]; // initialize dock image dock_image = [[NSImage alloc] initWithSize:NSMakeSize(128,128)]; [dock_image setCacheMode:NSImageCacheNever]; original_dock_image = [NSImage imageNamed:@"AntiRSI"]; draw_dock_image_q = YES; dock_badge = [[CTBadge systemBadge] retain]; [dock_badge setBadgeColor:elapsed]; // setup main window that will show either micropause or workbreak main_window = [[NSWindow alloc] initWithContentRect:[view frame] styleMask:NSBorderlessWindowMask backing:NSBackingStoreBuffered defer:YES]; [main_window setBackgroundColor:[NSColor clearColor]]; [main_window setLevel:NSStatusWindowLevel]; [main_window setAlphaValue:0.85]; [main_window setOpaque:NO]; [main_window setHasShadow:NO]; [main_window setMovableByWindowBackground:YES]; if ([main_window respondsToSelector:@selector(setCollectionBehavior:)]) [main_window setCollectionBehavior: NSWindowCollectionBehaviorCanJoinAllSpaces]; [main_window center]; [main_window setContentView:view]; NSTimeZone *utcZone = [NSTimeZone timeZoneForSecondsFromGMT:0]; [reset_session_time setTimeZone:utcZone]; [progress setEnabled:NO]; NSFont *myriad = [NSFont fontWithName: @"Myriad" size: 40]; if (myriad) [status setFont: myriad]; // initialze history filter h0 = 0; h1 = 0; h2 = 0; // initialize ticks date = [NSDate timeIntervalSinceReferenceDate]; // set background now [self setBackground:background]; // create initial values NSUserDefaultsController *dc = [NSUserDefaultsController sharedUserDefaultsController]; NSMutableDictionary* initial = [NSMutableDictionary dictionaryWithObjectsAndKeys: [NSNumber numberWithFloat:4], @"micro_pause_period", [NSNumber numberWithFloat:13], @"micro_pause_duration", [NSNumber numberWithFloat:50], @"work_break_period", [NSNumber numberWithFloat:8], @"work_break_duration", @"Smooth", @"sample_interval", [NSNumber numberWithBool:YES], @"draw_dock_image", [NSNumber numberWithBool:YES], @"draw_dock_badge", [NSNumber numberWithBool:NO], @"lock_focus", [NSNumber numberWithBool:NO], @"reset_session_timer_daily", [NSNumber numberWithBool:NO], @"reset_session_timer_after", [NSCalendarDate dateWithYear:2000 month:1 day:1 hour:6 minute:0 second:0 timeZone:utcZone], @"reset_session_timer_time", [NSNumber numberWithInt:8], @"reset_session_timer_after_hours", [NSArchiver archivedDataWithRootObject:elapsed], @"elapsed", [NSArchiver archivedDataWithRootObject:taking], @"taking", [NSArchiver archivedDataWithRootObject:background], @"background", nil]; [[NSUserDefaults standardUserDefaults] registerDefaults:initial]; [dc setInitialValues:initial]; // bind to defaults controller [self bind:@"micro_pause_period" toObject:dc withKeyPath:@"values.micro_pause_period" options:nil]; [self bind:@"micro_pause_duration" toObject:dc withKeyPath:@"values.micro_pause_duration" options:nil]; [self bind:@"work_break_period" toObject:dc withKeyPath:@"values.work_break_period" options:nil]; [self bind:@"work_break_duration" toObject:dc withKeyPath:@"values.work_break_duration" options:nil]; [self bind:@"sample_interval" toObject:dc withKeyPath:@"values.sample_interval" options:nil]; [self bind:@"draw_dock_image" toObject:dc withKeyPath:@"values.draw_dock_image" options:nil]; [self bind:@"draw_dock_badge" toObject:dc withKeyPath:@"values.draw_dock_badge" options:nil]; [self bind:@"lock_focus" toObject:dc withKeyPath:@"values.lock_focus" options:nil]; [self bind:@"reset_session_timer_daily" toObject:dc withKeyPath:@"values.reset_session_timer_daily" options:nil]; [self bind:@"reset_session_timer_after" toObject:dc withKeyPath:@"values.reset_session_timer_after" options:nil]; [self bind:@"reset_session_timer_time" toObject:dc withKeyPath:@"values.reset_session_timer_time" options:nil]; [self bind:@"reset_session_timer_after_hours" toObject:dc withKeyPath:@"values.reset_session_timer_after_hours" options:nil]; NSDictionary* unarchive = [NSDictionary dictionaryWithObject:NSUnarchiveFromDataTransformerName forKey:@"NSValueTransformerName"]; [self bind:@"elapsed" toObject:dc withKeyPath:@"values.elapsed" options:unarchive]; [self bind:@"taking" toObject:dc withKeyPath:@"values.taking" options:unarchive]; [self bind:@"background" toObject:dc withKeyPath:@"values.background" options:unarchive]; // alert every binding [dc revert:self]; // start the timer [self installTimer:sample_interval]; // about dialog [version setStringValue:[NSString stringWithFormat:@"Version %@", sVersion]]; } // tick every second and update status - (void)tick:(NSTimer *)timer { // calculate time since last tick NSTimeInterval new_date = [NSDate timeIntervalSinceReferenceDate]; NSTimeInterval tick_time = new_date - date; date = new_date; if (reset_session_timer_daily && date >= reset_session_date) { [self resetSession:nil]; [self computeResetSessionDate]; return; } // check if we are still on track of normal time, otherwise we might have slept or something if (tick_time > work_break_duration) { // set timers to 0 micro_pause_t = 0; work_break_t = 0; micro_pause_taking_t = micro_pause_duration; work_break_taking_t = work_break_duration; if (s_normal != state) { [self endBreak]; } // and do stuff on next tick return; } // just did a whole micropause beyond normal time if (tick_time > micro_pause_duration && s_taking_work_break != state) { // set micro_pause timers to 0 micro_pause_t = 0; micro_pause_taking_t = micro_pause_duration; if (s_normal != state) { [self endBreak]; } // and do stuff on next tick return; } // get idle time in seconds CFTimeInterval idle_time = CGSSecondsSinceLastInputEvent(kCGAnyInputEventType); // CFTimeInterval cgs_idle_time = idle_time; // from other people's reverse engineering of this function, on MDD G4s this can return a large positive number when input is in progress if (idle_time >= 18446744000.0) { idle_time = 0; } else if (CGEventSourceSecondsSinceLastEventType != NULL) { CGEventType eventTypes[] = { kCGEventLeftMouseDown, kCGEventLeftMouseUp, kCGEventRightMouseDown, kCGEventRightMouseUp, kCGEventMouseMoved, kCGEventLeftMouseDragged, kCGEventRightMouseDragged, kCGEventKeyDown, kCGEventKeyUp, kCGEventFlagsChanged, kCGEventScrollWheel, kCGEventTabletPointer, kCGEventTabletProximity, kCGEventOtherMouseDown, kCGEventOtherMouseUp, kCGEventOtherMouseDragged, kCGEventNull }; CFTimeInterval event_idle_time; idle_time = DBL_MAX; for (CGEventType *eventType = eventTypes ; *eventType != kCGEventNull ; eventType++) { event_idle_time = CGEventSourceSecondsSinceLastEventType(kCGEventSourceStateCombinedSessionState, *eventType); if (event_idle_time < idle_time) idle_time = event_idle_time; } } // NSLog(@"CGEventSource %.2f, CGS %.2f", idle_time, cgs_idle_time); if (reset_session_timer_after && idle_time > reset_session_timer_after_hours * 3600) { [self resetSession:nil]; return; } // calculate slack, this gives a sort of 3 history filtered idea. BOOL slack = (h2 + h1 + h0 > 15); // if new event comes in history bumps up if (h0 >= idle_time || idle_time < sample_interval) { h2 = h1; h1 = h0; } h0 = idle_time; switch (state) { case s_normal: // idle_time needs to be at least 0.3 * micro_pause_duration before kicking in // but we cut the user some slack based on previous idle_times if (idle_time <= micro_pause_duration * 0.3 && !slack) { micro_pause_t += tick_time; work_break_t += tick_time; if (idle_time < 1) { session_t += tick_time; } micro_pause_taking_t = 0; if (work_break_taking_t > 0) { work_break_taking_cached_t = work_break_taking_t; work_break_taking_cached_date = date; } work_break_taking_t = 0; } else if (micro_pause_t > 0) { // oke, leaway is over, increase micro_pause_taking_t unless micro_pause is already over //micro_pause_t stays put work_break_t += tick_time; micro_pause_taking_t += tick_time; work_break_taking_t = 0; } // if micro_pause_taking_t is above micro_pause_duration, then micro pause is over, // if still idleing workbreak_taking_t kicks in unless it is already over if (micro_pause_taking_t >= micro_pause_duration && work_break_t > 0) { work_break_taking_t += tick_time; micro_pause_t = 0; } // if work_break_taking_t is above work_break_duration, then work break is over if (work_break_taking_t >= work_break_duration) { micro_pause_t = 0; work_break_t = 0; // micro_pause_taking_t stays put // work_break_taking_t stays put } // if user needs to take a micro pause if (micro_pause_t >= micro_pause_period) { // anticipate next workbreak by not issuing this micro_pause ... if (work_break_t > work_break_period - (micro_pause_period / 2)) { work_break_t = work_break_period; [self doWorkBreak]; } else { [self doMicroPause]; } } // if user needs to take a work break if (work_break_t >= work_break_period) { // stop micro_pause stuff micro_pause_t = 0; micro_pause_taking_t = micro_pause_duration; // and display window [self doWorkBreak]; } break; // taking a micro pause with window case s_taking_micro_pause: // continue updating timers micro_pause_taking_t += tick_time; work_break_t += tick_time; // if we don't break, or interrupt the break, reset it if (idle_time < 1 && !slack) { micro_pause_taking_t = 0; session_t += tick_time; } // update window [self updateBreakWindowDuration:micro_pause_duration progress:micro_pause_taking_t nextBreak:work_break_period - work_break_t]; // check if we done enough if (micro_pause_taking_t > micro_pause_duration) { micro_pause_t = 0; [self endBreak]; } // if workbreak must be run ... if (work_break_t >= work_break_period) { // stop micro_pause stuff micro_pause_t = 0; micro_pause_taking_t = micro_pause_duration; // and display window [self doWorkBreak]; } else { double slip = (micro_pause_duration - micro_pause_taking_t) - (int)(micro_pause_duration - micro_pause_taking_t); [self installTimer: slip < 0.1 ? 1 : slip]; } break; // taking a work break with window case s_taking_work_break: // increase work_break_taking_t if (idle_time >= 2 || work_break_taking_t < 3) { work_break_taking_t += tick_time; } else if (idle_time < 1) { session_t += tick_time; } // draw window [self updateBreakWindowDuration:work_break_duration progress:work_break_taking_t nextBreak:work_break_period + work_break_duration - work_break_taking_t]; // and check if we done enough if (work_break_taking_t > work_break_duration) { micro_pause_t = 0; micro_pause_taking_t = micro_pause_duration; work_break_t = 0; work_break_taking_t = work_break_duration; [self endBreak]; } else { double slip = (work_break_duration - work_break_taking_t) - (int)(work_break_duration - work_break_taking_t); [self installTimer: slip < 0.1 ? 1 : slip]; } break; } // draw dock image if (draw_dock_image) [self drawDockImage]; } // dock image - (NSMenu *)applicationDockMenu:(NSApplication *)sender; { [session_time_item setTitle:[self sessionTimeString]]; return dock_menu; } // draw the dock icon - (void)drawDockImage { [dock_image lockFocus]; // clear all [[NSColor clearColor] set]; NSRectFill(NSMakeRect(0,0,127,127)); NSBezierPath* p; float end; //draw background circle [darkbackground set]; p =[NSBezierPath bezierPathWithOvalInRect:NSMakeRect(6,6,115,115)]; [p setLineWidth:4]; [p stroke]; //fill [background set]; [[NSBezierPath bezierPathWithOvalInRect:NSMakeRect(8,8,111,111)] fill]; //put dot in middle [darkbackground set]; [[NSBezierPath bezierPathWithOvalInRect:NSMakeRect(59,59,9,9)] fill]; // reuse this one p = [NSBezierPath bezierPath]; // draw work_break [elapsed set]; end = 360 - (360.0 / work_break_period * work_break_t - 90); if (end <= 90) end=90.1; [p appendBezierPathWithArcWithCenter:NSMakePoint(63.5, 63.5) radius:40 startAngle:90 endAngle:end clockwise:YES]; [p setLineWidth:22]; [p stroke]; // draw work break taking [taking set]; [p removeAllPoints]; end = 360 - (360.0 / work_break_duration * work_break_taking_t - 90); if (end <= 90) end=90.1; [p appendBezierPathWithArcWithCenter:NSMakePoint(63.5, 63.5) radius:40 startAngle:90 endAngle:end clockwise:YES]; [p setLineWidth:18]; [p stroke]; // draw micro pause [elapsed set]; [p removeAllPoints]; end = 360 - (360.0 / micro_pause_period * micro_pause_t - 90); if (end <= 90) end = 90.1; [p appendBezierPathWithArcWithCenter:NSMakePoint(63.5, 63.5) radius:17 startAngle:90 endAngle:end clockwise:YES]; [p setLineWidth:22]; [p stroke]; // draw micro pause taking [taking set]; [p removeAllPoints]; end = 360 - (360.0 / micro_pause_duration * micro_pause_taking_t - 90); if (end <= 90) end = 90.1; [p appendBezierPathWithArcWithCenter:NSMakePoint(63.5, 63.5) radius:17 startAngle:90 endAngle:end clockwise:YES]; [p setLineWidth:18]; [p stroke]; // draw session time if (draw_dock_badge) { static NSImage *badge = nil; int session_minutes = (int)session_t / 60; if (badge_session_minutes != session_minutes) { if (badge != nil) [badge release]; badge = [[dock_badge badgeOverlayImageForString: [NSString stringWithFormat:@"%d:%02d", session_minutes / 60, session_minutes % 60] insetX: 3 y: 3] retain]; badge_session_minutes = session_minutes; } [badge compositeToPoint:NSZeroPoint operation:NSCompositeSourceOver]; } [dock_image unlockFocus]; // and set it in the dock check draw_dock_image one last time ... if (draw_dock_image_q) [NSApp setApplicationIconImage:dock_image]; } static ProcessSerialNumber frontmostApp; // done with micro pause or work break - (void)endBreak { [main_window orderOut:NULL]; if (lock_focus && (frontmostApp.highLongOfPSN != 0 || frontmostApp.lowLongOfPSN != 0)) { SetFrontProcess(&frontmostApp); frontmostApp.highLongOfPSN = kNoProcess; } state = s_normal; // reset time interval to user's choice [self installTimer:sample_interval]; } // bring app to front, saving previous app - (void)focus { NSDictionary *activeProcessInfo = [[NSWorkspace sharedWorkspace] activeApplication]; if (lock_focus) { frontmostApp.highLongOfPSN = [[activeProcessInfo objectForKey:@"NSApplicationProcessSerialNumberHigh"] longValue]; frontmostApp.lowLongOfPSN = [[activeProcessInfo objectForKey:@"NSApplicationProcessSerialNumberLow"] longValue]; } [self tick: nil]; [main_window center]; [main_window orderFrontRegardless]; } // display micro_pause window with appropriate widgets and progress bar - (void)doMicroPause { micro_pause_taking_t = 0; [status setStringValue:@"Micro Pause"]; [progress setMaxValue:micro_pause_duration]; [progress setDoubleValue:micro_pause_taking_t]; [progress setWarningValue: 1]; [progress setCriticalValue: micro_pause_duration]; [postpone setHidden:YES]; state = s_taking_micro_pause; [self focus]; } // display work_break window with appropriate widgets and progress bar - (void)doWorkBreak { work_break_taking_t = 0; // incase you were already having an implicit work break and clicked the take work break now button // not more then 20 seconds ago we took a natural break longer then 0.2 * normal work break duration if (date - work_break_taking_cached_date < 20 && work_break_taking_cached_t > work_break_duration * 0.2) { work_break_taking_t = work_break_taking_cached_t; } [status setStringValue:@"Work Break"]; [progress setMaxValue:work_break_duration / 60]; [progress setDoubleValue:work_break_taking_t / 60 - 0.5]; [progress setWarningValue: 0]; [progress setCriticalValue: 0.4]; [postpone setHidden:NO]; state = s_taking_work_break; [self focus]; } - (NSString *)sessionTimeString; { int seconds = lrint(session_t); return [NSString stringWithFormat:@"Session: %d:%02d:%02d", seconds / 3600, (seconds / 60) % 60, seconds % 60]; } - (void)updateBreakWindowDuration:(double)duration progress:(double)progress_t nextBreak:(double)nextBreak; { // progress [progress setDoubleValue:duration >= 60 ? (progress_t / 60 - 0.5) : progress_t]; // time left int timeLeft = lrint(duration - progress_t); [time setStringValue:[NSString stringWithFormat:@"%d:%02d", timeLeft / 60, timeLeft % 60]]; // cumulative typing time in this session (e.g. today) [session_time setStringValue:[self sessionTimeString]]; // next break int minutes = round(nextBreak / 60.0); // nice hours, minutes ... if (minutes > 60) { [next_break setStringValue:[NSString stringWithFormat:@"next break in %d:%02d hours", minutes / 60, minutes % 60]]; } else { [next_break setStringValue:[NSString stringWithFormat:@"next break in %d minutes", minutes]]; } // if user likes to be interrupted if (lock_focus) { [NSApp activateIgnoringOtherApps:YES]; [main_window makeKeyAndOrderFront:self]; } } // goto website - (IBAction)gotoWebsite:(id)sender { [[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:sURL]]; } // check for update - (IBAction)checkForUpdate:(id)sender { NSString *latest_version = [NSString stringWithContentsOfURL: [NSURL URLWithString:sLatestVersionURL]]; if (latest_version == Nil) latest_version = @""; latest_version = [latest_version stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]]; if ([latest_version length] == 0) { NSRunInformationalAlertPanel( @"Unable to Determine", @"Unable to determine the latest AntiRSI version number.", @"Ok", nil, nil); } else if ([latest_version compare:sVersion] == NSOrderedDescending) { int r = NSRunInformationalAlertPanel( @"New Version", @"A new version (%@) of AntiRSI is available; would you like to go to its Web site now?", @"Goto Website", @"Cancel", nil, latest_version); if (r == NSOKButton) { [[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:sURL]]; } } else { NSRunInformationalAlertPanel( @"No Update Available", @"This is the latest version of AntiRSI.", @"OK", nil, nil); } } // stop work break and postpone by 10 minutes - (IBAction)postpone:(id)sender { if (s_taking_work_break == state) { micro_pause_t = 0; micro_pause_taking_t = 0; work_break_taking_t = 0; work_break_taking_cached_t = 0; work_break_t -= 10*60; // decrease with 10 minutes if (work_break_t < 0) work_break_t = 0; [self endBreak]; } } - (IBAction)breakNow:(id)sender { [self doWorkBreak]; } - (IBAction)resetSession:(id)sender; { if (s_normal != state) { [self endBreak]; } [self resetTimers]; } // validate menu items - (BOOL)validateMenuItem:(NSMenuItem *)anItem { if ([anItem action] == @selector(breakNow:) && state == s_normal) return YES; if ([anItem action] == @selector(postpone:) && state == s_taking_work_break) return YES; if ([anItem action] == @selector(resetSession:)) return YES; if ([anItem action] == @selector(gotoWebsite:)) return YES; if ([anItem action] == @selector(checkForUpdate:)) return YES; return NO; } // we are delegate of NSApplication, so we can restore the icon on quit. - (void)applicationWillTerminate:(NSNotification *)aNotification { // make sure timer doesn't tick once more ... draw_dock_image_q = NO; [mtimer invalidate]; [mtimer autorelease]; mtimer = nil; [dock_image release]; // stupid fix for icon beeing restored ... it is not my fault, // the dock or NSImage or setApplicationIconImage seem to be caching or taking // snapshot or something ... ! [NSApp setApplicationIconImage:original_dock_image]; [NSThread sleepUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.1]]; [NSApp setApplicationIconImage:original_dock_image]; } @end