source: trunk/Cocoa/AntiRSI/AntiRSI.m@ 328

Last change on this file since 328 was 328, checked in by Nicholas Riley, 17 years ago

AntiRSI.m: Remove 10.4.2 workaround; use text instead of images for window title

File size: 18.1 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 CFTimeInterval 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 // initialize dock image
143 dock_image = [[NSImage alloc] initWithSize:NSMakeSize(128,128)];
144 [dock_image setCacheMode:NSImageCacheNever];
145 original_dock_image = [NSImage imageNamed:@"AntiRSI"];
146 draw_dock_image_q = YES;
147
148 // setup main window that will show either micropause or workbreak
149 main_window = [[NSWindow alloc] initWithContentRect:[view frame]
150 styleMask:NSBorderlessWindowMask
151 backing:NSBackingStoreBuffered defer:YES];
152 [main_window setBackgroundColor:[NSColor clearColor]];
153 [main_window setLevel:NSStatusWindowLevel];
154 [main_window setAlphaValue:0.85];
155 [main_window setOpaque:NO];
156 [main_window setHasShadow:NO];
157 [main_window setMovableByWindowBackground:YES];
158 [main_window center];
159 [main_window setContentView:view];
160 [progress setEnabled:NO];
161
162 // initialze history filter
163 h0 = 0;
164 h1 = 0;
165 h2 = 0;
166
167 // initialize ticks
168 date = [NSDate timeIntervalSinceReferenceDate];
169
170 // set background now
171 [self setBackground:background];
172
173 // create initial values
174 NSMutableDictionary* initial = [NSMutableDictionary dictionaryWithCapacity:10];
175 [initial setObject:[NSNumber numberWithFloat:4] forKey:@"micro_pause_period"];
176 [initial setObject:[NSNumber numberWithFloat:13] forKey:@"micro_pause_duration"];
177 [initial setObject:[NSNumber numberWithFloat:50] forKey:@"work_break_period"];
178 [initial setObject:[NSNumber numberWithFloat:8] forKey:@"work_break_duration"];
179 [initial setObject:@"Normal" forKey:@"sample_interval"];
180 [initial setObject:[NSNumber numberWithBool:YES] forKey:@"draw_dock_image"];
181 [initial setObject:[NSNumber numberWithBool:NO] forKey:@"lock_focus"];
182 [initial setObject:[NSArchiver archivedDataWithRootObject:elapsed] forKey:@"elapsed"];
183 [initial setObject:[NSArchiver archivedDataWithRootObject:taking] forKey:@"taking"];
184 [initial setObject:[NSArchiver archivedDataWithRootObject:background] forKey:@"background"];
185 [[NSUserDefaultsController sharedUserDefaultsController] setInitialValues:initial];
186
187 // bind to defauls controller
188 id dc = [NSUserDefaultsController sharedUserDefaultsController];
189 [self bind:@"micro_pause_period" toObject:dc withKeyPath:@"values.micro_pause_period" options:nil];
190 [self bind:@"micro_pause_duration" toObject:dc withKeyPath:@"values.micro_pause_duration" options:nil];
191 [self bind:@"work_break_period" toObject:dc withKeyPath:@"values.work_break_period" options:nil];
192 [self bind:@"work_break_duration" toObject:dc withKeyPath:@"values.work_break_duration" options:nil];
193 [self bind:@"sample_interval" toObject:dc withKeyPath:@"values.sample_interval" options:nil];
194 [self bind:@"draw_dock_image" toObject:dc withKeyPath:@"values.draw_dock_image" options:nil];
195 [self bind:@"lock_focus" toObject:dc withKeyPath:@"values.lock_focus" options:nil];
196 NSDictionary* unarchive = [NSDictionary dictionaryWithObject:NSUnarchiveFromDataTransformerName forKey:@"NSValueTransformerName"];
197 [self bind:@"elapsed" toObject:dc withKeyPath:@"values.elapsed" options:unarchive];
198 [self bind:@"taking" toObject:dc withKeyPath:@"values.taking" options:unarchive];
199 [self bind:@"background" toObject:dc withKeyPath:@"values.background" options:unarchive];
200
201 // alert every binding
202 [[NSUserDefaultsController sharedUserDefaultsController] revert:self];
203
204 // start the timer
205 [self installTimer:sample_interval];
206}
207
208// tick every second and update status
209- (void)tick:(NSTimer *)timer
210{
211 // calculate time since last tick
212 double new_date = [NSDate timeIntervalSinceReferenceDate];
213 double tick_time = new_date - date;
214 date = new_date;
215
216 // check if we are still on track of normal time, otherwise we might have slept or something
217 if (tick_time > work_break_duration) {
218 // set timers to 0
219 micro_pause_t = 0;
220 work_break_t = 0;
221 micro_pause_taking_t = micro_pause_duration;
222 work_break_taking_t = work_break_duration;
223 if (s_normal != state) {
224 [self endBreak];
225 }
226 // and do stuff on next tick
227 return;
228 }
229
230 if (tick_time > micro_pause_duration && s_taking_work_break != state) {
231 // set micro_pause timers to 0
232 micro_pause_t = 0;
233 micro_pause_taking_t = micro_pause_duration;
234 if (s_normal != state) {
235 [self endBreak];
236 }
237 // and do stuff on next tick
238 return;
239 }
240
241 // get idle time in seconds
242 CFTimeInterval idle_time = CGSSecondsSinceLastInputEvent(kCGAnyInputEventType);
243 // CFTimeInterval cgs_idle_time = idle_time;
244 // from other people's reverse engineering of this function, on MDD G4s this can return a large positive number when input is in progress
245 if (idle_time >= 18446744000.0) {
246 idle_time = 0;
247 } else if (CGEventSourceSecondsSinceLastEventType != NULL) {
248 CGEventType eventTypes[] = { kCGEventLeftMouseDown, kCGEventLeftMouseUp, kCGEventRightMouseDown, kCGEventRightMouseUp, kCGEventMouseMoved, kCGEventLeftMouseDragged, kCGEventRightMouseDragged, kCGEventKeyDown, kCGEventKeyUp, kCGEventFlagsChanged, kCGEventScrollWheel, kCGEventTabletPointer, kCGEventTabletProximity, kCGEventOtherMouseDown, kCGEventOtherMouseUp, kCGEventOtherMouseDragged, kCGEventNull };
249 CFTimeInterval event_idle_time;
250 idle_time = DBL_MAX;
251 for (CGEventType *eventType = eventTypes ; *eventType != kCGEventNull ; eventType++) {
252 event_idle_time = CGEventSourceSecondsSinceLastEventType(kCGEventSourceStateCombinedSessionState, *eventType);
253 if (event_idle_time < idle_time) idle_time = event_idle_time;
254 }
255 }
256 // NSLog(@"CGEventSource %.2f, CGS %.2f", idle_time, cgs_idle_time);
257
258 // calculate slack, this gives a sort of 3 history filtered idea.
259 BOOL slack = (h2 + h1 + h0 > 15);
260
261 // if new event comes in history bumps up
262 if (h0 >= idle_time || idle_time < sample_interval) {
263 h2 = h1;
264 h1 = h0;
265 }
266 h0 = idle_time;
267
268 switch (state) {
269 case s_normal:
270 // idle_time needs to be at least 0.3 * micro_pause_duration before kicking in
271 // but we cut the user some slack based on previous idle_times
272 if (idle_time <= micro_pause_duration * 0.3 && !slack) {
273 micro_pause_t += tick_time;
274 work_break_t += tick_time;
275 micro_pause_taking_t = 0;
276 work_break_taking_t = 0;
277 } else if (micro_pause_t > 0) {
278 // oke, leaway is over, increase micro_pause_taking_t unless micro_pause is already over
279 //micro_pause_t stays put
280 work_break_t += tick_time;
281 micro_pause_taking_t += tick_time;
282 work_break_taking_t = 0;
283 }
284
285 // if micro_pause_taking_t is above micro_pause_duration, then micro pause is over,
286 // if still idleing workbreak_taking_t kicks in unless it is already over
287 if (micro_pause_taking_t >= micro_pause_duration && work_break_t > 0) {
288 work_break_taking_t += tick_time;
289 micro_pause_t = 0;
290 }
291
292 // if work_break_taking_t is above work_break_duration, then work break is over
293 if (work_break_taking_t >= work_break_duration) {
294 micro_pause_t = 0;
295 work_break_t = 0;
296 //micro_pause_taking_t stays put
297 // work_break_taking_t stays put
298 }
299
300 // if user needs to take a micro pause
301 if (micro_pause_t >= micro_pause_period) {
302 // anticipate next workbreak by not issuing this micro_pause ...
303 if (work_break_t > work_break_period - (micro_pause_period / 2)) {
304 work_break_t = work_break_period;
305 [self doWorkBreak];
306 } else {
307 [self doMicroPause];
308 }
309 }
310
311 // if user needs to take a work break
312 if (work_break_t >= work_break_period) {
313 // stop micro_pause stuff
314 micro_pause_t = 0;
315 micro_pause_taking_t = micro_pause_duration;
316 // and display window
317 [self doWorkBreak];
318 }
319 break;
320
321 // taking a micro pause with window
322 case s_taking_micro_pause:
323 // continue updating timers
324 micro_pause_taking_t += tick_time;
325 work_break_t += tick_time;
326
327 // if we don't break, or interrupt the break, reset it
328 if (idle_time < 1 && !slack) {
329 micro_pause_taking_t = 0;
330 }
331
332 // update window
333 [progress setDoubleValue:micro_pause_taking_t];
334 [self drawTimeLeft:micro_pause_duration - micro_pause_taking_t];
335 [self drawNextBreak:work_break_period - work_break_t];
336
337 // if user likes to be interrupted
338 if (lock_focus) {
339 [NSApp activateIgnoringOtherApps:YES];
340 [main_window makeKeyAndOrderFront:self];
341 }
342
343 // check if we done enough
344 if (micro_pause_taking_t > micro_pause_duration) {
345 micro_pause_t = 0;
346 [self endBreak];
347 }
348
349 // if workbreak must be run ...
350 if (work_break_t >= work_break_period) {
351 // stop micro_pause stuff
352 micro_pause_t = 0;
353 micro_pause_taking_t = micro_pause_duration;
354 // and display window
355 [self doWorkBreak];
356 } else {
357 double slip = (micro_pause_duration - micro_pause_taking_t) - (int)(micro_pause_duration - micro_pause_taking_t);
358 [self installTimer: slip < 0.1 ? 1 : slip];
359 }
360 break;
361
362 // taking a work break with window
363 case s_taking_work_break:
364 // increase work_break_taking_t
365 if (idle_time >= 2 || work_break_taking_t < 3) {
366 work_break_taking_t += tick_time;
367 }
368
369 // draw window
370 [progress setDoubleValue:work_break_taking_t / 60 - 0.5];
371 [self drawTimeLeft:work_break_duration - work_break_taking_t];
372 [self drawNextBreak:work_break_period + work_break_duration - work_break_taking_t];
373
374 // if user likes to be interrupted
375 if (lock_focus) {
376 [NSApp activateIgnoringOtherApps:YES];
377 [main_window makeKeyAndOrderFront:self];
378 }
379
380 // and check if we done enough
381 if (work_break_taking_t > work_break_duration) {
382 micro_pause_t = 0;
383 micro_pause_taking_t = micro_pause_duration;
384 work_break_t = 0;
385 work_break_taking_t = work_break_duration;
386 [self endBreak];
387 } else {
388 double slip = (work_break_duration - work_break_taking_t) - (int)(work_break_duration - work_break_taking_t);
389 [self installTimer: slip < 0.1 ? 1 : slip];
390 }
391 break;
392 }
393
394 // draw dock image
395 if (draw_dock_image) [self drawDockImage];
396}
397
398// draw the dock icon
399- (void)drawDockImage
400{
401 [dock_image lockFocus];
402
403 // clear all
404 [[NSColor clearColor] set];
405 NSRectFill(NSMakeRect(0,0,127,127));
406
407 NSBezierPath* p;
408 float end;
409
410 //draw background circle
411 [darkbackground set];
412 p =[NSBezierPath bezierPathWithOvalInRect:NSMakeRect(6,6,115,115)];
413 [p setLineWidth:4];
414 [p stroke];
415
416 //fill
417 [background set];
418 [[NSBezierPath bezierPathWithOvalInRect:NSMakeRect(8,8,111,111)] fill];
419
420 //put dot in middle
421 [darkbackground set];
422 [[NSBezierPath bezierPathWithOvalInRect:NSMakeRect(59,59,9,9)] fill];
423
424 // reuse this one
425 p = [NSBezierPath bezierPath];
426
427 // draw work_break
428 [elapsed set];
429 end = 360 - (360.0 / work_break_period * work_break_t - 90);
430 if (end <= 90) end=90.1;
431 [p appendBezierPathWithArcWithCenter:NSMakePoint(63.5, 63.5) radius:40 startAngle:90 endAngle:end clockwise:YES];
432 [p setLineWidth:22];
433 [p stroke];
434
435 // draw work break taking
436 [taking set];
437 [p removeAllPoints];
438 end = 360 - (360.0 / work_break_duration * work_break_taking_t - 90);
439 if (end <= 90) end=90.1;
440 [p appendBezierPathWithArcWithCenter:NSMakePoint(63.5, 63.5) radius:40 startAngle:90 endAngle:end clockwise:YES];
441 [p setLineWidth:18];
442 [p stroke];
443
444 // draw micro pause
445 [elapsed set];
446 [p removeAllPoints];
447 end = 360 - (360.0 / micro_pause_period * micro_pause_t - 90);
448 if (end <= 90) end = 90.1;
449 [p appendBezierPathWithArcWithCenter:NSMakePoint(63.5, 63.5) radius:17 startAngle:90 endAngle:end clockwise:YES];
450 [p setLineWidth:22];
451 [p stroke];
452
453 // draw micro pause taking
454 [taking set];
455 [p removeAllPoints];
456 end = 360 - (360.0 / micro_pause_duration * micro_pause_taking_t - 90);
457 if (end <= 90) end = 90.1;
458 [p appendBezierPathWithArcWithCenter:NSMakePoint(63.5, 63.5) radius:17 startAngle:90 endAngle:end clockwise:YES];
459 [p setLineWidth:18];
460 [p stroke];
461
462 [dock_image unlockFocus];
463
464 // and set it in the dock check draw_dock_image one last time ...
465 if (draw_dock_image_q) [NSApp setApplicationIconImage:dock_image];
466}
467
468// done with micro pause or work break
469- (void)endBreak
470{
471 [main_window orderOut:NULL];
472 state = s_normal;
473 // reset time interval to user's choice
474 [self installTimer:sample_interval];
475}
476
477// display micro_pause window with appropriate widgets and progress bar
478- (void)doMicroPause
479{
480 micro_pause_taking_t = 0;
481 [status setStringValue:@"Micro Pause"];
482 [progress setMaxValue:micro_pause_duration];
483 [progress setDoubleValue:micro_pause_taking_t];
484 [progress setWarningValue: 1];
485 [progress setCriticalValue: micro_pause_duration];
486 [postpone setHidden:YES];
487 state = s_taking_micro_pause;
488 [self tick: nil];
489 [main_window center];
490 [main_window orderFrontRegardless];
491}
492
493// display work_break window with appropriate widgets and progress bar
494- (void)doWorkBreak
495{
496 work_break_taking_t = 0;
497 [status setStringValue:@"Work Break"];
498 [progress setMaxValue:work_break_duration / 60];
499 [progress setDoubleValue:work_break_taking_t / 60 - 0.5];
500 [progress setWarningValue: 0];
501 [progress setCriticalValue: 0.4];
502 [postpone setHidden:NO];
503 state = s_taking_work_break;
504 [self tick: nil];
505 [main_window center];
506 [main_window orderFrontRegardless];
507}
508
509// diplays time left
510- (void)drawTimeLeft:(double)seconds
511{
512 [time setStringValue:[NSString stringWithFormat:@"%d:%02d", lrint(seconds) / 60, lrint(seconds) % 60]];
513}
514
515// displays next break
516- (void)drawNextBreak:(int)seconds
517{
518 int minutes = round(seconds / 60.0) ;
519
520 // nice hours, minutes ...
521 if (minutes > 60) {
522 [next_break setStringValue:[NSString stringWithFormat:@"next break in %d:%02d hours",
523 minutes / 60, minutes % 60]];
524 } else {
525 [next_break setStringValue:[NSString stringWithFormat:@"next break in %d minutes", minutes]];
526 }
527}
528
529// stop work break and postpone by 10 minutes
530- (IBAction)postpone:(id)sender
531{
532 if (s_taking_work_break == state) {
533 micro_pause_t = 0;
534 micro_pause_taking_t = 0;
535 work_break_taking_t = 0;
536 work_break_t -= 10*60; // decrease with 10 minutes
537 if (work_break_t < 0) work_break_t = 0;
538 [self endBreak];
539 }
540}
541
542- (IBAction)breakNow:(id)sender
543{
544 [self doWorkBreak];
545}
546
547// validate menu items
548- (BOOL)validateMenuItem:(NSMenuItem *)anItem
549{
550 if ([[anItem title] isEqualToString:@"Take Break Now"] && state == s_normal) {
551 return YES;
552 }
553
554 if ([[anItem title] isEqualToString:@"Postpone Break"] && state == s_taking_work_break) {
555 return YES;
556 }
557
558 return NO;
559}
560
561// we are delegate of NSApplication, so we can restore the icon on quit.
562- (void)applicationWillTerminate:(NSNotification *)aNotification
563{
564 // make sure timer doesn't tick once more ...
565 draw_dock_image_q = NO;
566 [mtimer invalidate];
567 [mtimer autorelease];
568 mtimer = nil;
569 [dock_image release];
570 // stupid fix for icon beeing restored ... it is not my fault,
571 // the dock or NSImage or setApplicationIconImage seem to be caching or taking
572 // snapshot or something ... !
573 [NSApp setApplicationIconImage:original_dock_image];
574 [NSThread sleepUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.1]];
575 [NSApp setApplicationIconImage:original_dock_image];
576
577}
578
579@end
580
Note: See TracBrowser for help on using the repository browser.