source: releases/Pester/1.1a5/Source/PSAlarm.m

Last change on this file 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.