source: trunk/Cocoa/Pester/Source/PSAlarmSetController.m@ 597

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

If date is today and we've picked a time before now, set the date to tomorrow. (Pester for hiptop did this too.)

File size: 26.4 KB
RevLine 
[21]1//
2// PSAlarmSetController.m
3// Pester
4//
5// Created by Nicholas Riley on Tue Oct 08 2002.
6// Copyright (c) 2002 Nicholas Riley. All rights reserved.
7//
8
9#import "PSAlarmSetController.h"
[34]10#import "PSAlarmAlertController.h"
[102]11#import "PSCalendarController.h"
[53]12#import "PSPowerManager.h"
[102]13#import "PSTimeDateEditor.h"
[133]14#import "PSVolumeController.h"
[21]15#import "NJRDateFormatter.h"
[34]16#import "NJRFSObjectSelector.h"
[53]17#import "NJRIntervalField.h"
[39]18#import "NJRQTMediaPopUpButton.h"
[133]19#import "NJRSoundManager.h"
[364]20#import "NJRValidatingField.h"
[34]21#import "NJRVoicePopUpButton.h"
[53]22#import "NSString-NJRExtensions.h"
23#import "NSAttributedString-NJRExtensions.h"
24#import "NSCalendarDate-NJRExtensions.h"
[21]25
[53]26#import "PSAlerts.h"
[34]27#import "PSDockBounceAlert.h"
28#import "PSScriptAlert.h"
29#import "PSNotifierAlert.h"
30#import "PSBeepAlert.h"
31#import "PSMovieAlert.h"
32#import "PSSpeechAlert.h"
[61]33#import "PSWakeAlert.h"
[34]34
[28]35/* Bugs to file:
[21]36
[28]37¥ any trailing spaces: -> exception for +[NSCalendarDate dateWithNaturalLanguageString]:
38 > NSCalendarDate dateWithNaturalLanguageString: '12 '
39 format error: internal error
[21]40
[28]41¥ NSDate natural language stuff in NSCalendarDate (why?), misspelled category name
42¥ NSCalendarDate natural language stuff behaves differently from NSDateFormatter (AM/PM has no effect, shouldn't they share code?)
[43]43¥ descriptionWithCalendarFormat:, dateWithNaturalLanguageString: does not default to current locale, instead it defaults to US unless you tell it otherwise
[28]44¥ NSDateFormatter doc class description gives two examples for natural language that are incorrect, no link to NSDate doc that describes exactly how natural language dates are parsed
45¥ NSTimeFormatString does not include %p when it should, meaning that AM/PM is stripped yet 12-hour time is still used
46¥ NSNextDayDesignations, NSNextNextDayDesignations are noted as 'a string' in NSUserDefaults docs, but maybe they are actually an array, or either an array or a string, given their names?
47¥ "Setting the Format for Dates" does not document how to get 1:15 AM, the answer is %1I - strftime has no exact equivalent; the closest is %l. strftime does not permit numeric prefixes. It also refers to "NSCalendar" when no such class exists.
[43]48¥ none of many mentions of NSAMPMDesignation indicates that they include the leading spaces (" AM", " PM"). In "Setting the Format for Dates", needs to mention that the leading spaces are not included in %p with strftime. But if you use the NSCalendarDate stuff, it appears %p doesn't include the space (because it doesn't use the locale dictionary).
49¥ If you feed NSCalendarDate dateWithNaturalLanguageString: an " AM"/" PM" locale, it doesn't accept that date format.
[28]50¥ descriptions for %X and %x are reversed (time zone is in %X, not %x)
[53]51¥ NSComboBox data source issues, canÕt have it appear as ÒtodayÓ because the formatter doesnÕt like that. Should be able to enter text into the data source and have the formatter process it without altering it.
[28]52¥ too hard to implement date-only or time-only formatters
53¥ should be able to specify that natural language favors date or time (10 = 10th of month, not 10am)
54¥ please expose the iCal controls!
55
56*/
57
[53]58static NSString * const PSAlertsSelected = @"Pester alerts selected"; // NSUserDefaults key
59static NSString * const PSAlertsEditing = @"Pester alerts editing"; // NSUserDefaults key
60
[26]61@interface PSAlarmSetController (Private)
[21]62
[53]63- (void)_readAlerts:(PSAlerts *)alerts;
64- (BOOL)_setAlerts;
[133]65- (void)_setVolume:(float)volume withPreview:(BOOL)preview;
[26]66- (void)_stopUpdateTimer;
67
68@end
69
[21]70@implementation PSAlarmSetController
71
72- (void)awakeFromNib;
73{
[53]74 NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
[60]75 NSNotificationCenter *notificationCenter = [NSNotificationCenter defaultCenter];
[26]76 alarm = [[PSAlarm alloc] init];
[21]77 [[self window] center];
[361]78 if ([[removeMessageButton image] size].width != 0)
79 [removeMessageButton setTitle: @""];
[102]80 [PSTimeDateEditor setUpTimeField: timeOfDay dateField: timeDate completions: timeDateCompletions];
[133]81 { // volume defaults, usually overridden by restored alert info
82 float volume = 0.5;
83 [NJRSoundManager getDefaultOutputVolume: &volume];
84 [self _setVolume: volume withPreview: NO];
85 }
[579]86 [editAlert setState: NSOnState]; // XXX temporary for 1.1b5
[53]87 {
88 NSDictionary *plAlerts = [defaults dictionaryForKey: PSAlertsSelected];
[364]89 PSAlerts *alerts = nil;
[53]90 if (plAlerts == nil) {
91 alerts = [[PSAlerts alloc] initWithPesterVersion1Alerts];
92 } else {
[364]93 @try {
[53]94 alerts = [[PSAlerts alloc] initWithPropertyList: plAlerts];
[364]95 } @catch (NSException *exception) {
96 NSRunAlertPanel(@"Unable to restore alerts", @"Pester could not restore recent alert information for one or more alerts in the Set Alarm window. The default set of alerts will be used instead.\n\n%@", nil, nil, nil, [exception reason]);
[53]97 alerts = [[PSAlerts alloc] initWithPesterVersion1Alerts];
[364]98 }
[53]99 }
100 [self _readAlerts: alerts];
101 }
102 [self inAtChanged: nil]; // by convention, if sender is nil, we're initializing
[34]103 [self playSoundChanged: nil];
104 [self doScriptChanged: nil];
105 [self doSpeakChanged: nil];
[53]106 [self editAlertChanged: nil];
[34]107 [script setFileTypes: [NSArray arrayWithObjects: @"applescript", @"script", NSFileTypeForHFSTypeCode(kOSAFileType), NSFileTypeForHFSTypeCode('TEXT'), nil]];
[60]108 [notificationCenter addObserver: self selector: @selector(silence:) name: PSAlarmAlertStopNotification object: nil];
109 [notificationCenter addObserver: self selector: @selector(playSoundChanged:) name: NJRQTMediaPopUpButtonMovieChangedNotification object: sound];
110 [notificationCenter addObserver: self selector: @selector(applicationWillHide:) name: NSApplicationWillHideNotification object: NSApp];
111 [notificationCenter addObserver: self selector: @selector(applicationDidUnhide:) name: NSApplicationDidUnhideNotification object: NSApp];
[61]112 [notificationCenter addObserver: self selector: @selector(applicationWillTerminate:) name: NSApplicationWillTerminateNotification object: NSApp];
[53]113 [voice setDelegate: self]; // XXX why don't we do this in IB? It should use the accessor...
114 [wakeUp setEnabled: [PSPowerManager autoWakeSupported]];
[522]115
[53]116 // XXX workaround for 10.1.x and 10.2.x bug which sets the first responder to the wrong field alternately, but it works if I set the initial first responder to nil... go figure.
[522]117 NSWindow *window = [self window];
118 [window setInitialFirstResponder: nil];
119 [window makeKeyAndOrderFront: nil];
[21]120}
121
122- (void)setStatus:(NSString *)aString;
123{
[26]124 // NSLog(@"%@", alarm);
[21]125 if (aString != status) {
126 [status release]; status = nil;
127 status = [aString retain];
128 [timeSummary setStringValue: status];
129 }
130}
131
[53]132// XXX with -[NSControl currentEditor] don't need to compare? Also check -[NSControl validateEditing]
[21]133- (id)objectValueForTextField:(NSTextField *)field whileEditing:(id)sender;
134{
135 if (sender == field) {
136 NSString *stringValue = [[[self window] fieldEditor: NO forObject: field] string];
137 id obj = nil;
138 [[field formatter] getObjectValue: &obj forString: stringValue errorDescription: NULL];
139 // NSLog(@"from field editor: %@", obj);
140 return obj;
141 } else {
142 // NSLog(@"from field: %@", [field objectValue]);
143 return [field objectValue];
144 }
145}
146
[53]147#pragma mark date/interval setting
148
[21]149- (void)setAlarmDateAndInterval:(id)sender;
150{
[26]151 if (isInterval) {
[53]152 [alarm setInterval: [timeInterval interval]];
[21]153 } else {
[24]154 [alarm setForDate: [self objectValueForTextField: timeDate whileEditing: sender]
155 atTime: [self objectValueForTextField: timeOfDay whileEditing: sender]];
[21]156 }
157}
158
[26]159- (void)_stopUpdateTimer;
160{
[28]161 [updateTimer invalidate]; [updateTimer release]; updateTimer = nil;
[26]162}
[21]163
[24]164- (IBAction)updateDateDisplay:(id)sender;
[21]165{
[26]166 // NSLog(@"updateDateDisplay: %@", sender);
[24]167 if ([alarm isValid]) {
[552]168 [self setStatus: [NSString stringWithFormat: @"Alarm will be set for %@\non %@.", [alarm timeString], [alarm dateString]]];
[21]169 [setButton setEnabled: YES];
[26]170 if (updateTimer == nil || ![updateTimer isValid]) {
171 // XXX this logic (and the timer) should really go into PSAlarm, to send notifications for status updates instead. Timer starts when people are watching, stops when people aren't.
172 // NSLog(@"setting timer");
173 if (isInterval) {
174 updateTimer = [NSTimer scheduledTimerWithTimeInterval: 1 target: self selector: @selector(updateDateDisplay:) userInfo: nil repeats: YES];
175 } else {
176 updateTimer = [NSTimer scheduledTimerWithTimeInterval: [alarm interval] target: self selector: @selector(updateDateDisplay:) userInfo: nil repeats: NO];
177 }
178 [updateTimer retain];
179 }
[21]180 } else {
181 [setButton setEnabled: NO];
[26]182 [self setStatus: [alarm invalidMessage]];
183 [self _stopUpdateTimer];
[21]184 }
185}
186
[26]187// Be careful not to hook up any of the text fields' actions to update: because we handle them in controlTextDidChange: instead. If we could get the active text field somehow via public API (guess we could use controlTextDidBegin/controlTextDidEndEditing) then we'd not need to overload the update sender for this purpose. Or, I guess, we could use another method other than update. It should not be this hard to implement what is essentially standard behavior. Sigh.
[43]188// Note: finding out whether a given control is editing is easier. See: <http://cocoa.mamasam.com/COCOADEV/2002/03/2/28501.php>.
[26]189
[24]190- (IBAction)update:(id)sender;
191{
192 // NSLog(@"update: %@", sender);
193 [self setAlarmDateAndInterval: sender];
194 [self updateDateDisplay: sender];
195}
196
[21]197- (IBAction)inAtChanged:(id)sender;
198{
[43]199 NSButtonCell *new = [inAtMatrix selectedCell], *old;
[26]200 isInterval = ([inAtMatrix selectedTag] == 0);
[43]201 old = [inAtMatrix cellWithTag: isInterval];
202 NSAssert(new != old, @"in and at buttons should be distinct!");
[364]203
204 if (sender != nil) {
205 // XXX validation doesn't work properly for date/time, so we just universally cancel editing now
206 if (![[self window] makeFirstResponder: nil] && !isInterval) {
207 // This works fine synchronously only if you're using the keyboard shortcut to switch in/at. Directly activating the button, a delayed invocation is necessary.
208 NSInvocation *i = [NSInvocation invocationWithMethodSignature:
209 [inAtMatrix methodSignatureForSelector: @selector(selectCellWithTag:)]];
210 int tag = [old tag];
211 [i setSelector: @selector(selectCellWithTag:)];
212 [i setTarget: inAtMatrix];
213 [i setArgument: &tag atIndex: 2];
214 [NSTimer scheduledTimerWithTimeInterval: 0 invocation: i repeats: NO];
215 return;
216 }
217 }
218
[43]219 [old setKeyEquivalent: [new keyEquivalent]];
220 [old setKeyEquivalentModifierMask: [new keyEquivalentModifierMask]];
221 [new setKeyEquivalent: @""];
222 [new setKeyEquivalentModifierMask: 0];
[26]223 [timeInterval setEnabled: isInterval];
224 [timeIntervalUnits setEnabled: isInterval];
[34]225 [timeIntervalRepeats setEnabled: isInterval];
[26]226 [timeOfDay setEnabled: !isInterval];
227 [timeDate setEnabled: !isInterval];
[361]228 [timeDateCompletions setEnabled: !isInterval && [NJRDateFormatter naturalLanguageParsingAvailable]];
[102]229 [timeCalendarButton setEnabled: !isInterval];
[21]230 if (sender != nil)
[364]231 [[self window] makeFirstResponder: isInterval ? (NSTextField *)timeInterval : timeOfDay];
232 if (!isInterval) // need to do this every time the controls are enabled
[102]233 [timeOfDay setNextKeyView: timeDate];
[21]234 // NSLog(@"UPDATING FROM inAtChanged");
235 [self update: nil];
236}
237
[53]238- (IBAction)dateCompleted:(NSPopUpButton *)sender;
239{
240 [timeDate setStringValue: [sender titleOfSelectedItem]];
241 [self update: sender];
242}
243
[102]244#pragma mark calendar
245
246- (IBAction)showCalendar:(NSButton *)sender;
247{
248 [PSCalendarController controllerWithDate: [NSCalendarDate dateForDay: [timeDate objectValue]] delegate: self];
249}
250
251- (void)calendarController:(PSCalendarController *)calendar didSetDate:(NSCalendarDate *)date;
252{
253 [timeDate setObjectValue: date];
254 [self update: self];
255}
256
257- (NSView *)calendarControllerLaunchingView:(PSCalendarController *)controller;
258{
259 return timeCalendarButton;
260}
261
[133]262#pragma mark volume
263
264- (IBAction)showVolume:(NSButton *)sender;
265{
266 [PSVolumeController controllerWithVolume: [sound outputVolume] delegate: self];
267}
268
269#define VOLUME_IMAGE_INDEX(vol) (vol * 4) - 0.01
270
271- (void)_setVolume:(float)volume withPreview:(BOOL)preview;
272{
273 float outputVolume = [sound outputVolume];
274 short volumeImageIndex = VOLUME_IMAGE_INDEX(volume);
275
276 if (outputVolume > 0 && volumeImageIndex == VOLUME_IMAGE_INDEX(outputVolume)) return;
277 NSString *volumeImageName = [NSString stringWithFormat: @"Volume %ld", volumeImageIndex];
278 [soundVolumeButton setImage: [NSImage imageNamed: volumeImageName]];
279
280 [sound setOutputVolume: volume withPreview: preview];
281}
282
283- (void)volumeController:(PSVolumeController *)controller didSetVolume:(float)volume;
284{
285 [self _setVolume: volume withPreview: YES];
286}
287
288- (NSView *)volumeControllerLaunchingView:(PSVolumeController *)controller;
289{
290 return soundVolumeButton;
291}
292
[53]293#pragma mark alert editing
294
[102]295- (IBAction)toggleAlertEditor:(id)sender;
296{
297 [editAlert performClick: self];
298}
299
[53]300- (IBAction)editAlertChanged:(id)sender;
301{
[579]302 BOOL editAlertSelected = [editAlert state] == NSOnState;
[53]303 NSWindow *window = [self window];
304 NSRect frame = [window frame];
305 if (editAlertSelected) {
306 NSSize editWinSize = [window maxSize];
[579]307 [editAlert setNextKeyView: [displayMessage controlView]];
[53]308 frame.origin.y += frame.size.height - editWinSize.height;
309 frame.size = editWinSize;
310 [window setFrame: frame display: (sender != nil) animate: (sender != nil)];
311 [self updateDateDisplay: sender];
312 [alertTabs selectTabViewItemWithIdentifier: @"edit"];
313 } else {
314 NSSize viewWinSize = [window minSize];
315 NSRect textFrame = [alertView frame];
316 float textHeight;
317 if (![self _setAlerts]) {
[103]318 [alertView setStringValue: [NSString stringWithFormat: @"%@\n%@", NSLocalizedString(@"Couldn't process alert information.", "Message shown in collapsed alert area when alert information is invalid or inconsistent (prevents setting alarm)"), status]];
[53]319 } else {
320 NSAttributedString *string = [[alarm alerts] prettyList];
321 if (string == nil) {
[103]322 [alertView setStringValue: NSLocalizedString(@"Do nothing. Click the button labeled 'Edit' to add an alert.", "Message shown in collapsed alert edit area when no alerts have been specified")];
[53]323 } else {
324 [alertView setAttributedStringValue: string];
325 [self updateDateDisplay: sender];
326 }
327 }
328 if (sender != nil) { // nil == we're initializing, don't mess with focus
329 NSResponder *oldResponder = [window firstResponder];
330 // make sure focus doesn't get stuck in the edit tab: it is confusing and leaves behind artifacts
[579]331 if (oldResponder == editAlert || [oldResponder isKindOfClass: [NSView class]] && [(NSView *)oldResponder isDescendantOf: alertTabs])
332 [window makeFirstResponder: messageField]; // would use editAlert, but can't get it to display anomaly-free.
[53]333 [self silence: sender];
334 }
335 // allow height to expand, though not arbitrarily (should still fit on an 800x600 screen)
336 textHeight = [[alertView cell] cellSizeForBounds: NSMakeRect(0, 0, textFrame.size.width, 400)].height;
337 textFrame.origin.y += textFrame.size.height - textHeight;
338 textFrame.size.height = textHeight;
339 [alertView setFrame: textFrame];
340 viewWinSize.height += textHeight;
341 [alertTabs selectTabViewItemWithIdentifier: @"view"];
342 frame.origin.y += frame.size.height - viewWinSize.height;
343 frame.size = viewWinSize;
344 [window setFrame: frame display: (sender != nil) animate: (sender != nil)];
[579]345 [editAlert setNextKeyView: cancelButton];
[53]346 }
347 if (sender != nil) {
348 [[NSUserDefaults standardUserDefaults] setBool: editAlertSelected forKey: PSAlertsEditing];
349 }
350}
351
[34]352- (IBAction)playSoundChanged:(id)sender;
353{
354 BOOL playSoundSelected = [playSound intValue];
[542]355 BOOL canRepeat = playSoundSelected ? [sound canRepeat] : NO;
[364]356 [sound setEnabled: playSoundSelected];
[41]357 [soundRepetitions setEnabled: canRepeat];
[133]358 [soundVolumeButton setEnabled: canRepeat && [sound hasAudio]];
[41]359 [soundRepetitionStepper setEnabled: canRepeat];
360 [soundRepetitionsLabel setTextColor: canRepeat ? [NSColor controlTextColor] : [NSColor disabledControlTextColor]];
[53]361 if (playSoundSelected && sender == playSound) {
[542]362 [[self window] makeFirstResponder: sound];
[53]363 }
[34]364}
365
[41]366- (IBAction)setSoundRepetitionCount:(id)sender;
367{
[43]368 NSTextView *fieldEditor = (NSTextView *)[soundRepetitions currentEditor];
369 BOOL isEditing = (fieldEditor != nil);
370 int newReps = [sender intValue], oldReps;
371 if (isEditing) {
372 // XXX work around bug where if you ask soundRepetitions for its intValue too often while it's editing, the field begins to flash
373 oldReps = [[[fieldEditor textStorage] string] intValue];
374 } else oldReps = [soundRepetitions intValue];
375 if (newReps != oldReps) {
[41]376 [soundRepetitions setIntValue: newReps];
[43]377 // NSLog(@"updating: new value %d, old value %d%@", newReps, oldReps, isEditing ? @", is editing" : @"");
378 // XXX work around 10.1 bug, otherwise field only displays every second value
379 if (isEditing) [soundRepetitions selectText: self];
380 }
[41]381}
382
[34]383// XXX should check the 'Do script:' button when someone drops a script on the button
384
385- (IBAction)doScriptChanged:(id)sender;
386{
387 BOOL doScriptSelected = [doScript intValue];
388 [script setEnabled: doScriptSelected];
389 [scriptSelectButton setEnabled: doScriptSelected];
[53]390 if (doScriptSelected && sender != nil) {
[34]391 [[self window] makeFirstResponder: scriptSelectButton];
[53]392 if ([script alias] == nil) [scriptSelectButton performClick: sender];
[355]393 } else {
394 [[self window] makeFirstResponder: sender];
[53]395 }
[34]396}
397
398- (IBAction)doSpeakChanged:(id)sender;
399{
[53]400 BOOL doSpeakSelected = [doSpeak state] == NSOnState;
[34]401 [voice setEnabled: doSpeakSelected];
402 if (doSpeakSelected && sender != nil)
403 [[self window] makeFirstResponder: voice];
[355]404 else
405 [[self window] makeFirstResponder: sender];
[34]406}
407
[53]408- (void)_readAlerts:(PSAlerts *)alerts;
[21]409{
[53]410 NSEnumerator *e = [alerts alertEnumerator];
411 PSAlert *alert;
412
413 [alarm setAlerts: alerts];
414
415 // turn off all alerts
416 [bounceDockIcon setState: NSOffState];
417 [doScript setIntValue: NO];
418 [displayMessage setIntValue: NO];
419 [playSound setIntValue: NO];
420 [doSpeak setIntValue: NO];
421
422 while ( (alert = [e nextObject]) != nil) {
423 if ([alert isKindOfClass: [PSDockBounceAlert class]]) {
[522]424 [bounceDockIcon setIntValue: YES]; // temporary for 1.1b8
[53]425 } else if ([alert isKindOfClass: [PSScriptAlert class]]) {
426 [doScript setIntValue: YES];
427 [script setAlias: [(PSScriptAlert *)alert scriptFileAlias]];
428 } else if ([alert isKindOfClass: [PSNotifierAlert class]]) {
429 [displayMessage setIntValue: YES];
[133]430 } else if ([alert isKindOfClass: [PSMediaAlert class]]) {
431 unsigned int repetitions = [(PSMediaAlert *)alert repetitions];
[53]432 [playSound setIntValue: YES];
433 [soundRepetitions setIntValue: repetitions];
434 [soundRepetitionStepper setIntValue: repetitions];
[133]435 [self _setVolume: [(PSMediaAlert *)alert outputVolume] withPreview: NO];
436 if ([alert isKindOfClass: [PSBeepAlert class]]) {
437 [sound setAlias: nil];
438 } else if ([alert isKindOfClass: [PSMovieAlert class]]) {
439 [sound setAlias: [(PSMovieAlert *)alert movieFileAlias]];
440 }
[53]441 } else if ([alert isKindOfClass: [PSSpeechAlert class]]) {
442 [doSpeak setIntValue: YES];
443 [voice setVoice: [(PSSpeechAlert *)alert voice]];
[61]444 } else if ([alert isKindOfClass: [PSWakeAlert class]]) {
445 [wakeUp setIntValue: YES];
[53]446 }
[21]447}
[61]448}
[21]449
[53]450- (BOOL)_setAlerts;
451{
452 PSAlerts *alerts = [alarm alerts];
453
454 [alerts removeAlerts];
[364]455 @try {
[53]456 // dock bounce alert
[522]457 if ([bounceDockIcon intValue]) // temporary for 1.1b8
[53]458 [alerts addAlert: [PSDockBounceAlert alert]];
459 // script alert
460 if ([doScript intValue]) {
461 BDAlias *scriptFileAlias = [script alias];
462 if (scriptFileAlias == nil) {
463 [self setStatus: @"Unable to set script alert (no script specified?)"];
464 return NO;
465 }
466 [alerts addAlert: [PSScriptAlert alertWithScriptFileAlias: scriptFileAlias]];
467 }
468 // notifier alert
469 if ([displayMessage intValue])
470 [alerts addAlert: [PSNotifierAlert alert]];
471 // sound alerts
472 if ([playSound intValue]) {
473 BDAlias *soundAlias = [sound selectedAlias];
474 unsigned short numReps = [soundRepetitions intValue];
[133]475 PSMediaAlert *alert;
[53]476 if (soundAlias == nil) // beep alert
[133]477 alert = [PSBeepAlert alertWithRepetitions: numReps];
[53]478 else // movie alert
[133]479 alert = [PSMovieAlert alertWithMovieFileAlias: soundAlias repetitions: numReps];
480 [alerts addAlert: alert];
481 [alert setOutputVolume: [sound outputVolume]];
[53]482 }
483 // speech alert
484 if ([doSpeak intValue])
[364]485 [alerts addAlert: [PSSpeechAlert alertWithVoice: [[voice selectedItem] representedObject]]];
[61]486 // wake alert
487 if ([wakeUp intValue])
488 [alerts addAlert: [PSWakeAlert alert]];
[53]489 [[NSUserDefaults standardUserDefaults] setObject: [alerts propertyListRepresentation] forKey: PSAlertsSelected];
[364]490 } @catch (NSException *exception) {
491 [self setStatus: [exception reason]];
492 return NO;
493 }
[53]494 return YES;
495}
496
497#pragma mark actions
498
[21]499// to ensure proper updating of interval, this should be the only method by which the window is shown (e.g. from the Alarm menu)
500- (IBAction)showWindow:(id)sender;
501{
[522]502 NSWindow *window = [self window];
503
504 if (![window isVisible]) {
[53]505 NSDate *today = [NSCalendarDate dateForDay: [NSDate date]];
506 if ([(NSDate *)[timeDate objectValue] compare: today] == NSOrderedAscending) {
507 [timeDate setObjectValue: today];
508 }
[21]509 [self update: self];
[53]510 // XXX bug workaround - otherwise, first responder appears to alternate every time the window is shown. And if you set the initial first responder, you can't tab in the window. :(
[522]511 [window makeFirstResponder: [window initialFirstResponder]];
[21]512 }
[522]513
[21]514 [super showWindow: sender];
515}
516
517- (IBAction)setAlarm:(NSButton *)sender;
518{
[45]519 // set alerts before setting alarm...
[53]520 if (![self _setAlerts]) return;
[45]521
522 // set alarm
523 [self setAlarmDateAndInterval: sender];
[53]524 [alarm setRepeating: [timeIntervalRepeats state] == NSOnState];
[45]525 [alarm setMessage: [messageField stringValue]];
526 if (![alarm setTimer]) {
[102]527 [self setStatus: [@"Unable to set alarm. " stringByAppendingString: [alarm invalidMessage]]];
[45]528 return;
529 }
[34]530
[24]531 [self setStatus: [[alarm date] descriptionWithCalendarFormat: @"Alarm set for %x at %X" timeZone: nil locale: nil]];
[21]532 [[self window] close];
[26]533 [alarm release];
534 alarm = [[PSAlarm alloc] init];
[21]535}
536
[34]537- (IBAction)silence:(id)sender;
538{
539 [sound stopSoundPreview: self];
540 [voice stopVoicePreview: self];
541}
542
[102]543- (BOOL)validateUserInterfaceItem:(id <NSValidatedUserInterfaceItem>)anItem;
544{
545 if ([anItem action] == @selector(toggleAlertEditor:)) {
546 if ([NSApp keyWindow] != [self window])
547 return NO;
548 [(NSMenuItem *)anItem setState: [editAlert intValue] ? NSOnState : NSOffState];
549 }
550 return YES;
551}
552
[26]553@end
554
555@implementation PSAlarmSetController (NSControlSubclassDelegate)
556
[53]557- (BOOL)control:(NSControl *)control didFailToFormatString:(NSString *)string errorDescription:(NSString *)error;
558{
559 if (control == timeInterval)
560 [timeInterval handleDidFailToFormatString: string errorDescription: error label: @"alarm interval"];
[364]561 else if (control == soundRepetitions)
562 [soundRepetitions handleDidFailToFormatString: string errorDescription: error label: @"alert repetitions"];
[53]563 return NO;
564}
565
[26]566- (void)control:(NSControl *)control didFailToValidatePartialString:(NSString *)string errorDescription:(NSString *)error;
567{
568 // NSLog(@"UPDATING FROM validation");
[53]569 if (control == timeInterval) [self update: timeInterval]; // make sure we still examine the field editor, otherwise if the existing numeric string is invalid, it'll be cleared
[26]570}
571
572@end
573
574@implementation PSAlarmSetController (NSWindowNotifications)
575
576- (void)windowWillClose:(NSNotification *)notification;
577{
578 // NSLog(@"stopping update timer");
[34]579 [self silence: nil];
[26]580 [self _stopUpdateTimer];
[53]581 [self _setAlerts];
[26]582}
583
584@end
585
586@implementation PSAlarmSetController (NSControlSubclassNotifications)
587
[21]588// called because we're the delegate
589
[588]590- (void)controlTextDidEndEditing:(NSNotification *)notification;
591{
592 if ([notification object] != timeOfDay)
593 return;
594
595 // if date is today and we've picked a time before now, set the date for tomorrow
596 NSDate *dateTime = [NSCalendarDate dateWithDate: [timeDate objectValue] atTime: [timeOfDay objectValue]];
597 if (dateTime == nil)
598 return;
599
600 NSDate *now = [NSDate date];
601 NSCalendarDate *today = [NSCalendarDate dateForDay: now];
602 NSCalendarDate *date = [NSCalendarDate dateForDay: [timeDate objectValue]];
603 if (![date isEqualToDate: today] || [dateTime compare: now] != NSOrderedAscending)
604 return;
605
606 [timeDate setObjectValue: [today dateByAddingYears: 0 months: 0 days: 1 hours: 0 minutes: 0 seconds: 0]];
607 [self update: timeOfDay];
608}
609
[21]610- (void)controlTextDidChange:(NSNotification *)notification;
611{
[43]612 // NSLog(@"UPDATING FROM controlTextDidChange: %@", [notification object]);
[21]613 [self update: [notification object]];
614}
615
[34]616@end
617
618@implementation PSAlarmSetController (NJRVoicePopUpButtonDelegate)
619
620- (NSString *)voicePopUpButton:(NJRVoicePopUpButton *)sender previewStringForVoice:(NSString *)voice;
621{
[53]622 NSString *message = [messageField stringValue];
623 if (message == nil || [message length] == 0)
624 message = [alarm message];
625 return message;
[34]626}
627
[60]628@end
629
630@implementation PSAlarmSetController (NSApplicationNotifications)
631
[61]632- (void)applicationWillTerminate:(NSNotification *)notification;
633{
634 [self _setAlerts];
635}
636
[60]637- (void)applicationWillHide:(NSNotification *)notification;
638{
639 if ([[self window] isVisible]) {
640 [self silence: nil];
641 [self _stopUpdateTimer];
642 }
643}
644
645- (void)applicationDidUnhide:(NSNotification *)notification;
646{
647 if ([[self window] isVisible]) {
648 [self update: self];
649 }
650}
651
[522]652@end
Note: See TracBrowser for help on using the repository browser.