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

Last change on this file since 355 was 355, checked in by Nicholas Riley, 14 years ago

English.lproj/MainMenu.nib: Modernize menu and alarm set dialog
layout. Use keyed archiving (10.2+) nib format.

Info-Pester.plist: Moved from old PBX project.

NJRFSObjectSelector.m: Bug fixes from code sent to Joey: remove
incorrect usage of tryToPerform:with:; fix logic error in menu
construction. Work around Cocoa's deciding that the menu font size
needs adjustment when it doesn't - so the menu font size now matches
the button font size, though the position is still off. Don't pop up
a menu if we're disabled. Use IconRefs? for menu icons, though not
(yet) for the button icon.

NJRHistoryTrackingComboBox.m: Remove item height adjustment
workaround; it now makes the items too tall.

NJRHotKey.m: Add a missing [super dealloc] caught by current GCC.

NJRHotKeyField.m: Add a missing [super dealloc] caught by current GCC.

NJRHotKeyManager.m: Add a missing [super dealloc] caught by current
GCC.

NJRIntervalField.m: Fix some type errors.

NJRQTMediaPopUpButton.m: Replace SoundFileManager? SPI usage, which
doesn't work in Leopard anyway, with manual enumeration of system
sounds. Start migration to QTKit. Use IconRefs? for menu icons.

NJRReadMeController.m: Change source encoding to UTF-8.

NJRSoundManager.m: Fix a type error.

NJRVoicePopUpButton.m: Change source encoding to UTF-8.

NSMenuItem-NJRExtensions.[hm]: Code from ICeCoffEE to use IconRefs? for
menu item icons.

PSAlarm.m: Change source encoding to UTF-8.

PSAlarms.m: Fix a signedness mismatch.

PSAlarmsController.m: Change source encoding to UTF-8.

PSAlarmSetController.m: Set keyboard focus after unchecking "Do
script:" and "Play" checkboxes.

PSAlerts.m: Add a missing [super dealloc] caught by current GCC. Fix
a memory leak in property list serialization.

PSPowerManager.[hm]: There's now API for scheduling wakeups; use it
(the old code asserted on startup). To fix: removing scheduled
wakeup. Fix a small type-checking error.

PSPreferencesController.m: Add a missing [super dealloc] caught by
current GCC.

PSScriptAlert.m: Change source encoding to UTF-8.

PSTimeDateEditor.m: Fix a tiny, and one-time, memory leak.

PSTimer.m: Update for new PSPowerManager API.

Pester.pbproj: Deleted; now supporting OS X 10.4+ (up from 10.1,
aiee.)

Pester.xcodeproj: Xcode 2.4+ project, upgraded targets, etc.

SoundFileManager?.h: Deleted; this SPI no longer exists in Leopard and
possibly earlier.

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