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

Last change on this file since 358 was 358, checked in by Nicholas Riley, 16 years ago

PSAlarm.m: Use 10.4-style (ICU-based) date formatters, which pick up format changes automatically. Still some un-localized bits for intervals and "on"/"at".

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