source: releases/Pester/1.1a3/Source/PSAlarmSetController.m@ 567

Last change on this file since 567 was 45, checked in by Nicholas Riley, 22 years ago

Pester 1.1a2.

English.lproj/Credits.html: Fixed some HTML formatting issues, added Ben Hines to credits (thanks for helping out with 1.1a1 testing!)

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

English.lproj/MainMenu.nib: Reconnected initialFirstResponder outlet on the window; somehow it became disconnected. Fixed keyboard navigation loop. Removed formatters from date/time fields which were causing crashes on launch on 10.2 (they're instantiated from code in any case). Removed text from date field because it didn't work without the formatter.

NJRDateFormatter: Workaround for 10.2 NSScanner bug [Ben Hines].

NJRQTMediaPopUpButton: Remove corrupt JPEG note, can no longer reproduce. Removed -validateRecentMedia invocation, debug code shouldn't have been left in.

PSAlarmSetController: Set alerts before setting alarm, otherwise alarm in bogus state remains. Set date to today in awakeFromNib, moved from the nib. Disconnect initial first responder to work around 10.1 bug so keyboard focus is set properly when the window opens.

Pester.pbproj: Added VERSION.

Read Me.rtfd: Updated for 1.1a2.

VERSION: Updated for 1.1a2.

File size: 15.2 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 "NJRDateFormatter.h"
12#import "NJRFSObjectSelector.h"
13#import "NJRQTMediaPopUpButton.h"
14#import "NJRVoicePopUpButton.h"
15#import <Carbon/Carbon.h>
16
17#import "PSDockBounceAlert.h"
18#import "PSScriptAlert.h"
19#import "PSNotifierAlert.h"
20#import "PSBeepAlert.h"
21#import "PSMovieAlert.h"
22#import "PSSpeechAlert.h"
23
24/* Bugs to file:
25
26¥ any trailing spaces: -> exception for +[NSCalendarDate dateWithNaturalLanguageString]:
27 > NSCalendarDate dateWithNaturalLanguageString: '12 '
28 format error: internal error
29
30¥ NSDate natural language stuff in NSCalendarDate (why?), misspelled category name
31¥ NSCalendarDate natural language stuff behaves differently from NSDateFormatter (AM/PM has no effect, shouldn't they share code?)
32¥ descriptionWithCalendarFormat:, dateWithNaturalLanguageString: does not default to current locale, instead it defaults to US unless you tell it otherwise
33¥ 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
34¥ NSTimeFormatString does not include %p when it should, meaning that AM/PM is stripped yet 12-hour time is still used
35¥ 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?
36¥ "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.
37¥ 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).
38¥ If you feed NSCalendarDate dateWithNaturalLanguageString: an " AM"/" PM" locale, it doesn't accept that date format.
39¥ descriptions for %X and %x are reversed (time zone is in %X, not %x)
40¥ too hard to implement date-only or time-only formatters
41¥ should be able to specify that natural language favors date or time (10 = 10th of month, not 10am)
42¥ please expose the iCal controls!
43
44*/
45
46@interface PSAlarmSetController (Private)
47
48- (void)_stopUpdateTimer;
49
50@end
51
52@implementation PSAlarmSetController
53
54- (void)awakeFromNib;
55{
56 alarm = [[PSAlarm alloc] init];
57 [[self window] center];
58 // XXX excessive retention of formatters? check later...
59 [timeOfDay setFormatter: [[NJRDateFormatter alloc] initWithDateFormat: [NJRDateFormatter localizedTimeFormatIncludingSeconds: NO] allowNaturalLanguage: YES]];
60 [timeDate setFormatter: [[NJRDateFormatter alloc] initWithDateFormat: [NJRDateFormatter localizedDateFormatIncludingWeekday: NO] allowNaturalLanguage: YES]];
61 {
62 NSArray *dayNames = [[NSUserDefaults standardUserDefaults] arrayForKey:
63 NSWeekDayNameArray];
64 NSArray *completions = [timeDateCompletions itemTitles];
65 NSEnumerator *e = [completions objectEnumerator];
66 NSString *title;
67 int itemIndex = 0;
68 NSRange matchingRange;
69 while ( (title = [e nextObject]) != nil) {
70 matchingRange = [title rangeOfString: @"ÇdayÈ"];
71 if (matchingRange.location != NSNotFound) {
72 NSMutableString *format = [title mutableCopy];
73 NSEnumerator *we = [dayNames objectEnumerator];
74 NSString *dayName;
75 [format deleteCharactersInRange: matchingRange];
76 [format insertString: @"%@" atIndex: matchingRange.location];
77 [timeDateCompletions removeItemAtIndex: itemIndex];
78 while ( (dayName = [we nextObject]) != nil) {
79 [timeDateCompletions insertItemWithTitle: [NSString stringWithFormat: format, dayName] atIndex: itemIndex];
80 itemIndex++;
81 }
82 } else itemIndex++;
83 }
84 }
85 [timeDate setObjectValue: [NSDate date]];
86 [self inAtChanged: nil];
87 [self playSoundChanged: nil];
88 [self doScriptChanged: nil];
89 [self doSpeakChanged: nil];
90 [script setFileTypes: [NSArray arrayWithObjects: @"applescript", @"script", NSFileTypeForHFSTypeCode(kOSAFileType), NSFileTypeForHFSTypeCode('TEXT'), nil]];
91 [[NSNotificationCenter defaultCenter] addObserver: self selector: @selector(silence:) name: PSAlarmAlertStopNotification object: nil];
92 [[NSNotificationCenter defaultCenter] addObserver: self selector: @selector(playSoundChanged:) name: NJRQTMediaPopUpButtonMovieChangedNotification object: sound];
93 [voice setDelegate: self];
94 if (floor(NSAppKitVersionNumber) <= NSAppKitVersionNumber10_1) {
95 // XXX workaround for 10.1.x bug which sets the first responder to the wrong field, but it works if I set the initial first responder to nil... go figure.
96 [[self window] setInitialFirstResponder: nil];
97 }
98 [[self window] makeKeyAndOrderFront: nil];
99}
100
101- (void)setStatus:(NSString *)aString;
102{
103 // NSLog(@"%@", alarm);
104 if (aString != status) {
105 [status release]; status = nil;
106 status = [aString retain];
107 [timeSummary setStringValue: status];
108 }
109}
110
111- (id)objectValueForTextField:(NSTextField *)field whileEditing:(id)sender;
112{
113 if (sender == field) {
114 NSString *stringValue = [[[self window] fieldEditor: NO forObject: field] string];
115 id obj = nil;
116 [[field formatter] getObjectValue: &obj forString: stringValue errorDescription: NULL];
117 // NSLog(@"from field editor: %@", obj);
118 return obj;
119 } else {
120 // NSLog(@"from field: %@", [field objectValue]);
121 return [field objectValue];
122 }
123}
124
125- (void)setAlarmDateAndInterval:(id)sender;
126{
127 if (isInterval) {
128 [alarm setInterval:
129 [[self objectValueForTextField: timeInterval whileEditing: sender] intValue] *
130 [timeIntervalUnits selectedTag]];
131 } else {
132 [alarm setForDate: [self objectValueForTextField: timeDate whileEditing: sender]
133 atTime: [self objectValueForTextField: timeOfDay whileEditing: sender]];
134 }
135}
136
137- (void)_stopUpdateTimer;
138{
139 [updateTimer invalidate]; [updateTimer release]; updateTimer = nil;
140}
141
142// XXX use OACalendar?
143
144- (IBAction)updateDateDisplay:(id)sender;
145{
146 // NSLog(@"updateDateDisplay: %@", sender);
147 if ([alarm isValid]) {
148 [self setStatus: [NSString stringWithFormat: @"Alarm will be set for %@ on %@", [alarm timeString], [alarm dateString]]];
149 [setButton setEnabled: YES];
150 if (updateTimer == nil || ![updateTimer isValid]) {
151 // 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.
152 // NSLog(@"setting timer");
153 if (isInterval) {
154 updateTimer = [NSTimer scheduledTimerWithTimeInterval: 1 target: self selector: @selector(updateDateDisplay:) userInfo: nil repeats: YES];
155 } else {
156 updateTimer = [NSTimer scheduledTimerWithTimeInterval: [alarm interval] target: self selector: @selector(updateDateDisplay:) userInfo: nil repeats: NO];
157 }
158 [updateTimer retain];
159 }
160 } else {
161 [setButton setEnabled: NO];
162 [self setStatus: [alarm invalidMessage]];
163 [self _stopUpdateTimer];
164 }
165}
166
167// 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.
168// Note: finding out whether a given control is editing is easier. See: <http://cocoa.mamasam.com/COCOADEV/2002/03/2/28501.php>.
169
170- (IBAction)update:(id)sender;
171{
172 // NSLog(@"update: %@", sender);
173 [self setAlarmDateAndInterval: sender];
174 [self updateDateDisplay: sender];
175}
176
177- (IBAction)inAtChanged:(id)sender;
178{
179 NSButtonCell *new = [inAtMatrix selectedCell], *old;
180 isInterval = ([inAtMatrix selectedTag] == 0);
181 old = [inAtMatrix cellWithTag: isInterval];
182 NSAssert(new != old, @"in and at buttons should be distinct!");
183 [old setKeyEquivalent: [new keyEquivalent]];
184 [old setKeyEquivalentModifierMask: [new keyEquivalentModifierMask]];
185 [new setKeyEquivalent: @""];
186 [new setKeyEquivalentModifierMask: 0];
187 [timeInterval setEnabled: isInterval];
188 [timeIntervalUnits setEnabled: isInterval];
189 [timeIntervalRepeats setEnabled: isInterval];
190 [timeOfDay setEnabled: !isInterval];
191 [timeDate setEnabled: !isInterval];
192 [timeDateCompletions setEnabled: !isInterval];
193 if (sender != nil)
194 [[self window] makeFirstResponder: isInterval ? timeInterval : timeOfDay];
195 // NSLog(@"UPDATING FROM inAtChanged");
196 [self update: nil];
197}
198
199- (IBAction)playSoundChanged:(id)sender;
200{
201 BOOL playSoundSelected = [playSound intValue];
202 BOOL canRepeat = playSoundSelected ? [sound canRepeat] : NO;
203 [sound setEnabled: playSoundSelected];
204 [soundRepetitions setEnabled: canRepeat];
205 [soundRepetitionStepper setEnabled: canRepeat];
206 [soundRepetitionsLabel setTextColor: canRepeat ? [NSColor controlTextColor] : [NSColor disabledControlTextColor]];
207 if (playSoundSelected && sender != nil)
208 [[self window] makeFirstResponder: sound];
209}
210
211- (IBAction)setSoundRepetitionCount:(id)sender;
212{
213 NSTextView *fieldEditor = (NSTextView *)[soundRepetitions currentEditor];
214 BOOL isEditing = (fieldEditor != nil);
215 int newReps = [sender intValue], oldReps;
216 if (isEditing) {
217 // XXX work around bug where if you ask soundRepetitions for its intValue too often while it's editing, the field begins to flash
218 oldReps = [[[fieldEditor textStorage] string] intValue];
219 } else oldReps = [soundRepetitions intValue];
220 if (newReps != oldReps) {
221 [soundRepetitions setIntValue: newReps];
222 // NSLog(@"updating: new value %d, old value %d%@", newReps, oldReps, isEditing ? @", is editing" : @"");
223 // XXX work around 10.1 bug, otherwise field only displays every second value
224 if (isEditing) [soundRepetitions selectText: self];
225 }
226}
227
228// XXX should check the 'Do script:' button when someone drops a script on the button
229
230- (IBAction)doScriptChanged:(id)sender;
231{
232 BOOL doScriptSelected = [doScript intValue];
233 [script setEnabled: doScriptSelected];
234 [scriptSelectButton setEnabled: doScriptSelected];
235 if (doScriptSelected && sender != nil)
236 [[self window] makeFirstResponder: scriptSelectButton];
237}
238
239- (IBAction)doSpeakChanged:(id)sender;
240{
241 BOOL doSpeakSelected = [doSpeak intValue];
242 [voice setEnabled: doSpeakSelected];
243 if (doSpeakSelected && sender != nil)
244 [[self window] makeFirstResponder: voice];
245}
246
247- (IBAction)dateCompleted:(NSPopUpButton *)sender;
248{
249 [timeDate setStringValue: [sender titleOfSelectedItem]];
250 [self update: sender];
251}
252
253// to ensure proper updating of interval, this should be the only method by which the window is shown (e.g. from the Alarm menu)
254- (IBAction)showWindow:(id)sender;
255{
256 if (![[self window] isVisible]) {
257 [self update: self];
258 // XXX 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. :(
259 [[self window] makeFirstResponder: [[self window] initialFirstResponder]];
260 }
261 [super showWindow: sender];
262}
263
264- (IBAction)setAlarm:(NSButton *)sender;
265{
266 // set alerts before setting alarm...
267 [alarm removeAlerts];
268 // dock bounce alert
269 if ([bounceDockIcon state] == NSOnState)
270 [alarm addAlert: [PSDockBounceAlert alert]];
271 // script alert
272 if ([doScript intValue]) {
273 BDAlias *scriptFileAlias = [script alias];
274 if (scriptFileAlias == nil) {
275 [self setStatus: @"Unable to set script alert (no script specified?)"];
276 return;
277 }
278 [alarm addAlert: [PSScriptAlert alertWithScriptFileAlias: scriptFileAlias]];
279 }
280 // notifier alert
281 if ([displayMessage intValue])
282 [alarm addAlert: [PSNotifierAlert alert]];
283 // sound alerts
284 if ([playSound intValue]) {
285 BDAlias *soundAlias = [sound selectedAlias];
286 unsigned short numReps = [soundRepetitions intValue];
287 if (soundAlias == nil) // beep alert
288 [alarm addAlert: [PSBeepAlert alertWithRepetitions: numReps]];
289 else // movie alert
290 [alarm addAlert: [PSMovieAlert alertWithMovieFileAlias: soundAlias repetitions: numReps]];
291 }
292 // speech alert
293 if ([doSpeak intValue])
294 [alarm addAlert: [PSSpeechAlert alertWithVoice: [voice titleOfSelectedItem]]];
295
296 // set alarm
297 [self setAlarmDateAndInterval: sender];
298 [alarm setMessage: [messageField stringValue]];
299 if (![alarm setTimer]) {
300 [self setStatus: [@"Unable to set alarm. " stringByAppendingString: [alarm invalidMessage]]];
301 return;
302 }
303
304 [self setStatus: [[alarm date] descriptionWithCalendarFormat: @"Alarm set for %x at %X" timeZone: nil locale: nil]];
305 [[self window] close];
306 [alarm release];
307 alarm = [[PSAlarm alloc] init];
308}
309
310- (IBAction)silence:(id)sender;
311{
312 [sound stopSoundPreview: self];
313 [voice stopVoicePreview: self];
314}
315
316@end
317
318@implementation PSAlarmSetController (NSControlSubclassDelegate)
319
320- (void)control:(NSControl *)control didFailToValidatePartialString:(NSString *)string errorDescription:(NSString *)error;
321{
322 unichar c;
323 int tag;
324 unsigned length = [string length];
325 if (control != timeInterval || length == 0) return;
326 c = [string characterAtIndex: length - 1];
327 switch (c) {
328 case 's': case 'S': tag = 1; break;
329 case 'm': case 'M': tag = 60; break;
330 case 'h': case 'H': tag = 60 * 60; break;
331 default: return;
332 }
333 [timeIntervalUnits selectItemAtIndex:
334 [timeIntervalUnits indexOfItemWithTag: tag]];
335 // NSLog(@"UPDATING FROM validation");
336 [self update: timeInterval]; // make sure we still examine the field editor, otherwise if the existing numeric string is invalid, it'll be cleared
337}
338
339@end
340
341@implementation PSAlarmSetController (NSWindowNotifications)
342
343- (void)windowWillClose:(NSNotification *)notification;
344{
345 // NSLog(@"stopping update timer");
346 [self silence: nil];
347 [self _stopUpdateTimer];
348}
349
350@end
351
352@implementation PSAlarmSetController (NSControlSubclassNotifications)
353
354// called because we're the delegate
355
356- (void)controlTextDidChange:(NSNotification *)notification;
357{
358 // NSLog(@"UPDATING FROM controlTextDidChange: %@", [notification object]);
359 [self update: [notification object]];
360}
361
362@end
363
364@implementation PSAlarmSetController (NJRVoicePopUpButtonDelegate)
365
366- (NSString *)voicePopUpButton:(NJRVoicePopUpButton *)sender previewStringForVoice:(NSString *)voice;
367{
368 return [messageField stringValue];
369}
370
371@end
Note: See TracBrowser for help on using the repository browser.