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

Last change on this file since 256 was 113, checked in by Nicholas Riley, 22 years ago

PSTimer.m: Fix bug 15, leaking PSTimer from NSInvocation argument retention.

Read Me.rtfd: Updated release notes.

PSAlarm.[hm]: Removed obsolete comment. Changed 1m-59m times to
display without `0h'. Moved code from deserialization to -resetTimer
to be used for alarm deletion undo - fix bug 14.

PSAlarmsController.m: Fix bug 14 - implement _restoreAlarms:,
_removeAlarms and _undoManager. Need to add localized string.

PSAlarmAlertController.m: Fix bug 17, move alert triggering from
constructor to -performAlertsForAlarm: to avoid nested notifications
delivering in wrong order.

PSAlarms.[hm]: Fix bug 14 - implement -restoreAlarms, invokes [PSAlarm
resetTimer]. Clarified a comment. Uncommented some debug logging.

package-Pester.sh: Implemented partition map-less disk image, saves a
few K on distribution. Removed obsolete comments. Added agvtool
bump.

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