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

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

Wrap date to second line in alarm preview.

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