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

Last change on this file since 608 was 608, checked in by Nicholas Riley, 10 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.