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

Last change on this file since 61 was 61, checked in by Nicholas Riley, 19 years ago

Pester 1.1b1.

PSPowerManager: Fixed delegate method selectors to better reflect what
is going on (Apple's docs in IOKit Fundamentals help with this; the
kIOMessage*Sleep constants are really poorly named).

VERSION: Updated for 1.1b1.

PSSpeechAlert.h: Fixed company name.

PSAlert.[hm]: Added -prepareForAlarm: to support PSWakeAlert.

PSTimer.[hm]: Replacement for NSTimer that works properly across
sleep/wake cycles and will schedule wake timers.

PSAlerts.[hm]: Added -prepareForAlarm: to support PSWakeAlert.

Read Me.rtfd: Updated for 1.1b1.

PSAlarm.[hm]: Added -setWakeUp:, invoke -[PSAlerts prepareForAlarm],
replaced alarm timer NSTimer with PSTimer.

PSApplication.[hm]: Replaced dock update timer NSTimer with PSTimer.
Uncovered some issues, need to fix later. Enable alarm discard for
beta release.

PSWakeAlert.[hm]: Shared alert implementation for wakeup. Doesn't do
anything at trigger time, but uses new preparation interface to work
at alarm set time (should work for repeating alarms too, but I didn't
bother to test...)

PSAlarmSetController.m: Added support for PSWakeAlert. Save default
alert information on quit. Removed debug statements on hide/unhide;
it works fine regardless of whether the app is explicitly hidden or
the window hides itself.

PSAlarms.m: PSTimer support - invoke +[PSTimer setUp] to initialize
timer list.

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