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

Last change on this file since 516 was 366, checked in by Nicholas Riley, 13 years ago

Info-Pester.plist: Updated for build 23.

NJRHotKey.m: Cosmetic cleanup.

PSAlarm.[hm]: Fix -[PSAlarm time] to work properly, though it no
longer has any clients.

PSSnoozeUntilController.m: Fix snooze until time being off by an hour
as displayed, or if edited, in some time zones.

PSSpeechAlert.m: Display the voice name instead of its identifier in
the description.

Pester.xcodeproj: Misc.

release-notes.html: Updated for build 23.

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