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

Last change on this file since 606 was 606, checked in by Nicholas Riley, 10 years ago

Parentheses to pacify GCC.

File size: 26.4 KB
Line 
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"
10#import "PSAlarmAlertController.h"
11#import "PSCalendarController.h"
12#import "PSPowerManager.h"
13#import "PSTimeDateEditor.h"
14#import "PSVolumeController.h"
15#import "NJRDateFormatter.h"
16#import "NJRFSObjectSelector.h"
17#import "NJRIntervalField.h"
18#import "NJRQTMediaPopUpButton.h"
19#import "NJRSoundManager.h"
20#import "NJRValidatingField.h"
21#import "NJRVoicePopUpButton.h"
22#import "NSString-NJRExtensions.h"
23#import "NSAttributedString-NJRExtensions.h"
24#import "NSCalendarDate-NJRExtensions.h"
25
26#import "PSAlerts.h"
27#import "PSDockBounceAlert.h"
28#import "PSScriptAlert.h"
29#import "PSNotifierAlert.h"
30#import "PSBeepAlert.h"
31#import "PSMovieAlert.h"
32#import "PSSpeechAlert.h"
33#import "PSWakeAlert.h"
34
35/* Bugs to file:
36
37¥ any trailing spaces: -> exception for +[NSCalendarDate dateWithNaturalLanguageString]:
38 > NSCalendarDate dateWithNaturalLanguageString: '12 '
39  format error: internal error
40
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¥ descriptionWithCalendarFormat:, dateWithNaturalLanguageString: does not default to current locale, instead it defaults to US unless you tell it otherwise
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.
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.
50¥ descriptions for %X and %x are reversed (time zone is in %X, not %x)
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.
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
58static NSString * const PSAlertsSelected = @"Pester alerts selected"; // NSUserDefaults key
59static NSString * const PSAlertsEditing = @"Pester alerts editing"; // NSUserDefaults key
60
61@interface PSAlarmSetController (Private)
62
63- (void)_readAlerts:(PSAlerts *)alerts;
64- (BOOL)_setAlerts;
65- (void)_setVolume:(float)volume withPreview:(BOOL)preview;
66- (void)_stopUpdateTimer;
67
68@end
69
70@implementation PSAlarmSetController
71
72- (void)awakeFromNib;
73{
74    NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
75    NSNotificationCenter *notificationCenter = [NSNotificationCenter defaultCenter];
76    alarm = [[PSAlarm alloc] init];
77    [[self window] center];
78    if ([[removeMessageButton image] size].width != 0)
79        [removeMessageButton setTitle: @""];
80    [PSTimeDateEditor setUpTimeField: timeOfDay dateField: timeDate completions: timeDateCompletions];
81    { // volume defaults, usually overridden by restored alert info
82        float volume = 0.5f;
83        [NJRSoundManager getDefaultOutputVolume: &volume];
84        [self _setVolume: volume withPreview: NO];
85    }
86    [editAlert setState: [defaults boolForKey: PSAlertsEditing]];
87    {
88        NSDictionary *plAlerts = [defaults dictionaryForKey: PSAlertsSelected];
89        PSAlerts *alerts = nil;
90        if (plAlerts == nil) {
91            alerts = [[PSAlerts alloc] initWithPesterVersion1Alerts];
92        } else {
93            @try {
94                alerts = [[PSAlerts alloc] initWithPropertyList: plAlerts];
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]);
97                alerts = [[PSAlerts alloc] initWithPesterVersion1Alerts];
98            }
99        }
100        [self _readAlerts: alerts];
101    }
102    [self inAtChanged: nil]; // by convention, if sender is nil, we're initializing
103    [self playSoundChanged: nil];
104    [self doScriptChanged: nil];
105    [self doSpeakChanged: nil];
106    [self editAlertChanged: nil];
107    [script setFileTypes: [NSArray arrayWithObjects: @"applescript", @"script", NSFileTypeForHFSTypeCode(kOSAFileType), NSFileTypeForHFSTypeCode('TEXT'), nil]];
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];
112    [notificationCenter addObserver: self selector: @selector(applicationWillTerminate:) name: NSApplicationWillTerminateNotification object: NSApp];
113    [voice setDelegate: self]; // XXX why don't we do this in IB?  It should use the accessor...
114    [wakeUp setEnabled: [PSPowerManager autoWakeSupported]];
115   
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.
117    NSWindow *window = [self window];
118    [window setInitialFirstResponder: nil];
119    [window makeKeyAndOrderFront: nil];
120}
121
122- (void)setStatus:(NSString *)aString;
123{
124    // NSLog(@"%@", alarm);
125    if (aString != status) {
126        [status release]; status = nil;
127        status = [aString retain];
128        [timeSummary setStringValue: status];
129    }
130}
131
132// XXX with -[NSControl currentEditor] don't need to compare?  Also check -[NSControl validateEditing]
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
147#pragma mark date/interval setting
148
149- (void)setAlarmDateAndInterval:(id)sender;
150{
151    if (isInterval) {
152        [alarm setInterval: [timeInterval interval]];
153    } else {
154        [alarm setForDate: [self objectValueForTextField: timeDate whileEditing: sender]
155                   atTime: [self objectValueForTextField: timeOfDay whileEditing: sender]];
156    }
157}
158
159- (void)_stopUpdateTimer;
160{
161    [updateTimer invalidate]; [updateTimer release]; updateTimer = nil;
162}
163
164- (IBAction)updateDateDisplay:(id)sender;
165{
166    // NSLog(@"updateDateDisplay: %@", sender);
167    if ([alarm isValid]) {
168        [self setStatus: [NSString stringWithFormat: @"Alarm will be set for %@\non %@.", [alarm timeString], [alarm dateString]]];
169        [setButton setEnabled: YES];
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        }
180    } else {
181        [setButton setEnabled: NO];
182        [self setStatus: [alarm invalidMessage]];
183        [self _stopUpdateTimer];
184    }
185}
186
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.
188// Note: finding out whether a given control is editing is easier.  See: <http://cocoa.mamasam.com/COCOADEV/2002/03/2/28501.php>.
189
190- (IBAction)update:(id)sender;
191{
192    // NSLog(@"update: %@", sender);
193    [self setAlarmDateAndInterval: sender];
194    [self updateDateDisplay: sender];
195}
196
197- (IBAction)inAtChanged:(id)sender;
198{
199    NSButtonCell *new = [inAtMatrix selectedCell], *old;
200    isInterval = ([inAtMatrix selectedTag] == 0);
201    old = [inAtMatrix cellWithTag: isInterval];
202    NSAssert(new != old, @"in and at buttons should be distinct!");
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   
219    [old setKeyEquivalent: [new keyEquivalent]];
220    [old setKeyEquivalentModifierMask: [new keyEquivalentModifierMask]];
221    [new setKeyEquivalent: @""];
222    [new setKeyEquivalentModifierMask: 0];
223    [timeInterval setEnabled: isInterval];
224    [timeIntervalUnits setEnabled: isInterval];
225    [timeIntervalRepeats setEnabled: isInterval];
226    [timeOfDay setEnabled: !isInterval];
227    [timeDate setEnabled: !isInterval];
228    [timeDateCompletions setEnabled: !isInterval && [NJRDateFormatter naturalLanguageParsingAvailable]];
229    [timeCalendarButton setEnabled: !isInterval];
230    if (sender != nil)
231        [[self window] makeFirstResponder: isInterval ? (NSTextField *)timeInterval : timeOfDay];
232    if (!isInterval) // need to do this every time the controls are enabled
233        [timeOfDay setNextKeyView: timeDate];
234    // NSLog(@"UPDATING FROM inAtChanged");
235    [self update: nil];
236}
237
238- (IBAction)dateCompleted:(NSPopUpButton *)sender;
239{
240    [timeDate setStringValue: [sender titleOfSelectedItem]];
241    [self update: sender];
242}
243
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
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
293#pragma mark alert editing
294
295- (IBAction)toggleAlertEditor:(id)sender;
296{
297    [editAlert performClick: self];
298}
299
300- (IBAction)editAlertChanged:(id)sender;
301{
302    BOOL editAlertSelected = [editAlert state] == NSOnState;
303    NSWindow *window = [self window];
304    NSRect frame = [window frame];
305    if (editAlertSelected) {
306        NSSize editWinSize = [window maxSize];
307        [editAlert setNextKeyView: [displayMessage controlView]];
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]) {
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]];
319        } else {
320            NSAttributedString *string = [[alarm alerts] prettyList];
321            if (string == nil) {
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")];
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
331            if (oldResponder == editAlert ||
332                ([oldResponder isKindOfClass: [NSView class]] && [(NSView *)oldResponder isDescendantOf: alertTabs]))
333                [window makeFirstResponder: messageField]; // would use editAlert, but can't get it to display anomaly-free.
334            [self silence: sender];
335        }
336        // allow height to expand, though not arbitrarily (should still fit on an 800x600 screen)
337        textHeight = [[alertView cell] cellSizeForBounds: NSMakeRect(0, 0, textFrame.size.width, 400)].height;
338        textFrame.origin.y += textFrame.size.height - textHeight;
339        textFrame.size.height = textHeight;
340        [alertView setFrame: textFrame];
341        viewWinSize.height += textHeight;
342        [alertTabs selectTabViewItemWithIdentifier: @"view"];
343        frame.origin.y += frame.size.height - viewWinSize.height;
344        frame.size = viewWinSize;
345        [window setFrame: frame display: (sender != nil) animate: (sender != nil)];
346        [editAlert setNextKeyView: cancelButton];
347    }
348    if (sender != nil) {
349        [[NSUserDefaults standardUserDefaults] setBool: editAlertSelected forKey: PSAlertsEditing];
350    }
351}
352
353- (IBAction)playSoundChanged:(id)sender;
354{
355    BOOL playSoundSelected = [playSound intValue];
356    BOOL canRepeat = playSoundSelected ? [sound canRepeat] : NO;
357    [sound setEnabled: playSoundSelected];
358    [soundRepetitions setEnabled: canRepeat];
359    [soundVolumeButton setEnabled: canRepeat && [sound hasAudio]];
360    [soundRepetitionStepper setEnabled: canRepeat];
361    [soundRepetitionsLabel setTextColor: canRepeat ? [NSColor controlTextColor] : [NSColor disabledControlTextColor]];
362    if (playSoundSelected && sender == playSound) {
363        [[self window] makeFirstResponder: sound];
364    }
365}
366
367- (IBAction)setSoundRepetitionCount:(id)sender;
368{
369    NSTextView *fieldEditor = (NSTextView *)[soundRepetitions currentEditor];
370    BOOL isEditing = (fieldEditor != nil);
371    int newReps = [sender intValue], oldReps;
372    if (isEditing) {
373        // XXX work around bug where if you ask soundRepetitions for its intValue too often while it's editing, the field begins to flash
374        oldReps = [[[fieldEditor textStorage] string] intValue];
375    } else oldReps = [soundRepetitions intValue];
376    if (newReps != oldReps) {
377        [soundRepetitions setIntValue: newReps];
378        // NSLog(@"updating: new value %d, old value %d%@", newReps, oldReps, isEditing ? @", is editing" : @"");
379        // XXX work around 10.1 bug, otherwise field only displays every second value
380        if (isEditing) [soundRepetitions selectText: self];
381    }
382}
383
384// XXX should check the 'Do script:' button when someone drops a script on the button
385
386- (IBAction)doScriptChanged:(id)sender;
387{
388    BOOL doScriptSelected = [doScript intValue];
389    [script setEnabled: doScriptSelected];
390    [scriptSelectButton setEnabled: doScriptSelected];
391    if (doScriptSelected && sender != nil) {
392        [[self window] makeFirstResponder: scriptSelectButton];
393        if ([script alias] == nil) [scriptSelectButton performClick: sender];
394    } else {
395        [[self window] makeFirstResponder: sender];
396    }
397}
398
399- (IBAction)doSpeakChanged:(id)sender;
400{
401    BOOL doSpeakSelected = [doSpeak state] == NSOnState;
402    [voice setEnabled: doSpeakSelected];
403    if (doSpeakSelected && sender != nil)
404        [[self window] makeFirstResponder: voice];
405    else
406        [[self window] makeFirstResponder: sender];     
407}
408
409- (void)_readAlerts:(PSAlerts *)alerts;
410{
411    NSEnumerator *e = [alerts alertEnumerator];
412    PSAlert *alert;
413   
414    [alarm setAlerts: alerts];
415
416    // turn off all alerts
417    [bounceDockIcon setState: NSOffState];
418    [doScript setIntValue: NO];
419    [displayMessage setIntValue: NO];
420    [playSound setIntValue: NO];
421    [doSpeak setIntValue: NO];
422
423    while ( (alert = [e nextObject]) != nil) {
424        if ([alert isKindOfClass: [PSDockBounceAlert class]]) {
425            [bounceDockIcon setIntValue: YES]; // temporary for 1.1b8
426        } else if ([alert isKindOfClass: [PSScriptAlert class]]) {
427            [doScript setIntValue: YES];
428            [script setAlias: [(PSScriptAlert *)alert scriptFileAlias]];
429        } else if ([alert isKindOfClass: [PSNotifierAlert class]]) {
430            [displayMessage setIntValue: YES];
431        } else if ([alert isKindOfClass: [PSMediaAlert class]]) {
432            unsigned int repetitions = [(PSMediaAlert *)alert repetitions];
433            [playSound setIntValue: YES];
434            [soundRepetitions setIntValue: repetitions];
435            [soundRepetitionStepper setIntValue: repetitions];
436            [self _setVolume: [(PSMediaAlert *)alert outputVolume] withPreview: NO];
437            if ([alert isKindOfClass: [PSBeepAlert class]]) {
438                [sound setAlias: nil];
439            } else if ([alert isKindOfClass: [PSMovieAlert class]]) {
440                [sound setAlias: [(PSMovieAlert *)alert movieFileAlias]];
441            }
442        } else if ([alert isKindOfClass: [PSSpeechAlert class]]) {
443            [doSpeak setIntValue: YES];
444            [voice setVoice: [(PSSpeechAlert *)alert voice]];
445        } else if ([alert isKindOfClass: [PSWakeAlert class]]) {
446            [wakeUp setIntValue: YES];
447        }
448}
449}
450
451- (BOOL)_setAlerts;
452{
453    PSAlerts *alerts = [alarm alerts];
454   
455    [alerts removeAlerts];
456    @try {
457        // dock bounce alert
458        if ([bounceDockIcon intValue]) // temporary for 1.1b8
459            [alerts addAlert: [PSDockBounceAlert alert]];
460        // script alert
461        if ([doScript intValue]) {
462            BDAlias *scriptFileAlias = [script alias];
463            if (scriptFileAlias == nil) {
464                [self setStatus: @"Unable to set script alert (no script specified?)"];
465                return NO;
466            }
467            [alerts addAlert: [PSScriptAlert alertWithScriptFileAlias: scriptFileAlias]];
468        }
469        // notifier alert
470        if ([displayMessage intValue])
471            [alerts addAlert: [PSNotifierAlert alert]];
472        // sound alerts
473        if ([playSound intValue]) {
474            BDAlias *soundAlias = [sound selectedAlias];
475            unsigned short numReps = [soundRepetitions intValue];
476            PSMediaAlert *alert;
477            if (soundAlias == nil) // beep alert
478                alert = [PSBeepAlert alertWithRepetitions: numReps];
479            else // movie alert
480                alert = [PSMovieAlert alertWithMovieFileAlias: soundAlias repetitions: numReps];
481            [alerts addAlert: alert];
482            [alert setOutputVolume: [sound outputVolume]];
483        }
484        // speech alert
485        if ([doSpeak intValue])
486            [alerts addAlert: [PSSpeechAlert alertWithVoice: [[voice selectedItem] representedObject]]];
487        // wake alert
488        if ([wakeUp intValue])
489            [alerts addAlert: [PSWakeAlert alert]];
490        [[NSUserDefaults standardUserDefaults] setObject: [alerts propertyListRepresentation] forKey: PSAlertsSelected];
491    } @catch (NSException *exception) {
492        [self setStatus: [exception reason]];
493        return NO;
494    }
495    return YES;
496}
497
498#pragma mark actions
499
500// to ensure proper updating of interval, this should be the only method by which the window is shown (e.g. from the Alarm menu)
501- (IBAction)showWindow:(id)sender;
502{
503    NSWindow *window = [self window];
504   
505    if (![window isVisible]) {
506        NSDate *today = [NSCalendarDate dateForDay: [NSDate date]];
507        if ([(NSDate *)[timeDate objectValue] compare: today] == NSOrderedAscending) {
508            [timeDate setObjectValue: today];
509        }
510        [self update: self];
511        // 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. :(
512        [window makeFirstResponder: [window initialFirstResponder]];
513    }
514   
515    [super showWindow: sender];
516}
517
518- (IBAction)setAlarm:(NSButton *)sender;
519{
520    // set alerts before setting alarm...
521    if (![self _setAlerts]) return;
522
523    // set alarm
524    [self setAlarmDateAndInterval: sender];
525    [alarm setRepeating: [timeIntervalRepeats state] == NSOnState];
526    [alarm setMessage: [messageField stringValue]];
527    if (![alarm setTimer]) {
528        [self setStatus: [@"Unable to set alarm. " stringByAppendingString: [alarm invalidMessage]]];
529        return;
530    }
531   
532    [self setStatus: [[alarm date] descriptionWithCalendarFormat: @"Alarm set for %x at %X" timeZone: nil locale: nil]];
533    [[self window] close];
534    [alarm release];
535    alarm = [[PSAlarm alloc] init];
536}
537
538- (IBAction)silence:(id)sender;
539{
540    [sound stopSoundPreview: self];
541    [voice stopVoicePreview: self];
542}
543
544- (BOOL)validateUserInterfaceItem:(id <NSValidatedUserInterfaceItem>)anItem;
545{
546    if ([anItem action] == @selector(toggleAlertEditor:)) {
547        if ([NSApp keyWindow] != [self window])
548            return NO;
549        [(NSMenuItem *)anItem setState: [editAlert intValue] ? NSOnState : NSOffState];
550    }
551    return YES;
552}
553
554@end
555
556@implementation PSAlarmSetController (NSControlSubclassDelegate)
557
558- (BOOL)control:(NSControl *)control didFailToFormatString:(NSString *)string errorDescription:(NSString *)error;
559{
560    if (control == timeInterval)
561        [timeInterval handleDidFailToFormatString: string errorDescription: error label: @"alarm interval"];
562    else if (control == soundRepetitions)
563        [soundRepetitions handleDidFailToFormatString: string errorDescription: error label: @"alert repetitions"];
564    return NO;
565}
566
567- (void)control:(NSControl *)control didFailToValidatePartialString:(NSString *)string errorDescription:(NSString *)error;
568{
569    // NSLog(@"UPDATING FROM validation");
570    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
571}
572
573@end
574
575@implementation PSAlarmSetController (NSWindowNotifications)
576
577- (void)windowWillClose:(NSNotification *)notification;
578{
579    // NSLog(@"stopping update timer");
580    [self silence: nil];
581    [self _stopUpdateTimer];
582    [self _setAlerts];
583}
584
585@end
586
587@implementation PSAlarmSetController (NSControlSubclassNotifications)
588
589// called because we're the delegate
590
591- (void)controlTextDidEndEditing:(NSNotification *)notification;
592{
593    if ([notification object] != timeOfDay)
594        return;
595
596    // if date is today and we've picked a time before now, set the date for tomorrow
597    NSDate *dateTime = [NSCalendarDate dateWithDate: [timeDate objectValue] atTime: [timeOfDay objectValue]];
598    if (dateTime == nil)
599        return;
600
601    NSDate *now = [NSDate date];
602    NSCalendarDate *today = [NSCalendarDate dateForDay: now];
603    NSCalendarDate *date = [NSCalendarDate dateForDay: [timeDate objectValue]];
604    if (![date isEqualToDate: today] || [dateTime compare: now] != NSOrderedAscending)
605        return;
606
607    [timeDate setObjectValue: [today dateByAddingYears: 0 months: 0 days: 1 hours: 0 minutes: 0 seconds: 0]];
608    [self update: timeOfDay];
609}
610
611- (void)controlTextDidChange:(NSNotification *)notification;
612{
613    // NSLog(@"UPDATING FROM controlTextDidChange: %@", [notification object]);
614    [self update: [notification object]];
615}
616
617@end
618
619@implementation PSAlarmSetController (NJRVoicePopUpButtonDelegate)
620
621- (NSString *)voicePopUpButton:(NJRVoicePopUpButton *)sender previewStringForVoice:(NSString *)voice;
622{
623    NSString *message = [messageField stringValue];
624    if (message == nil || [message length] == 0)
625        message = [alarm message];
626    return message;
627}
628
629@end
630
631@implementation PSAlarmSetController (NSApplicationNotifications)
632
633- (void)applicationWillTerminate:(NSNotification *)notification;
634{
635    [self _setAlerts];
636}
637
638- (void)applicationWillHide:(NSNotification *)notification;
639{
640    if ([[self window] isVisible]) {
641        [self silence: nil];
642        [self _stopUpdateTimer];
643    }
644}
645
646- (void)applicationDidUnhide:(NSNotification *)notification;
647{
648    if ([[self window] isVisible]) {
649        [self update: self];
650    }
651}
652
653@end
Note: See TracBrowser for help on using the repository browser.