source: trunk/Cocoa/AntiRSI.m @ 326

Last change on this file since 326 was 325, checked in by Nicholas Riley, 13 years ago

AntiRSI 1.3 + my changes, for Mac OS X 10.4.2 and earlier

File size: 19.3 KB
Line 
1/*
2 author: Onne Gorter
3 
4 This file is part of AntiRSI.
5 
6 AntiRSI is free software; you can redistribute it and/or modify
7 it under the terms of the GNU General Public License as published by
8 the Free Software Foundation; either version 2 of the License, or
9 (at your option) any later version.
10 
11 AntiRSI is distributed in the hope that it will be useful,
12 but WITHOUT ANY WARRANTY; without even the implied warranty of
13 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14 GNU General Public License for more details.
15 
16 You should have received a copy of the GNU General Public License
17 along with AntiRSI; if not, write to the Free Software
18 Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
19 */
20
21#import "AntiRSI.h"
22
23#include <math.h>
24#include <ApplicationServices/ApplicationServices.h>
25
26extern double CGSSecondsSinceLastInputEvent(unsigned long eventType);
27
28@implementation AntiRSI
29
30// bindings methods
31- (void)setMicro_pause_duration:(float)f
32{
33        micro_pause_duration = round(f);
34        if (s_taking_micro_pause == state) {
35                [progress setMaxValue:micro_pause_duration];
36                [progress setDoubleValue:micro_pause_taking_t];
37        }
38}
39
40- (void)setMicro_pause_period:(float)f
41{       micro_pause_period = 60 * round(f); }
42
43- (void)setWork_break_duration:(float)f
44{   
45        work_break_duration = 60 * round(f);
46        if (s_taking_work_break == state) {
47                [progress setMaxValue:work_break_duration / 60];
48                [progress setDoubleValue:work_break_taking_t / 60 - 0.5];
49        }
50}
51
52- (void)setWork_break_period:(float)f
53{       work_break_period = 60 * round(f); }
54
55- (void)installTimer:(double)interval
56{
57        if (mtimer != nil) {
58                [mtimer invalidate];
59                [mtimer autorelease];
60        }
61        mtimer = [[NSTimer scheduledTimerWithTimeInterval:interval target:self selector:@selector(tick:)
62                                             userInfo:nil repeats:YES] retain];
63}
64
65- (void)setSample_interval:(NSString *)s
66{
67        sample_interval = 1;
68        if ([s isEqualToString:@"Super Smooth"]) sample_interval = 0.1;
69        if ([s isEqualToString:@"Smooth"]) sample_interval = 0.33;
70        if ([s isEqualToString:@"Normal"]) sample_interval = 1;
71        if ([s isEqualToString:@"Low"]) sample_interval = 2;
72       
73        [self installTimer:sample_interval];
74}
75
76- (void)setDraw_dock_image:(BOOL)b
77{
78        draw_dock_image=b;
79        if (!b) {
80                [NSApp setApplicationIconImage:[NSImage imageNamed:@"AntiRSI"]];
81        } else {
82                [self drawDockImage];
83        }
84}
85
86- (void)setBackground:(NSColor *)c
87{
88        [background autorelease];
89        background=[c retain];
90       
91        // make new darkbackground color
92        float r,g,b,a;
93        [background getRed:&r green:&g blue:&b alpha:&a];
94        [darkbackground autorelease];
95        darkbackground=[[NSColor colorWithCalibratedRed:r*0.35 green:g*0.35 blue:b*0.35 alpha:a+0.2] retain];
96       
97        [self drawDockImage];
98}
99
100- (void)setElapsed:(NSColor *)c
101{
102        [elapsed autorelease];
103        elapsed=[c retain];
104        [self drawDockImage];
105}
106
107- (void)setTaking:(NSColor *)c
108{
109        [taking autorelease];
110        taking=[c retain];
111        [self drawDockImage];
112}
113
114// end of bindings
115
116- (void)awakeFromNib
117{
118        // want transparancy
119        [NSColor setIgnoresAlpha:NO];
120       
121        // initial colors
122        elapsed = [[NSColor colorWithCalibratedRed:0.3 green:0.3 blue:0.9 alpha:0.95] retain];
123        taking = [[NSColor colorWithCalibratedRed:0.3 green:0.9 blue:0.3 alpha:0.90] retain];
124        background = [NSColor colorWithCalibratedRed:0.9 green:0.9 blue:0.9 alpha:0.7];
125       
126        //initial values
127        micro_pause_period = 4*60;
128        micro_pause_duration = 13;
129        work_break_period = 50*60;
130        work_break_duration = 8*60;
131        sample_interval = 1;
132       
133        // set current state
134        state = s_normal;
135       
136        // set timers to 0
137        micro_pause_t = 0;
138        work_break_t = 0;
139        micro_pause_taking_t = 0;
140        work_break_taking_t = 0;
141       
142        // setup images
143        micro_pause_image = [NSImage imageNamed:@"micro_pause"];
144        work_break_image = [NSImage imageNamed:@"work_break"];
145
146        // initialize dock image
147        dock_image = [[NSImage alloc] initWithSize:NSMakeSize(128,128)];
148        [dock_image setCacheMode:NSImageCacheNever];
149        original_dock_image = [NSImage imageNamed:@"AntiRSI"];
150        draw_dock_image_q = YES;
151       
152        // setup main window that will show either micropause or workbreak
153        main_window = [[NSWindow alloc] initWithContentRect:[view frame]
154                                                                                          styleMask:NSBorderlessWindowMask
155                                                                                                backing:NSBackingStoreBuffered defer:YES];
156        [main_window setBackgroundColor:[NSColor clearColor]];
157        [main_window setLevel:NSStatusWindowLevel];
158        [main_window setAlphaValue:0.85];
159        [main_window setOpaque:NO];
160        [main_window setHasShadow:NO];
161        [main_window setMovableByWindowBackground:YES];
162        [main_window center];
163        [main_window setContentView:view];
164    [progress setEnabled:NO];
165       
166        // initialze history filter
167        h0 = 0;
168        h1 = 0;
169        h2 = 0;
170       
171        // initialize ticks
172        date = [NSDate timeIntervalSinceReferenceDate];
173       
174        // set background now
175        [self setBackground:background];
176       
177        // create initial values
178        NSMutableDictionary* initial = [NSMutableDictionary dictionaryWithCapacity:10];
179        [initial setObject:[NSNumber numberWithFloat:4] forKey:@"micro_pause_period"];
180        [initial setObject:[NSNumber numberWithFloat:13] forKey:@"micro_pause_duration"];
181        [initial setObject:[NSNumber numberWithFloat:50] forKey:@"work_break_period"];
182        [initial setObject:[NSNumber numberWithFloat:8] forKey:@"work_break_duration"];
183        [initial setObject:@"Normal" forKey:@"sample_interval"];
184        [initial setObject:[NSNumber numberWithBool:YES] forKey:@"draw_dock_image"];
185        [initial setObject:[NSNumber numberWithBool:NO] forKey:@"lock_focus"];
186        [initial setObject:[NSArchiver archivedDataWithRootObject:elapsed] forKey:@"elapsed"];
187        [initial setObject:[NSArchiver archivedDataWithRootObject:taking] forKey:@"taking"];
188        [initial setObject:[NSArchiver archivedDataWithRootObject:background] forKey:@"background"];
189        [[NSUserDefaultsController sharedUserDefaultsController] setInitialValues:initial];
190
191        // bind to defauls controller
192        id dc = [NSUserDefaultsController sharedUserDefaultsController];
193        [self bind:@"micro_pause_period" toObject:dc withKeyPath:@"values.micro_pause_period" options:nil];
194        [self bind:@"micro_pause_duration" toObject:dc withKeyPath:@"values.micro_pause_duration" options:nil];
195        [self bind:@"work_break_period" toObject:dc withKeyPath:@"values.work_break_period" options:nil];
196        [self bind:@"work_break_duration" toObject:dc withKeyPath:@"values.work_break_duration" options:nil];
197        [self bind:@"sample_interval" toObject:dc withKeyPath:@"values.sample_interval" options:nil];
198        [self bind:@"draw_dock_image" toObject:dc withKeyPath:@"values.draw_dock_image" options:nil];
199        [self bind:@"lock_focus" toObject:dc withKeyPath:@"values.lock_focus" options:nil];
200        NSDictionary* unarchive = [NSDictionary dictionaryWithObject:NSUnarchiveFromDataTransformerName forKey:@"NSValueTransformerName"];
201        [self bind:@"elapsed" toObject:dc withKeyPath:@"values.elapsed" options:unarchive];
202        [self bind:@"taking" toObject:dc withKeyPath:@"values.taking" options:unarchive];
203        [self bind:@"background" toObject:dc withKeyPath:@"values.background" options:unarchive];
204
205        // alert every binding
206        [[NSUserDefaultsController sharedUserDefaultsController] revert:self];
207
208        // start the timer
209        [self installTimer:sample_interval];
210}
211
212// tick every second and update status
213- (void)tick:(NSTimer *)timer
214{
215        // calculate time since last tick
216        double new_date = [NSDate timeIntervalSinceReferenceDate];
217        double tick_time = new_date - date;
218        date = new_date;
219       
220        // check if we are still on track of normal time, otherwise we might have slept or something
221        if (tick_time > work_break_duration) {
222                // set timers to 0
223                micro_pause_t = 0;
224                work_break_t = 0;
225                micro_pause_taking_t = micro_pause_duration;
226                work_break_taking_t = work_break_duration;
227                if (s_normal != state) {
228                        [self endBreak];
229                }
230                // and do stuff on next tick
231                return;
232        }
233       
234        if (tick_time > micro_pause_duration && s_taking_work_break != state) {
235                // set micro_pause timers to 0
236                micro_pause_t = 0;
237                micro_pause_taking_t = micro_pause_duration;
238                if (s_normal != state) {
239                        [self endBreak];
240                }
241                // and do stuff on next tick
242                return;
243        }
244       
245        // get idle time in seconds
246        double idle_time = CGSSecondsSinceLastInputEvent(kCGAnyInputEventType);
247    // double cgs_idle_time = idle_time;
248    // from other people's reverse engineering of this function, on MDD G4s this can return a large positive number when input is in progress
249    if (idle_time >= 18446744000.0) {
250        idle_time = 0.0;
251    } else if (CGEventSourceSecondsSinceLastEventType != NULL) {
252        // CGEventSourceSecondsSinceLastEventType in 10.4.2 returns a CGEventTimestamp of the last event, not a CFTimeInterval as is documented
253                CGEventType eventTypes[] = { kCGEventLeftMouseDown, kCGEventLeftMouseUp, kCGEventRightMouseDown, kCGEventRightMouseUp, kCGEventMouseMoved, kCGEventLeftMouseDragged, kCGEventRightMouseDragged, kCGEventKeyDown, kCGEventKeyUp, kCGEventFlagsChanged, kCGEventScrollWheel, kCGEventTabletPointer, kCGEventTabletProximity, kCGEventOtherMouseDown, kCGEventOtherMouseUp, kCGEventOtherMouseDragged, kCGEventNull };
254        double max_event_time = 0, event_time;
255        double current_time = CFAbsoluteTimeGetCurrent();
256        static double previous_time = 0;
257        for (CGEventType *eventType = eventTypes ; *eventType != kCGEventNull ; eventType++) {
258            event_time = CGEventSourceSecondsSinceLastEventType(kCGEventSourceStateCombinedSessionState, *eventType);
259            if (event_time > max_event_time)
260                max_event_time = event_time;
261        }
262        const double NANOSECONDS = 1000000000;
263        static double event_time_offset = 0;
264        event_time = current_time - (max_event_time / NANOSECONDS) + event_time_offset;
265        if (fabs(previous_time - current_time) > 5) {
266            // calibrate offset for first time or after sleep/wake, etc.
267            UpdateSystemActivity(UsrActivity);
268            // despite what the docs say, kCGAnyInputEventType includes UpdateSystemActivity
269            event_time_offset = (CGEventSourceSecondsSinceLastEventType(kCGEventSourceStateCombinedSessionState, kCGAnyInputEventType) / NANOSECONDS) - CFAbsoluteTimeGetCurrent();
270            // NSLog(@"reset offset to %.2f", event_time_offset);
271            idle_time = current_time - (max_event_time / NANOSECONDS) + event_time_offset;
272        } else {
273            idle_time = event_time;
274        }
275        previous_time = current_time;
276    }
277    // NSLog(@"CGEventSource %.2f, CGS %.2f", idle_time, cgs_idle_time);
278   
279        // calculate slack, this gives a sort of 3 history filtered idea.
280        BOOL slack = (h2 + h1 + h0 > 15);
281       
282        // if new event comes in history bumps up
283        if (h0 >= idle_time || idle_time < sample_interval) {
284                h2 = h1;
285                h1 = h0;
286        }
287        h0 = idle_time;
288       
289        switch (state) {
290                case s_normal:
291                        // idle_time needs to be at least 0.3 * micro_pause_duration before kicking in
292                        // but we cut the user some slack based on previous idle_times
293                        if (idle_time <= micro_pause_duration * 0.3 && !slack) {
294                                micro_pause_t += tick_time;
295                                work_break_t += tick_time;
296                                micro_pause_taking_t = 0;
297                                work_break_taking_t = 0;
298                        } else if (micro_pause_t > 0) {
299                        // oke, leaway is over, increase micro_pause_taking_t unless micro_pause is already over
300                                //micro_pause_t stays put
301                                work_break_t += tick_time;
302                                micro_pause_taking_t += tick_time;
303                                work_break_taking_t = 0;
304                        }
305                       
306                        // if micro_pause_taking_t is above micro_pause_duration, then micro pause is over,
307                        // if still idleing workbreak_taking_t kicks in unless it is already over
308                        if (micro_pause_taking_t >= micro_pause_duration && work_break_t > 0) {
309                                work_break_taking_t += tick_time;
310                                micro_pause_t = 0;
311                        }
312                       
313                        // if work_break_taking_t is above work_break_duration, then work break is over
314                        if (work_break_taking_t >= work_break_duration) {
315                                micro_pause_t = 0;
316                                work_break_t = 0;
317                                //micro_pause_taking_t stays put
318                                // work_break_taking_t stays put
319                        }
320               
321                        // if user needs to take a micro pause
322                        if (micro_pause_t >= micro_pause_period) {
323                                // anticipate next workbreak by not issuing this micro_pause ...
324                                if (work_break_t > work_break_period - (micro_pause_period / 2)) {
325                                        work_break_t = work_break_period;
326                                        [self doWorkBreak];
327                                } else {
328                                        [self doMicroPause];
329                                }
330                        }
331                       
332                        // if user needs to take a work break
333                        if (work_break_t >= work_break_period) {
334                                // stop micro_pause stuff
335                                micro_pause_t = 0;
336                                micro_pause_taking_t = micro_pause_duration;
337                                // and display window
338                                [self doWorkBreak];
339                        }
340                break;
341
342                // taking a micro pause with window
343                case s_taking_micro_pause:
344                        // continue updating timers
345                        micro_pause_taking_t += tick_time;
346                        work_break_t += tick_time;
347                       
348                        // if we don't break, or interrupt the break, reset it
349                        if (idle_time < 1 && !slack) {
350                                micro_pause_taking_t = 0;
351                        }
352                               
353                        // update window
354                        [progress setDoubleValue:micro_pause_taking_t];
355                        [self drawTimeLeft:micro_pause_duration - micro_pause_taking_t];
356                        [self drawNextBreak:work_break_period - work_break_t];
357
358                        // if user likes to be interrupted
359                        if (lock_focus) {
360                                [NSApp activateIgnoringOtherApps:YES];
361                                [main_window makeKeyAndOrderFront:self];
362                        }
363                       
364                        // check if we done enough
365                        if (micro_pause_taking_t > micro_pause_duration) {
366                                micro_pause_t = 0;
367                                [self endBreak];
368                        }
369               
370                        // if workbreak must be run ...
371                        if (work_break_t >= work_break_period) {
372                                // stop micro_pause stuff
373                                micro_pause_t = 0;
374                                micro_pause_taking_t = micro_pause_duration;
375                                // and display window
376                                [self doWorkBreak];
377                        } else {
378                double slip = (micro_pause_duration - micro_pause_taking_t) - (int)(micro_pause_duration - micro_pause_taking_t);
379                [self installTimer: slip < 0.1 ? 1 : slip];
380            }
381                        break;
382               
383                // taking a work break with window
384                case s_taking_work_break:
385                        // increase work_break_taking_t
386                        if (idle_time >= 2 || work_break_taking_t < 3) {
387                                work_break_taking_t += tick_time;
388                        }
389                       
390                        // draw window
391                        [progress setDoubleValue:work_break_taking_t / 60 - 0.5];
392                        [self drawTimeLeft:work_break_duration - work_break_taking_t];
393                        [self drawNextBreak:work_break_period + work_break_duration - work_break_taking_t];
394                       
395                        // if user likes to be interrupted
396                        if (lock_focus) {
397                                [NSApp activateIgnoringOtherApps:YES];
398                                [main_window makeKeyAndOrderFront:self];
399                        }
400
401                        // and check if we done enough
402                        if (work_break_taking_t > work_break_duration) {
403                                micro_pause_t = 0;
404                                micro_pause_taking_t = micro_pause_duration;
405                                work_break_t = 0;
406                                work_break_taking_t = work_break_duration;
407                                [self endBreak];
408                        } else {
409                double slip = (work_break_duration - work_break_taking_t) - (int)(work_break_duration - work_break_taking_t);
410                [self installTimer: slip < 0.1 ? 1 : slip];
411            }
412                        break;
413        }
414       
415        // draw dock image
416        if (draw_dock_image) [self drawDockImage];
417}
418
419// draw the dock icon
420- (void)drawDockImage
421{
422        [dock_image lockFocus];
423       
424        // clear all
425        [[NSColor clearColor] set]; 
426        NSRectFill(NSMakeRect(0,0,127,127));
427       
428        NSBezierPath* p;
429        float end;
430       
431        //draw background circle
432        [darkbackground set];
433        p =[NSBezierPath bezierPathWithOvalInRect:NSMakeRect(6,6,115,115)];
434        [p setLineWidth:4];
435        [p stroke];
436       
437        //fill
438        [background set];
439        [[NSBezierPath bezierPathWithOvalInRect:NSMakeRect(8,8,111,111)] fill];
440       
441        //put dot in middle
442        [darkbackground set];
443        [[NSBezierPath bezierPathWithOvalInRect:NSMakeRect(59,59,9,9)] fill];
444
445        // reuse this one
446        p = [NSBezierPath bezierPath];
447
448        // draw work_break
449        [elapsed set];
450        end = 360 - (360.0 / work_break_period * work_break_t - 90);
451        if (end <= 90) end=90.1;
452        [p appendBezierPathWithArcWithCenter:NSMakePoint(63.5, 63.5) radius:40 startAngle:90 endAngle:end clockwise:YES];
453        [p setLineWidth:22];
454        [p stroke];
455       
456        // draw work break taking
457        [taking set];
458        [p removeAllPoints];
459        end = 360 - (360.0 / work_break_duration * work_break_taking_t - 90);
460        if (end <= 90) end=90.1;
461        [p appendBezierPathWithArcWithCenter:NSMakePoint(63.5, 63.5) radius:40 startAngle:90 endAngle:end clockwise:YES];
462        [p setLineWidth:18];
463        [p stroke];
464       
465        // draw micro pause
466        [elapsed set];
467        [p removeAllPoints];
468        end = 360 - (360.0 / micro_pause_period * micro_pause_t - 90);
469        if (end <= 90) end = 90.1;
470        [p appendBezierPathWithArcWithCenter:NSMakePoint(63.5, 63.5) radius:17 startAngle:90 endAngle:end clockwise:YES];
471        [p setLineWidth:22];
472        [p stroke];
473       
474        // draw micro pause taking
475        [taking set];
476        [p removeAllPoints];
477        end = 360 - (360.0 / micro_pause_duration * micro_pause_taking_t - 90);
478        if (end <= 90) end = 90.1;
479        [p appendBezierPathWithArcWithCenter:NSMakePoint(63.5, 63.5) radius:17 startAngle:90 endAngle:end clockwise:YES];
480        [p setLineWidth:18];
481        [p stroke];
482       
483        [dock_image unlockFocus];
484
485        // and set it in the dock check draw_dock_image one last time ...
486        if (draw_dock_image_q) [NSApp setApplicationIconImage:dock_image];
487}
488
489// done with micro pause or work break
490- (void)endBreak
491{
492        [main_window orderOut:NULL];
493        state = s_normal;
494        // reset time interval to user's choice
495        [self installTimer:sample_interval];
496}
497
498// display micro_pause window with appropriate widgets and progress bar
499- (void)doMicroPause
500{
501        micro_pause_taking_t = 0;
502        [view setImage:micro_pause_image];
503        [progress setMaxValue:micro_pause_duration];
504        [progress setDoubleValue:micro_pause_taking_t];
505    [progress setWarningValue: 1];
506    [progress setCriticalValue: micro_pause_duration];
507        [postpone setHidden:YES];
508        state = s_taking_micro_pause;
509        [self tick: nil];
510        [main_window center];
511        [main_window orderFrontRegardless];
512}
513
514// display work_break window with appropriate widgets and progress bar
515- (void)doWorkBreak
516{
517        work_break_taking_t = 0;
518        [view setImage:work_break_image];
519        [progress setMaxValue:work_break_duration / 60];
520        [progress setDoubleValue:work_break_taking_t / 60 - 0.5];
521    [progress setWarningValue: 0];
522    [progress setCriticalValue: 0.4];
523        [postpone setHidden:NO];
524        state = s_taking_work_break;
525    [self tick: nil];
526        [main_window center];
527        [main_window orderFrontRegardless];
528}
529
530// diplays time left
531- (void)drawTimeLeft:(double)seconds
532{
533        [time setStringValue:[NSString stringWithFormat:@"%d:%02d", lrint(seconds) / 60, lrint(seconds) % 60]];
534}
535
536// displays next break
537- (void)drawNextBreak:(int)seconds
538{
539        int minutes = round(seconds / 60.0) ;
540       
541        // nice hours, minutes ...
542        if (minutes > 60) {
543                [next_break setStringValue:[NSString stringWithFormat:@"next break in %d:%02d hours",
544                        minutes / 60, minutes % 60]];
545        } else {
546                [next_break setStringValue:[NSString stringWithFormat:@"next break in %d minutes", minutes]];
547        }
548}
549
550// stop work break and postpone by 10 minutes
551- (IBAction)postpone:(id)sender
552{
553        if (s_taking_work_break == state) {
554                micro_pause_t = 0;
555                micro_pause_taking_t = 0;
556                work_break_taking_t = 0;
557                work_break_t -= 10*60; // decrease with 10 minutes
558                if (work_break_t < 0) work_break_t = 0;
559                [self endBreak];
560        }
561}
562
563- (IBAction)breakNow:(id)sender
564{
565        [self doWorkBreak];
566}
567
568// validate menu items
569- (BOOL)validateMenuItem:(NSMenuItem *)anItem
570{
571        if ([[anItem title] isEqualToString:@"Take Break Now"] && state == s_normal) {
572                return YES;
573        }
574       
575        if ([[anItem title] isEqualToString:@"Postpone Break"] && state == s_taking_work_break) {
576                return YES;
577        }
578       
579        return NO;
580}
581
582// we are delegate of NSApplication, so we can restore the icon on quit.
583- (void)applicationWillTerminate:(NSNotification *)aNotification
584{
585        // make sure timer doesn't tick once more ...
586        draw_dock_image_q = NO;
587        [mtimer invalidate];
588        [mtimer autorelease];
589        mtimer = nil;
590        [dock_image release];
591        // stupid fix for icon beeing restored ... it is not my fault,
592        // the dock or NSImage or setApplicationIconImage seem to be caching or taking
593        // snapshot or something ... !
594        [NSApp setApplicationIconImage:original_dock_image];
595        [NSThread sleepUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.1]];
596        [NSApp setApplicationIconImage:original_dock_image];
597
598}
599
600@end
601
Note: See TracBrowser for help on using the repository browser.