source: trunk/Cocoa/Pester/Source/PSAlarm.m@ 76

Last change on this file since 76 was 61, checked in by Nicholas Riley, 22 years ago

Pester 1.1b1.

PSPowerManager: Fixed delegate method selectors to better reflect what
is going on (Apple's docs in IOKit Fundamentals help with this; the
kIOMessage*Sleep constants are really poorly named).

VERSION: Updated for 1.1b1.

PSSpeechAlert.h: Fixed company name.

PSAlert.[hm]: Added -prepareForAlarm: to support PSWakeAlert.

PSTimer.[hm]: Replacement for NSTimer that works properly across
sleep/wake cycles and will schedule wake timers.

PSAlerts.[hm]: Added -prepareForAlarm: to support PSWakeAlert.

Read Me.rtfd: Updated for 1.1b1.

PSAlarm.[hm]: Added -setWakeUp:, invoke -[PSAlerts prepareForAlarm],
replaced alarm timer NSTimer with PSTimer.

PSApplication.[hm]: Replaced dock update timer NSTimer with PSTimer.
Uncovered some issues, need to fix later. Enable alarm discard for
beta release.

PSWakeAlert.[hm]: Shared alert implementation for wakeup. Doesn't do
anything at trigger time, but uses new preparation interface to work
at alarm set time (should work for repeating alarms too, but I didn't
bother to test...)

PSAlarmSetController.m: Added support for PSWakeAlert. Save default
alert information on quit. Removed debug statements on hide/unhide;
it works fine regardless of whether the app is explicitly hidden or
the window hides itself.

PSAlarms.m: PSTimer support - invoke +[PSTimer setUp] to initialize
timer list.

