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

Last change on this file since 516 was 364, checked in by Nicholas Riley, 13 years ago

English.lproj/Alarms.nib: Specify alternating row coloring in the nib,
now we're 10.4+.

English.lproj/InfoPlist.strings: Updated for 1.1b6.

English.lproj/Localizable.strings: Quote alarm message in pretty
description (used in tooltip). Change voice error now it no longer
incorporates OSStatus.

English.lproj/MainMenu.nib: Add speech prefs again; turn repetitions
field into a NJRValidatingField and hook up its delegate.

Info-Pester.plist: Updated for 1.1b6.

NJRHotKey.m: Switch to new Objective-C exception style.

NJRIntervalField.[hm]: Now a subclass of NJRValidatingField.

NJRTableDelegate.m: Get rid of our own tooltip support as NSTableView
now supports them (though with a minor visual glitch on the first
tooltip).

NJRTableView.[hm]: Remove tooltip support. Remove alternating row
coloring support.

NJRValidatingField.[hm]: Contains validation sheet stuff from
NJRIntervalField.

NJRVoicePopUpButton.[hm]: Switch to NSSpeechSynthesizer.

PSAlarm.m: Quote alarm message in pretty description (used in
tooltip). Fix repeating alarms not restoring as repeating if they
didn't expire while Pester was not running. No longer set timer on
Pester 1.0 alarm import, to help make importing atomic.

PSAlarmSetController.[hm]: Use NJRValidatingField for repetitions
field. Switch to new Objective-C exception style. Fix validation
issues on in/at changing. Temporary changes to restore speech support
and allow the sound popup to be removed entirely from the nib (rather
than being dragged out of the visible area, as it was in 1.1b5).
Changes for NSSpeechSynthesizer, which uses different voice names.

PSAlarms.m: Switch to new Objective-C exception style. Fix
duplication and error handling in Pester 1.0 alarm import, making
atomic.

PSAlarmsController.m: Use new tooltip support (since it's implemented
in the delegate rather than the data source, we have to proxy it).

PSAlerts.m: Wrap initialization in exception block so we don't leak.

PSApplication.m: Switch to new Objective-C exception style.

PSMediaAlert.m: Clamp repetitions at 1..99 so the user can't type an
invalid value, then quit and have it saved.

PSSpeechAlert.[hm]: Switch to NSSpeechSynthesizer. Throw an
intelligible exception if the voice is unavailable.

PSTimer.m: Switch to new Objective-C exception style.

Pester.xcodeproj: Remove VERSION generation; rename targets to be more
understandable.

Read Me.rtfd: Updated for 1.1b6.

SUSpeaker.[hm]: Gone in switch to NSSpeechSynthesizer.

VERSION: Gone - we use agvtool for everything now.

Updates/release-notes.html: Updated for 1.1b6.

Updates/updates.xml: Updated for 1.1b6.

