/* 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 double CGSSecondsSinceLastInputEvent(unsigned long eventType); @implementation AntiRSI // 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)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]; [self drawDockImage]; } - (void)setTaking:(NSColor *)c { [taking autorelease]; taking=[c retain]; [self drawDockImage]; } // end of bindings - (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; // set timers to 0 micro_pause_t = 0; work_break_t = 0; micro_pause_taking_t = 0; work_break_taking_t = 0; // setup images micro_pause_image = [NSImage imageNamed:@"micro_pause"]; work_break_image = [NSImage imageNamed:@"work_break"]; // 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; // 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]; [main_window center]; [main_window setContentView:view]; [progress setEnabled:NO]; // initialze history filter h0 = 0; h1 = 0; h2 = 0; // initialize ticks date = [NSDate timeIntervalSinceReferenceDate]; // set background now [self setBackground:background]; // create initial values NSMutableDictionary* initial = [NSMutableDictionary dictionaryWithCapacity:10]; [initial setObject:[NSNumber numberWithFloat:4] forKey:@"micro_pause_period"]; [initial setObject:[NSNumber numberWithFloat:13] forKey:@"micro_pause_duration"]; [initial setObject:[NSNumber numberWithFloat:50] forKey:@"work_break_period"]; [initial setObject:[NSNumber numberWithFloat:8] forKey:@"work_break_duration"]; [initial setObject:@"Normal" forKey:@"sample_interval"]; [initial setObject:[NSNumber numberWithBool:YES] forKey:@"draw_dock_image"]; [initial setObject:[NSNumber numberWithBool:NO] forKey:@"lock_focus"]; [initial setObject:[NSArchiver archivedDataWithRootObject:elapsed] forKey:@"elapsed"]; [initial setObject:[NSArchiver archivedDataWithRootObject:taking] forKey:@"taking"]; [initial setObject:[NSArchiver archivedDataWithRootObject:background] forKey:@"background"]; [[NSUserDefaultsController sharedUserDefaultsController] setInitialValues:initial]; // bind to defauls controller id dc = [NSUserDefaultsController sharedUserDefaultsController]; [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:@"lock_focus" toObject:dc withKeyPath:@"values.lock_focus" 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 [[NSUserDefaultsController sharedUserDefaultsController] revert:self]; // start the timer [self installTimer:sample_interval]; } // tick every second and update status - (void)tick:(NSTimer *)timer { // calculate time since last tick double new_date = [NSDate timeIntervalSinceReferenceDate]; double tick_time = new_date - date; date = new_date; // 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; } 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 double idle_time = CGSSecondsSinceLastInputEvent(kCGAnyInputEventType); // double 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.0; } else if (CGEventSourceSecondsSinceLastEventType != NULL) { // CGEventSourceSecondsSinceLastEventType in 10.4.2 returns a CGEventTimestamp of the last event, not a CFTimeInterval as is documented CGEventType eventTypes[] = { kCGEventLeftMouseDown, kCGEventLeftMouseUp, kCGEventRightMouseDown, kCGEventRightMouseUp, kCGEventMouseMoved, kCGEventLeftMouseDragged, kCGEventRightMouseDragged, kCGEventKeyDown, kCGEventKeyUp, kCGEventFlagsChanged, kCGEventScrollWheel, kCGEventTabletPointer, kCGEventTabletProximity, kCGEventOtherMouseDown, kCGEventOtherMouseUp, kCGEventOtherMouseDragged, kCGEventNull }; double max_event_time = 0, event_time; double current_time = CFAbsoluteTimeGetCurrent(); static double previous_time = 0; for (CGEventType *eventType = eventTypes ; *eventType != kCGEventNull ; eventType++) { event_time = CGEventSourceSecondsSinceLastEventType(kCGEventSourceStateCombinedSessionState, *eventType); if (event_time > max_event_time) max_event_time = event_time; } const double NANOSECONDS = 1000000000; static double event_time_offset = 0; event_time = current_time - (max_event_time / NANOSECONDS) + event_time_offset; if (fabs(previous_time - current_time) > 5) { // calibrate offset for first time or after sleep/wake, etc. UpdateSystemActivity(UsrActivity); // despite what the docs say, kCGAnyInputEventType includes UpdateSystemActivity event_time_offset = (CGEventSourceSecondsSinceLastEventType(kCGEventSourceStateCombinedSessionState, kCGAnyInputEventType) / NANOSECONDS) - CFAbsoluteTimeGetCurrent(); // NSLog(@"reset offset to %.2f", event_time_offset); idle_time = current_time - (max_event_time / NANOSECONDS) + event_time_offset; } else { idle_time = event_time; } previous_time = current_time; } // NSLog(@"CGEventSource %.2f, CGS %.2f", idle_time, cgs_idle_time); // 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; micro_pause_taking_t = 0; 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; } // update window [progress setDoubleValue:micro_pause_taking_t]; [self drawTimeLeft:micro_pause_duration - micro_pause_taking_t]; [self drawNextBreak:work_break_period - work_break_t]; // if user likes to be interrupted if (lock_focus) { [NSApp activateIgnoringOtherApps:YES]; [main_window makeKeyAndOrderFront:self]; } // 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; } // draw window [progress setDoubleValue:work_break_taking_t / 60 - 0.5]; [self drawTimeLeft:work_break_duration - work_break_taking_t]; [self drawNextBreak:work_break_period + work_break_duration - work_break_taking_t]; // if user likes to be interrupted if (lock_focus) { [NSApp activateIgnoringOtherApps:YES]; [main_window makeKeyAndOrderFront:self]; } // 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]; } // 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]; [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]; } // done with micro pause or work break - (void)endBreak { [main_window orderOut:NULL]; state = s_normal; // reset time interval to user's choice [self installTimer:sample_interval]; } // display micro_pause window with appropriate widgets and progress bar - (void)doMicroPause { micro_pause_taking_t = 0; [view setImage:micro_pause_image]; [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 tick: nil]; [main_window center]; [main_window orderFrontRegardless]; } // display work_break window with appropriate widgets and progress bar - (void)doWorkBreak { work_break_taking_t = 0; [view setImage:work_break_image]; [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 tick: nil]; [main_window center]; [main_window orderFrontRegardless]; } // diplays time left - (void)drawTimeLeft:(double)seconds { [time setStringValue:[NSString stringWithFormat:@"%d:%02d", lrint(seconds) / 60, lrint(seconds) % 60]]; } // displays next break - (void)drawNextBreak:(int)seconds { int minutes = round(seconds / 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]]; } } // 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_t -= 10*60; // decrease with 10 minutes if (work_break_t < 0) work_break_t = 0; [self endBreak]; } } - (IBAction)breakNow:(id)sender { [self doWorkBreak]; } // validate menu items - (BOOL)validateMenuItem:(NSMenuItem *)anItem { if ([[anItem title] isEqualToString:@"Take Break Now"] && state == s_normal) { return YES; } if ([[anItem title] isEqualToString:@"Postpone Break"] && state == s_taking_work_break) { 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