File size: 17.9 KB
RevLine 
[24]1//
2// PSAlarm.m
3// Pester
4//
5// Created by Nicholas Riley on Wed Oct 09 2002.
6// Copyright (c) 2002 Nicholas Riley. All rights reserved.
7//
8
9#import "PSAlarm.h"
[34]10#import "PSAlert.h"
[53]11#import "PSAlerts.h"
[61]12#import "PSTimer.h"
[43]13#import "NJRDateFormatter.h"
[53]14#import "NSCalendarDate-NJRExtensions.h"
15#import "NSDictionary-NJRExtensions.h"
16#import "NSString-NJRExtensions.h"
[24]17
[26]18NSString * const PSAlarmTimerSetNotification = @"PSAlarmTimerSetNotification";
19NSString * const PSAlarmTimerExpiredNotification = @"PSAlarmTimerExpiredNotification";
[53]20NSString * const PSAlarmDiedNotification = @"PSAlarmDiedNotification";
[24]21
[53]22// property list keys
23static NSString * const PLAlarmType = @"type"; // NSString
24static NSString * const PLAlarmDate = @"date"; // NSNumber
25static NSString * const PLAlarmInterval = @"interval"; // NSNumber
26static NSString * const PLAlarmSnoozeInterval = @"snooze interval"; // NSNumber
27static NSString * const PLAlarmMessage = @"message"; // NSString
28static NSString * const PLAlarmAlerts = @"alerts"; // NSDictionary
29static NSString * const PLAlarmRepeating = @"repeating"; // NSNumber
30
[43]31static NSString *dateFormat, *shortDateFormat, *timeFormat;
32static NSDictionary *locale;
33
[34]34// XXX need to reset pending alarms after sleep, they "freeze" and never expire.
35
[24]36@implementation PSAlarm
37
[51]38#pragma mark initialize-release
39
[43]40+ (void)initialize; // XXX change on locale modification, subscribe to NSNotifications
41{
42 dateFormat = [[NJRDateFormatter localizedDateFormatIncludingWeekday: YES] retain];
43 shortDateFormat = [[NJRDateFormatter localizedShortDateFormatIncludingWeekday: NO] retain];
44 timeFormat = [[NJRDateFormatter localizedTimeFormatIncludingSeconds: YES] retain];
45 locale = [[[NSUserDefaults standardUserDefaults] dictionaryRepresentation] retain];
46}
47
[26]48- (void)dealloc;
49{
50 // NSLog(@"DEALLOC %@", self);
51 alarmType = PSAlarmInvalid;
52 [alarmDate release]; alarmDate = nil;
53 [alarmMessage release]; alarmMessage = nil;
54 [invalidMessage release]; invalidMessage = nil;
[28]55 [timer invalidate]; [timer release]; timer = nil;
[34]56 [alerts release]; alerts = nil;
[26]57 [super dealloc];
58}
59
[51]60#pragma mark private
61
[24]62- (void)_setAlarmDate:(NSCalendarDate *)aDate;
63{
64 if (alarmDate != aDate) {
65 [alarmDate release];
66 alarmDate = nil;
67 alarmDate = [aDate retain];
68 }
69}
70
[51]71- (void)_beInvalid:(NSString *)aMessage;
[24]72{
73 alarmType = PSAlarmInvalid;
74 if (aMessage != invalidMessage) {
75 [invalidMessage release];
76 invalidMessage = nil;
77 [self _setAlarmDate: nil];
78 alarmInterval = 0;
79 invalidMessage = [aMessage retain];
80 }
81}
82
[51]83- (void)_beValidWithType:(PSAlarmType)type;
[24]84{
[28]85 if (alarmType == PSAlarmSet) return; // already valid
[24]86 [invalidMessage release];
87 invalidMessage = nil;
88 alarmType = type;
[53]89 if (type != PSAlarmInterval) [self setRepeating: NO];
[24]90}
91
92- (void)_setDateFromInterval;
93{
[53]94 [self _setAlarmDate: [NSCalendarDate dateWithTimeIntervalSinceNow: alarmInterval]];
[51]95 [self _beValidWithType: PSAlarmInterval];
[24]96}
97
[51]98- (void)_setIntervalFromDate;
[24]99{
[53]100 alarmInterval = [alarmDate timeIntervalSinceNow];
[24]101 if (alarmInterval <= 0) {
[51]102 [self _beInvalid: @"Please specify an alarm time in the future."];
103 return;
[24]104 }
[51]105 [self _beValidWithType: PSAlarmDate];
[24]106}
107
[53]108- (PSAlarmType)_alarmTypeForString:(NSString *)string;
109{
110 if ([string isEqualToString: @"PSAlarmDate"]) return PSAlarmDate;
111 if ([string isEqualToString: @"PSAlarmInterval"]) return PSAlarmInterval;
112 if ([string isEqualToString: @"PSAlarmSet"]) return PSAlarmSet;
113 if ([string isEqualToString: @"PSAlarmInvalid"]) return PSAlarmInvalid;
114 if ([string isEqualToString: @"PSAlarmSnooze"]) return PSAlarmSnooze;
115 if ([string isEqualToString: @"PSAlarmExpired"]) return PSAlarmExpired;
116 NSLog(@"unknown alarm type string: %@", string);
117 return nil;
118}
119
[51]120- (NSString *)_alarmTypeString;
[24]121{
[51]122 switch (alarmType) {
123 case PSAlarmDate: return @"PSAlarmDate";
124 case PSAlarmInterval: return @"PSAlarmInterval";
125 case PSAlarmSet: return @"PSAlarmSet";
126 case PSAlarmInvalid: return @"PSAlarmInvalid";
[53]127 case PSAlarmSnooze: return @"PSAlarmSnooze";
128 case PSAlarmExpired: return @"PSAlarmExpired";
[51]129 default: return [NSString stringWithFormat: @"<unknown: %u>", alarmType];
130 }
131}
132
[53]133- (NSString *)_stringForInterval:(unsigned long long)interval;
134{
135 const unsigned long long minute = 60, hour = minute * 60, day = hour * 24, year = day * 365.26;
136 // +[NSString stringWithFormat:] in 10.1 does not support long longs: work around it by converting to unsigned ints or longs for display
137 if (interval == 0) return nil;
138 if (interval < minute) return [NSString stringWithFormat: @"%us", (unsigned)interval];
139 if (interval < day) return [NSString stringWithFormat: @"%uh %um", (unsigned)(interval / hour), (unsigned)((interval % hour) / minute)];
140 if (interval < 2 * day) return @"One day";
141 if (interval < year) return [NSString stringWithFormat: @"%u days", (unsigned)(interval / day)];
142 if (interval < 2 * year) return @"One year";
143 return [NSString stringWithFormat: @"%lu years", (unsigned long)(interval / year)];
144}
145
[61]146- (void)_timerExpired:(PSTimer *)aTimer;
[51]147{
[53]148 NSLog(@"expired: %@; now %@", [[aTimer fireDate] description], [[NSDate date] description]);
149 alarmType = PSAlarmExpired;
[51]150 [[NSNotificationCenter defaultCenter] postNotificationName: PSAlarmTimerExpiredNotification object: self];
151 [timer release]; timer = nil;
152}
153
154#pragma mark alarm setting
155
156- (void)setInterval:(NSTimeInterval)anInterval;
157{
158 alarmInterval = anInterval;
[24]159 if (alarmInterval <= 0) {
[51]160 [self _beInvalid: @"Please specify an alarm interval."]; return;
[24]161 }
[51]162 [self _setDateFromInterval];
[24]163}
164
[26]165- (void)setForDateAtTime:(NSCalendarDate *)dateTime;
[24]166{
[26]167 [self _setAlarmDate: dateTime];
[24]168 [self _setIntervalFromDate];
169}
170
171- (void)setForDate:(NSDate *)date atTime:(NSDate *)time;
172{
[53]173 NSCalendarDate *dateTime;
[24]174 if (time == nil && date == nil) {
[51]175 [self _beInvalid: @"Please specify an alarm date and time."]; return;
[24]176 }
177 if (time == nil) {
[51]178 [self _beInvalid: @"Please specify an alarm time."]; return;
[24]179 }
180 if (date == nil) {
[51]181 [self _beInvalid: @"Please specify an alarm date."]; return;
[24]182 }
183 // XXX if calTime's date is different from the default date, complain
[53]184 dateTime = [NSCalendarDate dateWithDate: date atTime: time];
185 if (dateTime == nil) {
[51]186 [self _beInvalid: @"Please specify a reasonable date and time."];
[24]187 }
[53]188 [self setForDateAtTime: dateTime];
[24]189}
190
[53]191- (void)setRepeating:(BOOL)isRepeating;
192{
193 repeating = isRepeating;
194}
195
196- (void)setSnoozeInterval:(NSTimeInterval)anInterval;
197{
198 snoozeInterval = anInterval;
199 NSAssert(alarmType == PSAlarmExpired, @"CanÕt snooze an alarm that hasnÕt expired");
200 alarmType = PSAlarmSnooze;
201}
202
[61]203- (void)setWakeUp:(BOOL)doWake;
204{
205 [timer setWakeUp: doWake];
206}
207
[51]208#pragma mark accessing
209
210- (NSString *)message;
[24]211{
[51]212 if (alarmMessage == nil || [alarmMessage isEqualToString: @""])
213 return @"Alarm!";
214 return alarmMessage;
[24]215}
216
217- (void)setMessage:(NSString *)aMessage;
218{
219 if (aMessage != alarmMessage) {
220 [alarmMessage release];
221 alarmMessage = nil;
222 alarmMessage = [aMessage retain];
223 }
224}
225
[51]226- (BOOL)isValid;
[24]227{
[51]228 if (alarmType == PSAlarmDate) [self _setIntervalFromDate];
[53]229 if (alarmType == PSAlarmInvalid ||
230 (alarmType == PSAlarmExpired && ![self isRepeating])) return NO;
231 return YES;
[24]232}
233
234- (NSString *)invalidMessage;
235{
[26]236 if (invalidMessage == nil) return @"";
[24]237 return invalidMessage;
238}
239
[28]240- (NSCalendarDate *)date;
[24]241{
242 if (alarmType == PSAlarmInterval) [self _setDateFromInterval];
243 return alarmDate;
244}
245
[51]246- (NSCalendarDate *)time;
247{
248 if (alarmType == PSAlarmInterval) [self _setDateFromInterval];
249 return [[NSCalendarDate alloc] initWithYear: 0
250 month: 1
251 day: 1
252 hour: [alarmDate hourOfDay]
253 minute: [alarmDate minuteOfHour]
254 second: [alarmDate secondOfMinute]
255 timeZone: nil];
256}
257
258- (NSTimeInterval)interval;
259{
[53]260 if (alarmType == PSAlarmDate) [self _setIntervalFromDate];
[51]261 return alarmInterval;
262}
263
[53]264- (NSTimeInterval)snoozeInterval;
[51]265{
[53]266 return snoozeInterval;
[51]267}
268
[53]269- (NSTimeInterval)timeRemaining;
[51]270{
[53]271 NSAssert1(alarmType == PSAlarmSet, @"CanÕt get time remaining on alarm with no timer set: %@", self);
272 return -[[NSDate date] timeIntervalSinceDate: alarmDate];
[51]273}
274
[53]275- (void)setAlerts:(PSAlerts *)theAlerts;
[51]276{
[53]277 [alerts release]; alerts = nil;
278 alerts = [theAlerts retain];
[51]279}
280
[53]281- (PSAlerts *)alerts;
282{
283 if (alerts == nil) alerts = [[PSAlerts alloc] init];
284 return alerts;
285}
286
287- (BOOL)isRepeating;
288{
289 return repeating;
290}
291
[43]292- (NSString *)dateString;
293{
294 return [[self date] descriptionWithCalendarFormat: dateFormat locale: locale];
295}
296
[28]297- (NSString *)shortDateString;
298{
[43]299 return [[self date] descriptionWithCalendarFormat: shortDateFormat locale: locale];
[28]300}
301
302- (NSString *)timeString;
303{
[43]304 return [[self date] descriptionWithCalendarFormat: timeFormat locale: locale];
[28]305}
306
[53]307- (NSString *)dateTimeString;
[28]308{
[53]309 return [NSString stringWithFormat: @"%@ at %@", [self dateString], [self timeString]];
310}
311
312- (NSString *)nextDateTimeString;
313{
314 if (![self isRepeating]) {
315 return nil;
316 } else {
317 NSCalendarDate *date = [[NSCalendarDate alloc] initWithTimeIntervalSinceNow: [self interval]];
318 NSString *nextDateTimeString = [NSString stringWithFormat: @"%@ at %@",
319 [date descriptionWithCalendarFormat: dateFormat locale: locale],
320 [date descriptionWithCalendarFormat: timeFormat locale: locale]];
321 [date release];
322 return nextDateTimeString;
323 }
324}
325
326- (NSString *)intervalString;
327{
328 const unsigned long long mval = 99, minute = 60, hour = minute * 60;
[28]329 unsigned long long interval = [self interval];
[53]330 if (interval == 0) return nil;
331 if (interval == 1) return @"One second";
332 if (interval == minute) return @"One minute";
333 if (interval % minute == 0) return [NSString stringWithFormat: @"%u minutes", (unsigned)(interval / minute)];
334 if (interval <= mval) return [NSString stringWithFormat: @"%u seconds", (unsigned)interval];
335 if (interval == hour) return @"One hour";
336 if (interval % hour == 0) return [NSString stringWithFormat: @"%u hours", (unsigned)(interval / hour)];
337 if (interval <= mval * minute) return [NSString stringWithFormat: @"%u minutes", (unsigned)(interval / minute)];
338 if (interval <= mval * hour) return [NSString stringWithFormat: @"%u hours", (unsigned)(interval / hour)];
339 return [self _stringForInterval: interval];
[28]340}
341
[53]342- (NSString *)timeRemainingString;
343{
344 NSString *timeRemainingString = [self _stringForInterval: llround([self timeRemaining])];
345
346 if (timeRemainingString == nil) return @"ÇexpiredÈ";
347 return timeRemainingString;
348}
349
350- (NSAttributedString *)prettyDescription;
351{
352 NSMutableAttributedString *string = [[NSMutableAttributedString alloc] init];
353 NSAttributedString *alertList = [alerts prettyList];
354
355 [string appendAttributedString:
356 [[NSString stringWithFormat: @"At alarm time for Ò%@Ó:\n", [self message]] small]];
357 if (alertList != nil) {
358 [string appendAttributedString: alertList];
359 } else {
360 [string appendAttributedString: [@"Do nothing." small]];
361 }
362 if ([self isRepeating]) {
363 [string appendAttributedString:
364 [[NSString stringWithFormat: @"\nAlarm repeats every %@.", [[self intervalString] lowercaseString]] small]];
365 }
366 return [string autorelease];
367}
368
[51]369#pragma mark actions
[24]370
[26]371- (BOOL)setTimer;
372{
[53]373 if (alarmType == PSAlarmExpired) {
374 if ([self isRepeating]) {
375 [self _setDateFromInterval];
376 } else {
377 [[NSNotificationCenter defaultCenter] postNotificationName: PSAlarmDiedNotification object: self];
[26]378 return NO;
[53]379 }
380 } else if (alarmType == PSAlarmDate) {
381 if (![self isValid]) return NO;
382 } else if (alarmType == PSAlarmSnooze) {
383 [self _setAlarmDate: [NSCalendarDate dateWithTimeIntervalSinceNow: snoozeInterval]];
384 } else if (alarmType != PSAlarmInterval) {
385 return NO;
[26]386 }
[61]387 timer = [PSTimer scheduledTimerWithTimeInterval: (alarmType == PSAlarmSnooze ? snoozeInterval : alarmInterval) target: self selector: @selector(_timerExpired:) userInfo: nil repeats: NO];
[53]388 if (timer == nil) return NO;
389 [timer retain];
390 alarmType = PSAlarmSet;
[61]391 [alerts prepareForAlarm: self];
392
[53]393 [[NSNotificationCenter defaultCenter] postNotificationName: PSAlarmTimerSetNotification object: self];
394 // NSLog(@"set: %@; now %@; remaining %@", [[timer fireDate] description], [[NSDate date] description], [self timeRemainingString]);
395 return YES;
[26]396}
397
[51]398- (void)cancelTimer;
[26]399{
[28]400 [timer invalidate]; [timer release]; timer = nil;
[53]401 [self setRepeating: NO];
[26]402}
403
[51]404#pragma mark comparing
[26]405
[51]406- (NSComparisonResult)compareDate:(PSAlarm *)otherAlarm;
[26]407{
408 return [[self date] compare: [otherAlarm date]];
409}
410
[51]411- (NSComparisonResult)compareMessage:(PSAlarm *)otherAlarm;
[34]412{
[51]413 return [[self message] caseInsensitiveCompare: [otherAlarm message]];
[34]414}
415
[51]416#pragma mark printing
[34]417
[26]418- (NSString *)description;
419{
420 return [NSString stringWithFormat: @"%@: type %@ date %@ interval %.1f%@",
421 [super description], [self _alarmTypeString], alarmDate, alarmInterval,
422 (alarmType == PSAlarmInvalid ?
423 [NSString stringWithFormat: @"\ninvalid message: %@", invalidMessage]
424 : (alarmType == PSAlarmSet ?
425 [NSString stringWithFormat: @"\ntimer: %@", timer] : @""))];
426}
427
[53]428#pragma mark property list serialization (Pester 1.1)
[51]429
[53]430- (NSDictionary *)propertyListRepresentation;
431{
432 NSMutableDictionary *dict = [NSMutableDictionary dictionaryWithCapacity: 5];
433 if (![self isValid]) return nil;
434 [dict setObject: [self _alarmTypeString] forKey: PLAlarmType];
435 switch (alarmType) {
436 case PSAlarmDate:
437 case PSAlarmSet:
438 [dict setObject: [NSNumber numberWithDouble: [alarmDate timeIntervalSinceReferenceDate]] forKey: PLAlarmDate];
439 break;
440 case PSAlarmSnooze:
441 case PSAlarmInterval:
442 case PSAlarmExpired:
443 [dict setObject: [NSNumber numberWithDouble: alarmInterval] forKey: PLAlarmInterval];
444 [dict setObject: [NSNumber numberWithBool: repeating] forKey: PLAlarmRepeating];
445 break;
446 default:
447 NSAssert1(NO, @"CanÕt save alarm type %@", [self _alarmTypeString]);
448 break;
449 }
450 if (snoozeInterval != 0)
451 [dict setObject: [NSNumber numberWithDouble: snoozeInterval] forKey: PLAlarmSnoozeInterval];
452 [dict setObject: alarmMessage forKey: PLAlarmMessage];
453 if (alerts != nil) {
454 [dict setObject: [alerts propertyListRepresentation] forKey: PLAlarmAlerts];
455 }
456 return dict;
457}
458
459- (id)initWithPropertyList:(NSDictionary *)dict;
460{
461 if ( (self = [self init]) != nil) {
462 PSAlerts *alarmAlerts;
463 alarmType = [self _alarmTypeForString: [dict objectForRequiredKey: PLAlarmType]];
464 switch (alarmType) {
465 case PSAlarmDate:
466 case PSAlarmSet:
467 { NSCalendarDate *date = [[NSCalendarDate alloc] initWithTimeIntervalSinceReferenceDate: [[dict objectForRequiredKey: PLAlarmDate] doubleValue]];
468 [self _setAlarmDate: date];
469 [date release];
470 }
471 break;
472 case PSAlarmSnooze: // snooze interval set but not confirmed; ignore
473 alarmType = PSAlarmExpired;
474 case PSAlarmInterval:
475 case PSAlarmExpired:
476 alarmInterval = [[dict objectForRequiredKey: PLAlarmInterval] doubleValue];
477 repeating = [[dict objectForRequiredKey: PLAlarmRepeating] boolValue];
478 break;
479 default:
480 NSAssert1(NO, @"CanÕt load alarm type %@", [self _alarmTypeString]);
481 break;
482 }
483 snoozeInterval = [[dict objectForKey: PLAlarmSnoozeInterval] doubleValue];
484 [self setMessage: [dict objectForRequiredKey: PLAlarmMessage]];
485 alarmAlerts = [[PSAlerts alloc] initWithPropertyList: [dict objectForRequiredKey: PLAlarmAlerts]];
486 [self setAlerts: alarmAlerts];
487 [alarmAlerts release];
488 if (alarmType == PSAlarmSet) {
489 alarmType = PSAlarmDate;
490 [self setTimer];
491 }
492 if (alarmType == PSAlarmExpired) {
493 [self setTimer];
494 if (alarmType == PSAlarmExpired) { // failed to restart
495 [self release];
496 self = nil;
497 }
498 }
499 }
500 return self;
501}
502
503#pragma mark archiving (Pester 1.0)
504
[26]505- (void)encodeWithCoder:(NSCoder *)coder;
506{
507 if (![self isValid]) return;
508 [coder encodeValueOfObjCType: @encode(PSAlarmType) at: &alarmType];
509 switch (alarmType) {
510 case PSAlarmDate:
511 case PSAlarmSet:
512 [coder encodeObject: alarmDate];
513 break;
514 case PSAlarmInterval:
515 [coder encodeValueOfObjCType: @encode(NSTimeInterval) at: &alarmInterval];
516 break;
517 default:
518 break;
519 }
520 [coder encodeObject: alarmMessage];
[28]521 // NSLog(@"encoded: %@", self); // XXX happening twice, gdb refuses to show proper backtrace, grr
[26]522 return;
523}
524
525- (id)initWithCoder:(NSCoder *)coder;
526{
[53]527 if ( (self = [self init]) != nil) {
528 PSAlerts *legacyAlerts = [[PSAlerts alloc] initWithPesterVersion1Alerts];
529 [self setAlerts: legacyAlerts];
530 [legacyAlerts release];
[26]531 [coder decodeValueOfObjCType: @encode(PSAlarmType) at: &alarmType];
532 switch (alarmType) {
533 case PSAlarmDate:
534 case PSAlarmSet:
535 [self _setAlarmDate: [coder decodeObject]];
536 break;
537 case PSAlarmInterval:
538 [coder decodeValueOfObjCType: @encode(NSTimeInterval) at: &alarmInterval];
539 break;
540 default:
541 break;
542 }
543 [self setMessage: [coder decodeObject]];
544 if (alarmType == PSAlarmSet) {
545 alarmType = PSAlarmDate;
546 [self setTimer];
547 }
548 }
549 return self;
550}
551
[24]552@end
Note: See TracBrowser for help on using the repository browser.