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

Last change on this file since 580 was 551, checked in by Nicholas Riley, 15 years ago

Maximum interval is now 999 weeks.

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 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 = 999, minute = 60, hour = minute * 60, day = hour * 24, week = day * 7;
331 unsigned long long interval = [self interval];
332 if (interval == 0) return nil;
333
334 if (interval == 1) return @"One second";
335 if (interval == minute) return @"One minute";
336 if (interval == hour) return @"One hour";
337 if (interval == day) return @"One day";
338 if (interval == week) return @"One week";
339
340 if (interval % week == 0) return [NSString stringWithFormat: @"%u weeks", (unsigned)(interval / week)];
341 if (interval % day == 0) return [NSString stringWithFormat: @"%u days", (unsigned)(interval / day)];
342 if (interval % hour == 0) return [NSString stringWithFormat: @"%u hours", (unsigned)(interval / hour)];
343 if (interval % minute == 0) return [NSString stringWithFormat: @"%u minutes", (unsigned)(interval / minute)];
344
345 if (interval <= mval) return [NSString stringWithFormat: @"%u seconds", (unsigned)interval];
346 if (interval <= mval * minute) return [NSString stringWithFormat: @"%u minutes", (unsigned)(interval / minute)];
347 if (interval <= mval * hour) return [NSString stringWithFormat: @"%u hours", (unsigned)(interval / hour)];
348 if (interval <= mval * day) return [NSString stringWithFormat: @"%u days", (unsigned)(interval / day)];
349 return [NSString stringWithFormat: @"%u weeks", (unsigned)(interval / week)];
350
351 return [self _stringForInterval: interval];
352}
353
354- (NSString *)timeRemainingString;
355{
356 NSString *timeRemainingString = [self _stringForInterval: llround([self timeRemaining])];
357
358 if (timeRemainingString == nil) return @"«expired»";
359 return timeRemainingString;
360}
361
362- (NSAttributedString *)prettyDescription;
363{
364 NSMutableAttributedString *string = [[NSMutableAttributedString alloc] init];
365 NSAttributedString *alertList = [alerts prettyList];
366
367 [string appendAttributedString:
368 [[NSString stringWithFormat: NSLocalizedString(@"At alarm time for '%@':\n", "Alert list title in pretty description, %@ replaced with message"), [self message]] small]];
369 if (alertList != nil) {
370 [string appendAttributedString: alertList];
371 } else {
372 [string appendAttributedString: [@"Do nothing." small]];
373 }
374 if ([self isRepeating]) {
375 [string appendAttributedString:
376 [[NSString stringWithFormat: @"\nAlarm repeats every %@.", [[self intervalString] lowercaseString]] small]];
377 }
378 return [string autorelease];
379}
380
381#pragma mark actions
382
383- (BOOL)setTimer;
384{
385 if (alarmType == PSAlarmExpired) {
386 if ([self isRepeating]) {
387 [self _setDateFromInterval];
388 } else {
389 [[NSNotificationCenter defaultCenter] postNotificationName: PSAlarmDiedNotification object: self];
390 return NO;
391 }
392 } else if (alarmType == PSAlarmDate) {
393 if (![self isValid]) return NO;
394 } else if (alarmType == PSAlarmSnooze) {
395 [self _setAlarmDate: [NSCalendarDate dateWithTimeIntervalSinceNow: snoozeInterval]];
396 } else if (alarmType != PSAlarmInterval) {
397 return NO;
398 }
399 timer = [PSTimer scheduledTimerWithTimeInterval: (alarmType == PSAlarmSnooze ? snoozeInterval : alarmInterval) target: self selector: @selector(_timerExpired:) userInfo: nil repeats: NO];
400 if (timer == nil) return NO;
401 [timer retain];
402 alarmType = PSAlarmSet;
403 [alerts prepareForAlarm: self];
404
405 [[NSNotificationCenter defaultCenter] postNotificationName: PSAlarmTimerSetNotification object: self];
406 // NSLog(@"set: %@; now %@; remaining %@", [[timer fireDate] description], [[NSDate date] description], [self timeRemainingString]);
407 return YES;
408}
409
410- (void)cancelTimer;
411{
412 [timer invalidate]; [timer release]; timer = nil;
413}
414
415- (void)resetTimer;
416{
417 if (timer != nil || alarmType != PSAlarmSet)
418 return;
419
420 alarmType = PSAlarmDate;
421 if (![self isRepeating]) {
422 [self setTimer];
423 } else {
424 // 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)
425 NSTimeInterval savedInterval = alarmInterval;
426 if ([self setTimer]) {
427 // alarm is set, but not repeating - and the interval is wrong because it was computed from the date
428 alarmInterval = savedInterval;
429 [self setRepeating: YES];
430 } else {
431 // alarm is now invalid: expired in the past, so we start the timer over again
432 // We could potentially start counting from the expiration date (or expiration date + n * interval), but this doesn't match our existing behavior.
433 alarmType = PSAlarmInterval;
434 [self setInterval: savedInterval];
435 [self setTimer];
436 }
437 }
438}
439
440#pragma mark comparing
441
442- (NSComparisonResult)compareDate:(PSAlarm *)otherAlarm;
443{
444 return [[self date] compare: [otherAlarm date]];
445}
446
447- (NSComparisonResult)compareMessage:(PSAlarm *)otherAlarm;
448{
449 return [[self message] caseInsensitiveCompare: [otherAlarm message]];
450}
451
452#pragma mark printing
453
454- (NSString *)description;
455{
456 return [NSString stringWithFormat: @"%@: type %@ date %@ interval %.1f%@%@",
457 [super description], [self _alarmTypeString], alarmDate, alarmInterval,
458 (repeating ? @" repeating" : @""),
459 (alarmType == PSAlarmInvalid ?
460 [NSString stringWithFormat: @"\ninvalid message: %@", invalidMessage]
461 : (alarmType == PSAlarmSet ?
462 [NSString stringWithFormat: @"\ntimer: %@", timer] : @""))];
463}
464
465#pragma mark property list serialization (Pester 1.1)
466
467- (NSDictionary *)propertyListRepresentation;
468{
469 NSMutableDictionary *dict = [NSMutableDictionary dictionaryWithCapacity: 5];
470 if (![self isValid]) return nil;
471 [dict setObject: [self _alarmTypeString] forKey: PLAlarmType];
472 switch (alarmType) {
473 case PSAlarmDate:
474 case PSAlarmSet:
475 [dict setObject: [NSNumber numberWithDouble: [alarmDate timeIntervalSinceReferenceDate]] forKey: PLAlarmDate];
476 case PSAlarmSnooze:
477 case PSAlarmInterval:
478 case PSAlarmExpired:
479 break;
480 default:
481 NSAssert1(NO, NSLocalizedString(@"Can't save alarm type %@", "Assertion for invalid PSAlarm type on string; %@ replaced with alarm type string"), [self _alarmTypeString]);
482 break;
483 }
484 if ((alarmType != PSAlarmSet || repeating) && alarmType != PSAlarmDate) {
485 [dict setObject: [NSNumber numberWithBool: repeating] forKey: PLAlarmRepeating];
486 [dict setObject: [NSNumber numberWithDouble: alarmInterval] forKey: PLAlarmInterval];
487 }
488 if (snoozeInterval != 0)
489 [dict setObject: [NSNumber numberWithDouble: snoozeInterval] forKey: PLAlarmSnoozeInterval];
490 [dict setObject: alarmMessage forKey: PLAlarmMessage];
491 if (alerts != nil) {
492 [dict setObject: [alerts propertyListRepresentation] forKey: PLAlarmAlerts];
493 }
494 return dict;
495}
496
497- (id)initWithPropertyList:(NSDictionary *)dict;
498{
499 if ( (self = [self init]) != nil) {
500 PSAlerts *alarmAlerts;
501 alarmType = [self _alarmTypeForString: [dict objectForRequiredKey: PLAlarmType]];
502 switch (alarmType) {
503 case PSAlarmDate:
504 case PSAlarmSet:
505 { NSCalendarDate *date = [[NSCalendarDate alloc] initWithTimeIntervalSinceReferenceDate: [[dict objectForRequiredKey: PLAlarmDate] doubleValue]];
506 [self _setAlarmDate: date];
507 [date release];
508 }
509 break;
510 case PSAlarmSnooze: // snooze interval set but not confirmed; ignore
511 alarmType = PSAlarmExpired;
512 case PSAlarmInterval:
513 case PSAlarmExpired:
514 break;
515 default:
516 NSAssert1(NO, NSLocalizedString(@"Can't load alarm type %@", "Assertion for invalid PSAlarm type on load; %@ replaced with alarm type string"), [self _alarmTypeString]);
517 break;
518 }
519 repeating = [[dict objectForKey: PLAlarmRepeating] boolValue];
520 if ((alarmType != PSAlarmSet || repeating) && alarmType != PSAlarmDate)
521 alarmInterval = [[dict objectForRequiredKey: PLAlarmInterval] doubleValue];
522 snoozeInterval = [[dict objectForKey: PLAlarmSnoozeInterval] doubleValue];
523 [self setMessage: [dict objectForRequiredKey: PLAlarmMessage]];
524 alarmAlerts = [[PSAlerts alloc] initWithPropertyList: [dict objectForRequiredKey: PLAlarmAlerts]];
525 [self setAlerts: alarmAlerts];
526 [alarmAlerts release];
527 [self resetTimer];
528 if (alarmType == PSAlarmExpired) {
529 [self setTimer];
530 if (alarmType == PSAlarmExpired) { // failed to restart
531 [self release];
532 self = nil;
533 }
534 }
535 }
536 return self;
537}
538
539#pragma mark archiving (Pester 1.0)
540
541- (void)encodeWithCoder:(NSCoder *)coder;
542{
543 if (![self isValid]) return;
544 [coder encodeValueOfObjCType: @encode(PSAlarmType) at: &alarmType];
545 switch (alarmType) {
546 case PSAlarmDate:
547 case PSAlarmSet:
548 [coder encodeObject: alarmDate];
549 break;
550 case PSAlarmInterval:
551 [coder encodeValueOfObjCType: @encode(NSTimeInterval) at: &alarmInterval];
552 break;
553 default:
554 break;
555 }
556 [coder encodeObject: alarmMessage];
557 // NSLog(@"encoded: %@", self); // XXX happening twice, gdb refuses to show proper backtrace, grr
558 return;
559}
560
561- (id)initWithCoder:(NSCoder *)coder;
562{
563 if ( (self = [self init]) != nil) {
564 PSAlerts *legacyAlerts = [[PSAlerts alloc] initWithPesterVersion1Alerts];
565 [self setAlerts: legacyAlerts];
566 [legacyAlerts release];
567 [coder decodeValueOfObjCType: @encode(PSAlarmType) at: &alarmType];
568 switch (alarmType) {
569 case PSAlarmDate:
570 case PSAlarmSet:
571 [self _setAlarmDate: [coder decodeObject]];
572 break;
573 case PSAlarmInterval:
574 [coder decodeValueOfObjCType: @encode(NSTimeInterval) at: &alarmInterval];
575 break;
576 default:
577 break;
578 }
579 [self setMessage: [coder decodeObject]];
580 if (alarmType == PSAlarmSet)
581 alarmType = PSAlarmDate;
582 // Note: the timer is not set here, so these alarms are inert.
583 // This helps make importing atomic (see -[PSAlarms importVersion1Alarms])
584 }
585 return self;
586}
587
588@end
Note: See TracBrowser for help on using the repository browser.