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

Last change on this file since 587 was 587, checked in by Nicholas Riley, 10 years ago

Fix some problems/deprecation identified by compiling with the 10.6 SDK.

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