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

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

Updated for Pester 1.1a5 (very limited release).

Pester 1.1a4 was never released.

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