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

Last change on this file since 58 was 53, checked in by Nicholas Riley, 21 years ago

Updated for Pester 1.1a5 (very limited release).

Pester 1.1a4 was never released.

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