package-Pester.sh: Use agvtool to get version. Atomically update
file on Web server to avoid partial downloads.

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    // 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    [[self window] setInitialFirstResponder: nil];
118    [[self window] makeKeyAndOrderFront: nil];
119}
120
121- (void)setStatus:(NSString *)aString;
122{
123    // NSLog(@"%@", alarm);
124    if (aString != status) {
125        [status release]; status = nil;
126        status = [aString retain];
127        [timeSummary setStringValue: status];
128    }
129}
130
131// XXX with -[NSControl currentEditor] don't need to compare?  Also check -[NSControl validateEditing]
132- (id)objectValueForTextField:(NSTextField *)field whileEditing:(id)sender;
133{
134    if (sender == field) {
135        NSString *stringValue = [[[self window] fieldEditor: NO forObject: field] string];
136        id obj = nil;
137        [[field formatter] getObjectValue: &obj forString: stringValue errorDescription: NULL];
138        // NSLog(@"from field editor: %@", obj);
139        return obj;
140    } else {
141        // NSLog(@"from field: %@", [field objectValue]);
142        return [field objectValue];
143    }
144}
145
146#pragma mark date/interval setting
147
148- (void)setAlarmDateAndInterval:(id)sender;
149{
150    if (isInterval) {
151        [alarm setInterval: [timeInterval interval]];
152    } else {
153        [alarm setForDate: [self objectValueForTextField: timeDate whileEditing: sender]
154                   atTime: [self objectValueForTextField: timeOfDay whileEditing: sender]];
155    }
156}
157
158- (void)_stopUpdateTimer;
159{
160    [updateTimer invalidate]; [updateTimer release]; updateTimer = nil;
161}
162
163- (IBAction)updateDateDisplay:(id)sender;
164{
165    // NSLog(@"updateDateDisplay: %@", sender);
166    if ([alarm isValid]) {
167        [self setStatus: [NSString stringWithFormat: @"Alarm will be set for %@ on %@.", [alarm timeString], [alarm dateString]]];
168        [setButton setEnabled: YES];
169        if (updateTimer == nil || ![updateTimer isValid]) {
170            // 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.
171            // NSLog(@"setting timer");
172            if (isInterval) {
173                updateTimer = [NSTimer scheduledTimerWithTimeInterval: 1 target: self selector: @selector(updateDateDisplay:) userInfo: nil repeats: YES];
174            } else {
175                updateTimer = [NSTimer scheduledTimerWithTimeInterval: [alarm interval] target: self selector: @selector(updateDateDisplay:) userInfo: nil repeats: NO];
176            }
177            [updateTimer retain];
178        }
179    } else {
180        [setButton setEnabled: NO];
181        [self setStatus: [alarm invalidMessage]];
182        [self _stopUpdateTimer];
183    }
184}
185
186// 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.
187// Note: finding out whether a given control is editing is easier.  See: <http://cocoa.mamasam.com/COCOADEV/2002/03/2/28501.php>.
188
189- (IBAction)update:(id)sender;
190{
191    // NSLog(@"update: %@", sender);
192    [self setAlarmDateAndInterval: sender];
193    [self updateDateDisplay: sender];
194}
195
196- (IBAction)inAtChanged:(id)sender;
197{
198    NSButtonCell *new = [inAtMatrix selectedCell], *old;
199    isInterval = ([inAtMatrix selectedTag] == 0);
200    old = [inAtMatrix cellWithTag: isInterval];
201    NSAssert(new != old, @"in and at buttons should be distinct!");
202   
203    if (sender != nil) {
204        // XXX validation doesn't work properly for date/time, so we just universally cancel editing now
205        if (![[self window] makeFirstResponder: nil] && !isInterval) {
206            // 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.
207            NSInvocation *i = [NSInvocation invocationWithMethodSignature:
208                               [inAtMatrix methodSignatureForSelector: @selector(selectCellWithTag:)]];
209            int tag = [old tag];
210            [i setSelector: @selector(selectCellWithTag:)];
211            [i setTarget: inAtMatrix];
212            [i setArgument: &tag atIndex: 2];
213            [NSTimer scheduledTimerWithTimeInterval: 0 invocation: i repeats: NO];
214            return;
215        }
216    }
217   
218    [old setKeyEquivalent: [new keyEquivalent]];
219    [old setKeyEquivalentModifierMask: [new keyEquivalentModifierMask]];
220    [new setKeyEquivalent: @""];
221    [new setKeyEquivalentModifierMask: 0];
222    [timeInterval setEnabled: isInterval];
223    [timeIntervalUnits setEnabled: isInterval];
224    [timeIntervalRepeats setEnabled: isInterval];
225    [timeOfDay setEnabled: !isInterval];
226    [timeDate setEnabled: !isInterval];
227    [timeDateCompletions setEnabled: !isInterval && [NJRDateFormatter naturalLanguageParsingAvailable]];
228    [timeCalendarButton setEnabled: !isInterval];
229    if (sender != nil)
230        [[self window] makeFirstResponder: isInterval ? (NSTextField *)timeInterval : timeOfDay];
231    if (!isInterval) // need to do this every time the controls are enabled
232        [timeOfDay setNextKeyView: timeDate];
233    // NSLog(@"UPDATING FROM inAtChanged");
234    [self update: nil];
235}
236
237- (IBAction)dateCompleted:(NSPopUpButton *)sender;
238{
239    [timeDate setStringValue: [sender titleOfSelectedItem]];
240    [self update: sender];
241}
242
243#pragma mark calendar
244
245- (IBAction)showCalendar:(NSButton *)sender;
246{
247    [PSCalendarController controllerWithDate: [NSCalendarDate dateForDay: [timeDate objectValue]] delegate: self];
248}
249
250- (void)calendarController:(PSCalendarController *)calendar didSetDate:(NSCalendarDate *)date;
251{
252    [timeDate setObjectValue: date];
253    [self update: self];
254}
255
256- (NSView *)calendarControllerLaunchingView:(PSCalendarController *)controller;
257{
258    return timeCalendarButton;
259}
260
261#pragma mark volume
262
263- (IBAction)showVolume:(NSButton *)sender;
264{
265    [PSVolumeController controllerWithVolume: [sound outputVolume] delegate: self];
266}
267
268#define VOLUME_IMAGE_INDEX(vol) (vol * 4) - 0.01
269
270- (void)_setVolume:(float)volume withPreview:(BOOL)preview;
271{
272    float outputVolume = [sound outputVolume];
273    short volumeImageIndex = VOLUME_IMAGE_INDEX(volume);
274
275    if (outputVolume > 0 && volumeImageIndex == VOLUME_IMAGE_INDEX(outputVolume)) return;
276    NSString *volumeImageName = [NSString stringWithFormat: @"Volume %ld", volumeImageIndex];
277    [soundVolumeButton setImage: [NSImage imageNamed: volumeImageName]];
278
279    [sound setOutputVolume: volume withPreview: preview];
280}
281
282- (void)volumeController:(PSVolumeController *)controller didSetVolume:(float)volume;
283{
284    [self _setVolume: volume withPreview: YES];
285}
286
287- (NSView *)volumeControllerLaunchingView:(PSVolumeController *)controller;
288{
289    return soundVolumeButton;
290}
291
292#pragma mark alert editing
293
294- (IBAction)toggleAlertEditor:(id)sender;
295{
296    [editAlert performClick: self];
297}
298
299- (IBAction)editAlertChanged:(id)sender;
300{
301    BOOL editAlertSelected = [editAlert intValue];
302    NSView *editAlertControl = [editAlert controlView];
303    NSWindow *window = [self window];
304    NSRect frame = [window frame];
305    if (editAlertSelected) {
306        NSSize editWinSize = [window maxSize];
307        [editAlertControl 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 == editAlertControl || [oldResponder isKindOfClass: [NSView class]] && [(NSView *)oldResponder isDescendantOf: alertTabs])
332                [window makeFirstResponder: messageField]; // would use editAlertControl, but can't get it to display anomaly-free.
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)];
345        [editAlertControl setNextKeyView: cancelButton];
346    }
347    if (sender != nil) {
348        [[NSUserDefaults standardUserDefaults] setBool: editAlertSelected forKey: PSAlertsEditing];
349    }
350}
351
352- (IBAction)playSoundChanged:(id)sender;
353{
354    BOOL playSoundSelected = [playSound intValue];
355    BOOL canRepeat = playSoundSelected; // ? [sound canRepeat] : NO; // XXX temporary for 1.1b6
356    [sound setEnabled: playSoundSelected];
357    [soundRepetitions setEnabled: canRepeat];
358    [soundVolumeButton setEnabled: canRepeat && [sound hasAudio]];
359    [soundRepetitionStepper setEnabled: canRepeat];
360    [soundRepetitionsLabel setTextColor: canRepeat ? [NSColor controlTextColor] : [NSColor disabledControlTextColor]];
361    if (playSoundSelected && sender == playSound) {
362        [[self window] makeFirstResponder: soundRepetitions]; // sound]; // XXX temporary for 1.1b6
363    }
364}
365
366- (IBAction)setSoundRepetitionCount:(id)sender;
367{
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) {
376        [soundRepetitions setIntValue: newReps];
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    }
381}
382
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];
390    if (doScriptSelected && sender != nil) {
391        [[self window] makeFirstResponder: scriptSelectButton];
392        if ([script alias] == nil) [scriptSelectButton performClick: sender];
393    } else {
394        [[self window] makeFirstResponder: sender];
395    }
396}
397
398- (IBAction)doSpeakChanged:(id)sender;
399{
400    BOOL doSpeakSelected = [doSpeak state] == NSOnState;
401    [voice setEnabled: doSpeakSelected];
402    if (doSpeakSelected && sender != nil)
403        [[self window] makeFirstResponder: voice];
404    else
405        [[self window] makeFirstResponder: sender];     
406}
407
408- (void)_readAlerts:(PSAlerts *)alerts;
409{
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]]) {
424            [bounceDockIcon setState: NSOnState];
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];
430        } else if ([alert isKindOfClass: [PSMediaAlert class]]) {
431            unsigned int repetitions = [(PSMediaAlert *)alert repetitions];
432            [playSound setIntValue: YES];
433            [soundRepetitions setIntValue: repetitions];
434            [soundRepetitionStepper setIntValue: repetitions];
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            }
441        } else if ([alert isKindOfClass: [PSSpeechAlert class]]) {
442            [doSpeak setIntValue: YES];
443            [voice setVoice: [(PSSpeechAlert *)alert voice]];
444        } else if ([alert isKindOfClass: [PSWakeAlert class]]) {
445            [wakeUp setIntValue: YES];
446        }
447}
448}
449
450- (BOOL)_setAlerts;
451{
452    PSAlerts *alerts = [alarm alerts];
453   
454    [alerts removeAlerts];
455    @try {
456        // dock bounce alert
457        if ([bounceDockIcon state] == NSOnState)
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];
475            PSMediaAlert *alert;
476            if (soundAlias == nil) // beep alert
477                alert = [PSBeepAlert alertWithRepetitions: numReps];
478            else // movie alert
479                alert = [PSMovieAlert alertWithMovieFileAlias: soundAlias repetitions: numReps];
480            [alerts addAlert: alert];
481            [alert setOutputVolume: [sound outputVolume]];
482        }
483        // speech alert
484        if ([doSpeak intValue])
485            [alerts addAlert: [PSSpeechAlert alertWithVoice: [[voice selectedItem] representedObject]]];
486        // wake alert
487        if ([wakeUp intValue])
488            [alerts addAlert: [PSWakeAlert alert]];
489        [[NSUserDefaults standardUserDefaults] setObject: [alerts propertyListRepresentation] forKey: PSAlertsSelected];
490    } @catch (NSException *exception) {
491        [self setStatus: [exception reason]];
492        return NO;
493    }
494    return YES;
495}
496
497#pragma mark actions
498
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{
502    if (![[self window] isVisible]) {
503        NSDate *today = [NSCalendarDate dateForDay: [NSDate date]];
504        if ([(NSDate *)[timeDate objectValue] compare: today] == NSOrderedAscending) {
505            [timeDate setObjectValue: today];
506        }
507        [self update: self];
508        // 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. :(
509        [[self window] makeFirstResponder: [[self window] initialFirstResponder]];
510    }
511    [super showWindow: sender];
512}
513
514- (IBAction)setAlarm:(NSButton *)sender;
515{
516    // set alerts before setting alarm...
517    if (![self _setAlerts]) return;
518
519    // set alarm
520    [self setAlarmDateAndInterval: sender];
521    [alarm setRepeating: [timeIntervalRepeats state] == NSOnState];
522    [alarm setMessage: [messageField stringValue]];
523    if (![alarm setTimer]) {
524        [self setStatus: [@"Unable to set alarm. " stringByAppendingString: [alarm invalidMessage]]];
525        return;
526    }
527   
528    [self setStatus: [[alarm date] descriptionWithCalendarFormat: @"Alarm set for %x at %X" timeZone: nil locale: nil]];
529    [[self window] close];
530    [alarm release];
531    alarm = [[PSAlarm alloc] init];
532}
533
534- (IBAction)silence:(id)sender;
535{
536    [sound stopSoundPreview: self];
537    [voice stopVoicePreview: self];
538}
539
540- (BOOL)validateUserInterfaceItem:(id <NSValidatedUserInterfaceItem>)anItem;
541{
542    if ([anItem action] == @selector(toggleAlertEditor:)) {
543        if ([NSApp keyWindow] != [self window])
544            return NO;
545        [(NSMenuItem *)anItem setState: [editAlert intValue] ? NSOnState : NSOffState];
546    }
547    return YES;
548}
549
550@end
551
552@implementation PSAlarmSetController (NSControlSubclassDelegate)
553
554- (BOOL)control:(NSControl *)control didFailToFormatString:(NSString *)string errorDescription:(NSString *)error;
555{
556    if (control == timeInterval)
557        [timeInterval handleDidFailToFormatString: string errorDescription: error label: @"alarm interval"];
558    else if (control == soundRepetitions)
559        [soundRepetitions handleDidFailToFormatString: string errorDescription: error label: @"alert repetitions"];
560    return NO;
561}
562
563- (void)control:(NSControl *)control didFailToValidatePartialString:(NSString *)string errorDescription:(NSString *)error;
564{
565    // NSLog(@"UPDATING FROM validation");
566    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
567}
568
569@end
570
571@implementation PSAlarmSetController (NSWindowNotifications)
572
573- (void)windowWillClose:(NSNotification *)notification;
574{
575    // NSLog(@"stopping update timer");
576    [self silence: nil];
577    [self _stopUpdateTimer];
578    [self _setAlerts];
579}
580
581@end
582
583@implementation PSAlarmSetController (NSControlSubclassNotifications)
584
585// called because we're the delegate
586
587- (void)controlTextDidChange:(NSNotification *)notification;
588{
589    // NSLog(@"UPDATING FROM controlTextDidChange: %@", [notification object]);
590    [self update: [notification object]];
591}
592
593@end
594
595@implementation PSAlarmSetController (NJRVoicePopUpButtonDelegate)
596
597- (NSString *)voicePopUpButton:(NJRVoicePopUpButton *)sender previewStringForVoice:(NSString *)voice;
598{
599    NSString *message = [messageField stringValue];
600    if (message == nil || [message length] == 0)
601        message = [alarm message];
602    return message;
603}
604
605@end
606
607@implementation PSAlarmSetController (NSApplicationNotifications)
608
609- (void)applicationWillTerminate:(NSNotification *)notification;
610{
611    [self _setAlerts];
612}
613
614- (void)applicationWillHide:(NSNotification *)notification;
615{
616    if ([[self window] isVisible]) {
617        [self silence: nil];
618        [self _stopUpdateTimer];
619    }
620}
621
622- (void)applicationDidUnhide:(NSNotification *)notification;
623{
624    if ([[self window] isVisible]) {
625        [self update: self];
626    }
627}
628
629@end
Note: See TracBrowser for help on using the repository browser.