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

Last change on this file since 618 was 608, checked in by Nicholas Riley, 14 years ago

Display "today" or "tomorrow" when setting an alarm.

File size: 20.7 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 "PSTimer.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 NSDateFormatter *dateFormatter, *shortDateFormatter, *timeFormatter;
31
32static NSCalendar *gregorianCalendar;
33
34static NSDate *midnightOnDate(NSDate *date) {
35 return [gregorianCalendar dateFromComponents:
36 [gregorianCalendar components: NSMonthCalendarUnit|NSDayCalendarUnit|NSYearCalendarUnit fromDate: date]];
37}
38
39@implementation PSAlarm
40
41#pragma mark initialize-release
42
43+ (void)initialize;
44{
45 [NSDateFormatter setDefaultFormatterBehavior: NSDateFormatterBehavior10_4];
46 dateFormatter = [[NSDateFormatter alloc] init];
47 [dateFormatter setTimeStyle: NSDateFormatterNoStyle];
48 [dateFormatter setDateStyle: NSDateFormatterFullStyle];
49 shortDateFormatter = [[NSDateFormatter alloc] init];
50 [shortDateFormatter setTimeStyle: NSDateFormatterNoStyle];
51 [shortDateFormatter setDateStyle: NSDateFormatterShortStyle];
52 timeFormatter = [[NSDateFormatter alloc] init];
53 [timeFormatter setTimeStyle: NSDateFormatterMediumStyle];
54 [timeFormatter setDateStyle: NSDateFormatterNoStyle];
55 gregorianCalendar = [[NSCalendar alloc] initWithCalendarIdentifier: NSGregorianCalendar];
56}
57
58- (void)dealloc;
59{
60 // NSLog(@"DEALLOC %@", self);
61 alarmType = PSAlarmInvalid;
62 [alarmDate release]; alarmDate = nil;
63 [alarmMessage release]; alarmMessage = nil;
64 [invalidMessage release]; invalidMessage = nil;
65 [timer invalidate]; [timer release]; timer = nil;
66 [alerts release]; alerts = nil;
67 [super dealloc];
68}
69
70#pragma mark private
71
72- (void)_setAlarmDate:(NSCalendarDate *)aDate;
73{
74 if (alarmDate != aDate) {
75 [alarmDate release];
76 alarmDate = nil;
77 alarmDate = [aDate retain];
78 }
79}
80
81- (void)_beInvalid:(NSString *)aMessage;
82{
83 alarmType = PSAlarmInvalid;
84 if (aMessage != invalidMessage) {
85 [invalidMessage release];
86 invalidMessage = nil;
87 [self _setAlarmDate: nil];
88 alarmInterval = 0;
89 invalidMessage = [aMessage retain];
90 }
91}
92
93- (void)_beValidWithType:(PSAlarmType)type;
94{
95 if (alarmType == PSAlarmSet) return; // already valid
96 [invalidMessage release];
97 invalidMessage = nil;
98 alarmType = type;
99 if (type != PSAlarmInterval) [self setRepeating: NO];
100}
101
102- (void)_setDateFromInterval;
103{
104 [self _setAlarmDate: [NSCalendarDate dateWithTimeIntervalSinceNow: alarmInterval]];
105 [self _beValidWithType: PSAlarmInterval];
106}
107
108- (void)_setIntervalFromDate;
109{
110 alarmInterval = [alarmDate timeIntervalSinceNow];
111 if (alarmInterval <= 0) {
112 [self _beInvalid: @"Please specify an alarm time in the future."];
113 return;
114 }
115 [self _beValidWithType: PSAlarmDate];
116}
117
118- (PSAlarmType)_alarmTypeForString:(NSString *)string;
119{
120 if ([string isEqualToString: @"PSAlarmDate"]) return PSAlarmDate;
121 if ([string isEqualToString: @"PSAlarmInterval"]) return PSAlarmInterval;
122 if ([string isEqualToString: @"PSAlarmSet"]) return PSAlarmSet;
123 if ([string isEqualToString: @"PSAlarmInvalid"]) return PSAlarmInvalid;
124 if ([string isEqualToString: @"PSAlarmSnooze"]) return PSAlarmSnooze;
125 if ([string isEqualToString: @"PSAlarmExpired"]) return PSAlarmExpired;
126 return PSAlarmInvalid;
127}
128
129- (NSString *)_alarmTypeString;
130{
131 switch (alarmType) {
132 case PSAlarmDate: return @"PSAlarmDate";
133 case PSAlarmInterval: return @"PSAlarmInterval";
134 case PSAlarmSet: return @"PSAlarmSet";
135 case PSAlarmInvalid: return @"PSAlarmInvalid";
136 case PSAlarmSnooze: return @"PSAlarmSnooze";
137 case PSAlarmExpired: return @"PSAlarmExpired";
138 default: return [NSString stringWithFormat: @"<unknown: %u>", alarmType];
139 }
140}
141
142- (NSString *)_stringForInterval:(unsigned long long)interval;
143{
144 const unsigned long long minute = 60, hour = minute * 60, day = hour * 24, year = day * 365.26;
145 // +[NSString stringWithFormat:] in 10.1 does not support long longs: work around it by converting to unsigned ints or longs for display
146 if (interval == 0) return nil;
147 if (interval < minute) return [NSString stringWithFormat: @"%us", (unsigned)interval];
148 if (interval < hour) return [NSString stringWithFormat: @"%um", (unsigned)(interval / minute)];
149 if (interval < day) return [NSString stringWithFormat: @"%uh %um", (unsigned)(interval / hour), (unsigned)((interval % hour) / minute)];
150 if (interval < 2 * day) return @"One day";
151 if (interval < year) return [NSString stringWithFormat: @"%u days", (unsigned)(interval / day)];
152 if (interval < 2 * year) return @"One year";
153 return [NSString stringWithFormat: @"%lu years", (unsigned long)(interval / year)];
154}
155
156- (void)_timerExpired:(PSTimer *)aTimer;
157{
158 NSLog(@"expired: %@; now %@", [[aTimer fireDate] description], [[NSDate date] description]);
159 alarmType = PSAlarmExpired;
160 [[NSNotificationCenter defaultCenter] postNotificationName: PSAlarmTimerExpiredNotification object: self];
161 [timer release]; timer = nil;
162}
163
164#pragma mark alarm setting
165
166- (void)setInterval:(NSTimeInterval)anInterval;
167{
168 alarmInterval = anInterval;
169 if (alarmInterval <= 0) {
170 [self _beInvalid: @"Please specify an alarm interval."]; return;
171 }
172 [self _setDateFromInterval];
173}
174
175- (void)setForDateAtTime:(NSCalendarDate *)dateTime;
176{
177 [self _setAlarmDate: dateTime];
178 [self _setIntervalFromDate];
179}
180
181- (void)setForDate:(NSDate *)aDate atTime:(NSDate *)aTime;
182{
183 NSCalendarDate *dateTime;
184 if (aTime == nil && aDate == nil) {
185 [self _beInvalid: @"Please specify an alarm date and time."]; return;
186 }
187 if (aTime == nil) {
188 [self _beInvalid: @"Please specify an alarm time."]; return;
189 }
190 if (aDate == nil) {
191 [self _beInvalid: @"Please specify an alarm date."]; return;
192 }
193 // XXX if calTime's date is different from the default date, complain
194 dateTime = [NSCalendarDate dateWithDate: aDate atTime: aTime];
195 if (dateTime == nil) {
196 [self _beInvalid: @"Please specify a reasonable date and time."]; return;
197 }
198 [self setForDateAtTime: dateTime];
199}
200
201- (void)setRepeating:(BOOL)isRepeating;
202{
203 repeating = isRepeating;
204}
205
206- (void)setSnoozeInterval:(NSTimeInterval)anInterval;
207{
208 snoozeInterval = anInterval;
209 NSAssert(alarmType == PSAlarmExpired, NSLocalizedString(@"Can't snooze an alarm that hasn't expired", "Assertion for PSAlarm snooze setting"));
210 alarmType = PSAlarmSnooze;
211}
212
213- (void)setWakeUp:(BOOL)doWake;
214{
215 [timer setWakeUp: doWake];
216}
217
218#pragma mark accessing
219
220- (NSString *)message;
221{
222 if (alarmMessage == nil || [alarmMessage isEqualToString: @""])
223 return @"Alarm!";
224 return alarmMessage;
225}
226
227- (void)setMessage:(NSString *)aMessage;
228{
229 if (aMessage != alarmMessage) {
230 [alarmMessage release];
231 alarmMessage = nil;
232 alarmMessage = [aMessage retain];
233 }
234}
235
236- (BOOL)isValid;
237{
238 if (alarmType == PSAlarmDate) [self _setIntervalFromDate];
239 if (alarmType == PSAlarmInvalid ||
240 (alarmType == PSAlarmExpired && ![self isRepeating])) return NO;
241 return YES;
242}
243
244- (NSString *)invalidMessage;
245{
246 if (invalidMessage == nil) return @"";
247 return invalidMessage;
248}
249
250- (NSCalendarDate *)date;
251{
252 if (alarmType == PSAlarmInterval) [self _setDateFromInterval];
253 return alarmDate;
254}
255
256- (NSDate *)midnightOnDate;
257{
258 if (alarmType == PSAlarmInterval) [self _setDateFromInterval];
259
260 return midnightOnDate(alarmDate);
261}
262
263- (NSDate *)time;
264{
265 // XXX this works, but the result is unlikely to be useful until we move away from NSCalendarDate elsewhere
266 if (alarmType == PSAlarmInterval) [self _setDateFromInterval];
267
268 return [gregorianCalendar dateFromComponents:
269 [gregorianCalendar components: NSHourCalendarUnit|NSMinuteCalendarUnit|NSSecondCalendarUnit fromDate: alarmDate]];
270}
271
272- (int)daysFromToday;
273{
274 if (alarmType == PSAlarmInterval) [self _setDateFromInterval];
275
276 return [[gregorianCalendar components: NSDayCalendarUnit fromDate: midnightOnDate([NSDate date]) toDate: alarmDate options: 0] day];
277}
278
279- (NSTimeInterval)interval;
280{
281 if (alarmType == PSAlarmDate) [self _setIntervalFromDate];
282 return alarmInterval;
283}
284
285- (NSTimeInterval)snoozeInterval;
286{
287 return snoozeInterval;
288}
289
290- (NSTimeInterval)timeRemaining;
291{
292 NSAssert1(alarmType == PSAlarmSet, NSLocalizedString(@"Can't get time remaining on alarm with no timer set: %@", "Assertion for PSAlarm time remaining, internal error; %@ replaced by alarm description"), self);
293 return -[[NSDate date] timeIntervalSinceDate: alarmDate];
294}
295
296- (void)setAlerts:(PSAlerts *)theAlerts;
297{
298 [alerts release]; alerts = nil;
299 alerts = [theAlerts retain];
300}
301
302- (PSAlerts *)alerts;
303{
304 if (alerts == nil) alerts = [[PSAlerts alloc] init];
305 return alerts;
306}
307
308- (BOOL)isRepeating;
309{
310 return repeating;
311}
312
313- (NSString *)dateString;
314{
315 return [dateFormatter stringFromDate: [self date]];
316}
317
318- (NSString *)shortDateString;
319{
320 return [shortDateFormatter stringFromDate: [self date]];
321}
322
323- (NSString *)timeString;
324{
325 return [timeFormatter stringFromDate: [self date]];
326}
327
328- (NSString *)dateTimeString;
329{
330 return [NSString stringWithFormat: @"%@ at %@", [self dateString], [self timeString]];
331}
332
333- (NSString *)nextDateTimeString;
334{
335 if (![self isRepeating]) {
336 return nil;
337 } else {
338 NSCalendarDate *date = [[NSCalendarDate alloc] initWithTimeIntervalSinceNow: [self interval]];
339 NSString *nextDateTimeString = [NSString stringWithFormat: @"%@ at %@",
340 [dateFormatter stringFromDate: date],
341 [timeFormatter stringFromDate: date]];
342 [date release];
343 return nextDateTimeString;
344 }
345}
346
347- (NSString *)intervalString;
348{
349 const unsigned long long mval = 999, minute = 60, hour = minute * 60, day = hour * 24, week = day * 7;
350 unsigned long long interval = [self interval];
351 if (interval == 0) return nil;
352
353 if (interval == 1) return @"One second";
354 if (interval == minute) return @"One minute";
355 if (interval == hour) return @"One hour";
356 if (interval == day) return @"One day";
357 if (interval == week) return @"One week";
358
359 if (interval % week == 0) return [NSString stringWithFormat: @"%u weeks", (unsigned)(interval / week)];
360 if (interval % day == 0) return [NSString stringWithFormat: @"%u days", (unsigned)(interval / day)];
361 if (interval % hour == 0) return [NSString stringWithFormat: @"%u hours", (unsigned)(interval / hour)];
362 if (interval % minute == 0) return [NSString stringWithFormat: @"%u minutes", (unsigned)(interval / minute)];
363
364 if (interval <= mval) return [NSString stringWithFormat: @"%u seconds", (unsigned)interval];
365 if (interval <= mval * minute) return [NSString stringWithFormat: @"%u minutes", (unsigned)(interval / minute)];
366 if (interval <= mval * hour) return [NSString stringWithFormat: @"%u hours", (unsigned)(interval / hour)];
367 if (interval <= mval * day) return [NSString stringWithFormat: @"%u days", (unsigned)(interval / day)];
368 return [NSString stringWithFormat: @"%u weeks", (unsigned)(interval / week)];
369
370 return [self _stringForInterval: interval];
371}
372
373- (NSString *)timeRemainingString;
374{
375 NSString *timeRemainingString = [self _stringForInterval: llround([self timeRemaining])];
376
377 if (timeRemainingString == nil) return @"«expired»";
378 return timeRemainingString;
379}
380
381- (NSAttributedString *)prettyDescription;
382{
383 NSMutableAttributedString *string = [[NSMutableAttributedString alloc] init];
384 NSAttributedString *alertList = [alerts prettyList];
385
386 [string appendAttributedString:
387 [[NSString stringWithFormat: NSLocalizedString(@"At alarm time for '%@':\n", "Alert list title in pretty description, %@ replaced with message"), [self message]] small]];
388 if (alertList != nil) {
389 [string appendAttributedString: alertList];
390 } else {
391 [string appendAttributedString: [@"Do nothing." small]];
392 }
393 if ([self isRepeating]) {
394 [string appendAttributedString:
395 [[NSString stringWithFormat: @"\nAlarm repeats every %@.", [[self intervalString] lowercaseString]] small]];
396 }
397 return [string autorelease];
398}
399
400#pragma mark actions
401
402- (BOOL)setTimer;
403{
404 if (alarmType == PSAlarmExpired) {
405 if ([self isRepeating]) {
406 [self _setDateFromInterval];
407 } else {
408 [[NSNotificationCenter defaultCenter] postNotificationName: PSAlarmDiedNotification object: self];
409 return NO;
410 }
411 } else if (alarmType == PSAlarmDate) {
412 if (![self isValid]) return NO;
413 } else if (alarmType == PSAlarmSnooze) {
414 [self _setAlarmDate: [NSCalendarDate dateWithTimeIntervalSinceNow: snoozeInterval]];
415 } else if (alarmType != PSAlarmInterval) {
416 return NO;
417 }
418 timer = [PSTimer scheduledTimerWithTimeInterval: (alarmType == PSAlarmSnooze ? snoozeInterval : alarmInterval) target: self selector: @selector(_timerExpired:) userInfo: nil repeats: NO];
419 if (timer == nil) return NO;
420 [timer retain];
421 alarmType = PSAlarmSet;
422 [alerts prepareForAlarm: self];
423
424 [[NSNotificationCenter defaultCenter] postNotificationName: PSAlarmTimerSetNotification object: self];
425 // NSLog(@"set: %@; now %@; remaining %@", [[timer fireDate] description], [[NSDate date] description], [self timeRemainingString]);
426 return YES;
427}
428
429- (void)cancelTimer;
430{
431 [timer invalidate]; [timer release]; timer = nil;
432}
433
434- (void)resetTimer;
435{
436 if (timer != nil || alarmType != PSAlarmSet)
437 return;
438
439 alarmType = PSAlarmDate;
440 if (![self isRepeating]) {
441 [self setTimer];
442 } else {
443 // don't want to put this logic in setTimer or isValid because it can cause invalid alarms to be set (consider when someone clicks the "repeat" checkbox, then switches to a [nonrepeating, by design] date alarm, and enters a date that has passed: we do -not- want the alarm to magically morph into a repeating interval alarm)
444 NSTimeInterval savedInterval = alarmInterval;
445 if ([self setTimer]) {
446 // alarm is set, but not repeating - and the interval is wrong because it was computed from the date
447 alarmInterval = savedInterval;
448 [self setRepeating: YES];
449 } else {
450 // alarm is now invalid: expired in the past, so we start the timer over again
451 // We could potentially start counting from the expiration date (or expiration date + n * interval), but this doesn't match our existing behavior.
452 alarmType = PSAlarmInterval;
453 [self setInterval: savedInterval];
454 [self setTimer];
455 }
456 }
457}
458
459#pragma mark comparing
460
461- (NSComparisonResult)compareDate:(PSAlarm *)otherAlarm;
462{
463 return [[self date] compare: [otherAlarm date]];
464}
465
466- (NSComparisonResult)compareMessage:(PSAlarm *)otherAlarm;
467{
468 return [[self message] caseInsensitiveCompare: [otherAlarm message]];
469}
470
471#pragma mark printing
472
473- (NSString *)description;
474{
475 return [NSString stringWithFormat: @"%@: type %@ date %@ interval %.1f%@%@",
476 [super description], [self _alarmTypeString], alarmDate, alarmInterval,
477 (repeating ? @" repeating" : @""),
478 (alarmType == PSAlarmInvalid ?
479 [NSString stringWithFormat: @"\ninvalid message: %@", invalidMessage]
480 : (alarmType == PSAlarmSet ?
481 [NSString stringWithFormat: @"\ntimer: %@", timer] : @""))];
482}
483
484#pragma mark property list serialization (Pester 1.1)
485
486- (NSDictionary *)propertyListRepresentation;
487{
488 NSMutableDictionary *dict = [NSMutableDictionary dictionaryWithCapacity: 5];
489 if (![self isValid]) return nil;
490 [dict setObject: [self _alarmTypeString] forKey: PLAlarmType];
491 switch (alarmType) {
492 case PSAlarmDate:
493 case PSAlarmSet:
494 [dict setObject: [NSNumber numberWithDouble: [alarmDate timeIntervalSinceReferenceDate]] forKey: PLAlarmDate];
495 case PSAlarmSnooze:
496 case PSAlarmInterval:
497 case PSAlarmExpired:
498 break;
499 default:
500 NSAssert1(NO, NSLocalizedString(@"Can't save alarm type %@", "Assertion for invalid PSAlarm type on string; %@ replaced with alarm type string"), [self _alarmTypeString]);
501 break;
502 }
503 if ((alarmType != PSAlarmSet || repeating) && alarmType != PSAlarmDate) {
504 [dict setObject: [NSNumber numberWithBool: repeating] forKey: PLAlarmRepeating];
505 [dict setObject: [NSNumber numberWithDouble: alarmInterval] forKey: PLAlarmInterval];
506 }
507 if (snoozeInterval != 0)
508 [dict setObject: [NSNumber numberWithDouble: snoozeInterval] forKey: PLAlarmSnoozeInterval];
509 [dict setObject: alarmMessage forKey: PLAlarmMessage];
510 if (alerts != nil) {
511 [dict setObject: [alerts propertyListRepresentation] forKey: PLAlarmAlerts];
512 }
513 return dict;
514}
515
516- (id)initWithPropertyList:(NSDictionary *)dict;
517{
518 if ( (self = [self init]) != nil) {
519 PSAlerts *alarmAlerts;
520 alarmType = [self _alarmTypeForString: [dict objectForRequiredKey: PLAlarmType]];
521 switch (alarmType) {
522 case PSAlarmDate:
523 case PSAlarmSet:
524 { NSCalendarDate *date = [[NSCalendarDate alloc] initWithTimeIntervalSinceReferenceDate: [[dict objectForRequiredKey: PLAlarmDate] doubleValue]];
525 [self _setAlarmDate: date];
526 [date release];
527 }
528 break;
529 case PSAlarmSnooze: // snooze interval set but not confirmed; ignore
530 alarmType = PSAlarmExpired;
531 case PSAlarmInterval:
532 case PSAlarmExpired:
533 break;
534 default:
535 NSAssert1(NO, NSLocalizedString(@"Can't load alarm type %@", "Assertion for invalid PSAlarm type on load; %@ replaced with alarm type string"), [self _alarmTypeString]);
536 break;
537 }
538 repeating = [[dict objectForKey: PLAlarmRepeating] boolValue];
539 if ((alarmType != PSAlarmSet || repeating) && alarmType != PSAlarmDate)
540 alarmInterval = [[dict objectForRequiredKey: PLAlarmInterval] doubleValue];
541 snoozeInterval = [[dict objectForKey: PLAlarmSnoozeInterval] doubleValue];
542 [self setMessage: [dict objectForRequiredKey: PLAlarmMessage]];
543 alarmAlerts = [[PSAlerts alloc] initWithPropertyList: [dict objectForRequiredKey: PLAlarmAlerts]];
544 [self setAlerts: alarmAlerts];
545 [alarmAlerts release];
546 [self resetTimer];
547 if (alarmType == PSAlarmExpired) {
548 [self setTimer];
549 if (alarmType == PSAlarmExpired) { // failed to restart
550 [self release];
551 self = nil;
552 }
553 }
554 }
555 return self;
556}
557
558#pragma mark archiving (Pester 1.0)
559
560- (void)encodeWithCoder:(NSCoder *)coder;
561{
562 if (![self isValid]) return;
563 [coder encodeValueOfObjCType: @encode(PSAlarmType) at: &alarmType];
564 switch (alarmType) {
565 case PSAlarmDate:
566 case PSAlarmSet:
567 [coder encodeObject: alarmDate];
568 break;
569 case PSAlarmInterval:
570 [coder encodeValueOfObjCType: @encode(NSTimeInterval) at: &alarmInterval];
571 break;
572 default:
573 break;
574 }
575 [coder encodeObject: alarmMessage];
576 // NSLog(@"encoded: %@", self); // XXX happening twice, gdb refuses to show proper backtrace, grr
577 return;
578}
579
580- (id)initWithCoder:(NSCoder *)coder;
581{
582 if ( (self = [self init]) != nil) {
583 PSAlerts *legacyAlerts = [[PSAlerts alloc] initWithPesterVersion1Alerts];
584 [self setAlerts: legacyAlerts];
585 [legacyAlerts release];
586 [coder decodeValueOfObjCType: @encode(PSAlarmType) at: &alarmType];
587 switch (alarmType) {
588 case PSAlarmDate:
589 case PSAlarmSet:
590 [self _setAlarmDate: [coder decodeObject]];
591 break;
592 case PSAlarmInterval:
593 [coder decodeValueOfObjCType: @encode(NSTimeInterval) at: &alarmInterval];
594 break;
595 default:
596 break;
597 }
598 [self setMessage: [coder decodeObject]];
599 if (alarmType == PSAlarmSet)
600 alarmType = PSAlarmDate;
601 // Note: the timer is not set here, so these alarms are inert.
602 // This helps make importing atomic (see -[PSAlarms importVersion1Alarms])
603 }
604 return self;
605}
606
607@end
Note: See TracBrowser for help on using the repository browser.