source: releases/Pester/1.1b5/Source/PSAlarmSetController.m@ 495

Last change on this file since 495 was 361, checked in by Nicholas Riley, 17 years ago

VERSION: Updated for 1.1b5.

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

English.lproj/MainMenu.nib: Change IB compatibility level to 10.4.
Disable controls which lead to unstable functionality in this version.
Added Sparkle support. Hook up removeMessageButton to
PSAlarmSetController for "-" fallback drawing.

English.lproj/Preferences.nib: Added Sparkle checkbox.

English.lproj/Read Me.nib: Leopard-ized with source list coloring,
highlight and removed focus ring (which caused artifacts anyway).

English.lproj/Snooze until.nib: Use new natural language date strings
and calendar button bezel style.

Info-Pester.plist: Updated for 1.1b5. Added Sparkle support.

NJRDateFormatter.m: Comment out debug logging on every keystroke.

PSAlarmSetController.[hm]: If "-" image exists (on Leopard), don't
draw it with text as well.

PSAlerts.m: Don't describe bounce alert (which is present for
backwards compatibility, but doesn't work yet in this version).

PSPowerManager.m: Temporarily disable; untested in this version.

Pester.xcodeproj: Change Development/Deployment to Debug/Release.
Link to Sparkle. STRIP_INSTALLED_PRODUCT=NO on release version for
now (eventually, can investigate using symbol files).

Read Me.rtfd: Updated for 1.1b5. Use real links. Clean up release
notes. Credit Sparkle and Date::Manip. Clean up styles so they look
better in the read me viewer. Fix Omni link.

Sparkle.diff: Changes to Sparkle, which is included as an external.
These aren't yet automatically applied.

Updates/Application icon.png: The icon converted to a PNG. This will
probably go away as it's too big.

Updates/release-notes.css: Release notes CSS based on the example that
comes with Sparkle.

Updates/release-notes.html: Dummy release notes file; nobody should
see this yet.

Updates/updates.xml: Initial Sparkle appcast file.

package-Pester.sh: Update for xcodebuild, Sparkle, etc. No longer
include source in download.

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