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

Last change on this file since 43 was 43, checked in by Nicholas Riley, 21 years ago

Pester 1.1a1.

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

English.lproj/MainMenu.nib: Placeholder for day names in popup menu, fixed up by code (this means you can still edit it from IB though). Added command-shift-T to both in/at cells (required, code removes one or the other as appropriate). Fixed up sizes of fields. Default to today (this will need fixing when we localized the word "today", but it's fine for now...).

English.lproj/Notifier.nib: Remove date formatter because we set a string directly now instead (could set formatter from code, but we don't).

NJRDateFormatter: many workarounds for Cocoa bugs: missing AM/PM, incorrect results with space before AM/PM, etc. Added class methods to do format manipulation and return localized formats which work for output (though not always for input; this class has an internal workaround for the AM/PM problem).

NJRFSObjectSelector: properly handle enabled attribute, store internally and report externally as appropriate. Previously, the button would become enabled if you dropped something on it even if it was supposed to be disabled.

NJRQTMediaPopUpButton: stop sound preview when button disabled.

NJRVoicePopUpButton: stop voice preview when button disabled.

PSAlarm: new method -dateString returns long date string. Maintain local copy of long date, short date and time formats, and locale, using NJRDateFormatter.

PSAlarmNotifierController: update to use -[PSAlarm dateString], -[PSAlarm timeString] for localization instead of using broken formatter.

PSAlarmSetController: update documentation for some more Cocoa bugs I need to file. Set time of day and date formatters with localized date formats from NJRDateFormatter (retain/release issue here?) Localize weekday popup for predefined dates. Localize static date display with NJRDateFormatter. Note a solution (thanks to Douglas Davidson) for figuring out which control is editing. Added command-shift-T key equivalent to toggle in/at. Properly work around bugs witih soundRepetitionCount flashing, except where it's impossible to do anything else.

Read Me.rtfd: Updated for 1.1a1.

VERSION: Updated for 1.1a1.

File size: 14.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 "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 [self inAtChanged: nil];
86 [self playSoundChanged: nil];
87 [self doScriptChanged: nil];
88 [self doSpeakChanged: nil];
89 [script setFileTypes: [NSArray arrayWithObjects: @"applescript", @"script", NSFileTypeForHFSTypeCode(kOSAFileType), NSFileTypeForHFSTypeCode('TEXT'), nil]];
90 [[NSNotificationCenter defaultCenter] addObserver: self selector: @selector(silence:) name: PSAlarmAlertStopNotification object: nil];
91 [[NSNotificationCenter defaultCenter] addObserver: self selector: @selector(playSoundChanged:) name: NJRQTMediaPopUpButtonMovieChangedNotification object: sound];
92 [voice setDelegate: self];
93 [[self window] makeKeyAndOrderFront: nil];
94}
95
96- (void)setStatus:(NSString *)aString;
97{
98 // NSLog(@"%@", alarm);
99 if (aString != status) {
100 [status release]; status = nil;
101 status = [aString retain];
102 [timeSummary setStringValue: status];
103 }
104}
105
106- (id)objectValueForTextField:(NSTextField *)field whileEditing:(id)sender;
107{
108 if (sender == field) {
109 NSString *stringValue = [[[self window] fieldEditor: NO forObject: field] string];
110 id obj = nil;
111 [[field formatter] getObjectValue: &obj forString: stringValue errorDescription: NULL];
112 // NSLog(@"from field editor: %@", obj);
113 return obj;
114 } else {
115 // NSLog(@"from field: %@", [field objectValue]);
116 return [field objectValue];
117 }
118}
119
120- (void)setAlarmDateAndInterval:(id)sender;
121{
122 if (isInterval) {
123 [alarm setInterval:
124 [[self objectValueForTextField: timeInterval whileEditing: sender] intValue] *
125 [timeIntervalUnits selectedTag]];
126 } else {
127 [alarm setForDate: [self objectValueForTextField: timeDate whileEditing: sender]
128 atTime: [self objectValueForTextField: timeOfDay whileEditing: sender]];
129 }
130}
131
132- (void)_stopUpdateTimer;
133{
134 [updateTimer invalidate]; [updateTimer release]; updateTimer = nil;
135}
136
137// XXX use OACalendar?
138
139- (IBAction)updateDateDisplay:(id)sender;
140{
141 // NSLog(@"updateDateDisplay: %@", sender);
142 if ([alarm isValid]) {
143 [self setStatus: [NSString stringWithFormat: @"Alarm will be set for %@ on %@", [alarm timeString], [alarm dateString]]];
144 [setButton setEnabled: YES];
145 if (updateTimer == nil || ![updateTimer isValid]) {
146 // 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.
147 // NSLog(@"setting timer");
148 if (isInterval) {
149 updateTimer = [NSTimer scheduledTimerWithTimeInterval: 1 target: self selector: @selector(updateDateDisplay:) userInfo: nil repeats: YES];
150 } else {
151 updateTimer = [NSTimer scheduledTimerWithTimeInterval: [alarm interval] target: self selector: @selector(updateDateDisplay:) userInfo: nil repeats: NO];
152 }
153 [updateTimer retain];
154 }
155 } else {
156 [setButton setEnabled: NO];
157 [self setStatus: [alarm invalidMessage]];
158 [self _stopUpdateTimer];
159 }
160}
161
162// 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.
163// Note: finding out whether a given control is editing is easier. See: <http://cocoa.mamasam.com/COCOADEV/2002/03/2/28501.php>.
164
165- (IBAction)update:(id)sender;
166{
167 // NSLog(@"update: %@", sender);
168 [self setAlarmDateAndInterval: sender];
169 [self updateDateDisplay: sender];
170}
171
172- (IBAction)inAtChanged:(id)sender;
173{
174 NSButtonCell *new = [inAtMatrix selectedCell], *old;
175 isInterval = ([inAtMatrix selectedTag] == 0);
176 old = [inAtMatrix cellWithTag: isInterval];
177 NSAssert(new != old, @"in and at buttons should be distinct!");
178 [old setKeyEquivalent: [new keyEquivalent]];
179 [old setKeyEquivalentModifierMask: [new keyEquivalentModifierMask]];
180 [new setKeyEquivalent: @""];
181 [new setKeyEquivalentModifierMask: 0];
182 [timeInterval setEnabled: isInterval];
183 [timeIntervalUnits setEnabled: isInterval];
184 [timeIntervalRepeats setEnabled: isInterval];
185 [timeOfDay setEnabled: !isInterval];
186 [timeDate setEnabled: !isInterval];
187 [timeDateCompletions setEnabled: !isInterval];
188 if (sender != nil)
189 [[self window] makeFirstResponder: isInterval ? timeInterval : timeOfDay];
190 // NSLog(@"UPDATING FROM inAtChanged");
191 [self update: nil];
192}
193
194- (IBAction)playSoundChanged:(id)sender;
195{
196 BOOL playSoundSelected = [playSound intValue];
197 BOOL canRepeat = playSoundSelected ? [sound canRepeat] : NO;
198 [sound setEnabled: playSoundSelected];
199 [soundRepetitions setEnabled: canRepeat];
200 [soundRepetitionStepper setEnabled: canRepeat];
201 [soundRepetitionsLabel setTextColor: canRepeat ? [NSColor controlTextColor] : [NSColor disabledControlTextColor]];
202 if (playSoundSelected && sender != nil)
203 [[self window] makeFirstResponder: sound];
204}
205
206- (IBAction)setSoundRepetitionCount:(id)sender;
207{
208 NSTextView *fieldEditor = (NSTextView *)[soundRepetitions currentEditor];
209 BOOL isEditing = (fieldEditor != nil);
210 int newReps = [sender intValue], oldReps;
211 if (isEditing) {
212 // XXX work around bug where if you ask soundRepetitions for its intValue too often while it's editing, the field begins to flash
213 oldReps = [[[fieldEditor textStorage] string] intValue];
214 } else oldReps = [soundRepetitions intValue];
215 if (newReps != oldReps) {
216 [soundRepetitions setIntValue: newReps];
217 // NSLog(@"updating: new value %d, old value %d%@", newReps, oldReps, isEditing ? @", is editing" : @"");
218 // XXX work around 10.1 bug, otherwise field only displays every second value
219 if (isEditing) [soundRepetitions selectText: self];
220 }
221}
222
223// XXX should check the 'Do script:' button when someone drops a script on the button
224
225- (IBAction)doScriptChanged:(id)sender;
226{
227 BOOL doScriptSelected = [doScript intValue];
228 [script setEnabled: doScriptSelected];
229 [scriptSelectButton setEnabled: doScriptSelected];
230 if (doScriptSelected && sender != nil)
231 [[self window] makeFirstResponder: scriptSelectButton];
232}
233
234- (IBAction)doSpeakChanged:(id)sender;
235{
236 BOOL doSpeakSelected = [doSpeak intValue];
237 [voice setEnabled: doSpeakSelected];
238 if (doSpeakSelected && sender != nil)
239 [[self window] makeFirstResponder: voice];
240}
241
242- (IBAction)dateCompleted:(NSPopUpButton *)sender;
243{
244 [timeDate setStringValue: [sender titleOfSelectedItem]];
245 [self update: sender];
246}
247
248// to ensure proper updating of interval, this should be the only method by which the window is shown (e.g. from the Alarm menu)
249- (IBAction)showWindow:(id)sender;
250{
251 if (![[self window] isVisible]) {
252 [self update: self];
253 // 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. :(
254 [[self window] makeFirstResponder: [[self window] initialFirstResponder]];
255 }
256 [super showWindow: sender];
257}
258
259- (IBAction)setAlarm:(NSButton *)sender;
260{
261 // set alarm
262 [self setAlarmDateAndInterval: sender];
263 [alarm setMessage: [messageField stringValue]];
264 if (![alarm setTimer]) {
265 [self setStatus: [@"Unable to set alarm. " stringByAppendingString: [alarm invalidMessage]]];
266 return;
267 }
268
269 [alarm removeAlerts];
270 // dock bounce alert
271 if ([bounceDockIcon state] == NSOnState)
272 [alarm addAlert: [PSDockBounceAlert alert]];
273 // script alert
274 if ([doScript intValue]) {
275 BDAlias *scriptFileAlias = [script alias];
276 if (scriptFileAlias == nil) {
277 [self setStatus: @"Unable to set script alert (no script specified?)"];
278 return;
279 }
280 [alarm addAlert: [PSScriptAlert alertWithScriptFileAlias: scriptFileAlias]];
281 }
282 // notifier alert
283 if ([displayMessage intValue])
284 [alarm addAlert: [PSNotifierAlert alert]];
285 // sound alerts
286 if ([playSound intValue]) {
287 BDAlias *soundAlias = [sound selectedAlias];
288 unsigned short numReps = [soundRepetitions intValue];
289 if (soundAlias == nil) // beep alert
290 [alarm addAlert: [PSBeepAlert alertWithRepetitions: numReps]];
291 else // movie alert
292 [alarm addAlert: [PSMovieAlert alertWithMovieFileAlias: soundAlias repetitions: numReps]];
293 }
294 // speech alert
295 if ([doSpeak intValue])
296 [alarm addAlert: [PSSpeechAlert alertWithVoice: [voice titleOfSelectedItem]]];
297
298 [self setStatus: [[alarm date] descriptionWithCalendarFormat: @"Alarm set for %x at %X" timeZone: nil locale: nil]];
299 [[self window] close];
300 [alarm release];
301 alarm = [[PSAlarm alloc] init];
302}
303
304- (IBAction)silence:(id)sender;
305{
306 [sound stopSoundPreview: self];
307 [voice stopVoicePreview: self];
308}
309
310@end
311
312@implementation PSAlarmSetController (NSControlSubclassDelegate)
313
314- (void)control:(NSControl *)control didFailToValidatePartialString:(NSString *)string errorDescription:(NSString *)error;
315{
316 unichar c;
317 int tag;
318 unsigned length = [string length];
319 if (control != timeInterval || length == 0) return;
320 c = [string characterAtIndex: length - 1];
321 switch (c) {
322 case 's': case 'S': tag = 1; break;
323 case 'm': case 'M': tag = 60; break;
324 case 'h': case 'H': tag = 60 * 60; break;
325 default: return;
326 }
327 [timeIntervalUnits selectItemAtIndex:
328 [timeIntervalUnits indexOfItemWithTag: tag]];
329 // NSLog(@"UPDATING FROM validation");
330 [self update: timeInterval]; // make sure we still examine the field editor, otherwise if the existing numeric string is invalid, it'll be cleared
331}
332
333@end
334
335@implementation PSAlarmSetController (NSWindowNotifications)
336
337- (void)windowWillClose:(NSNotification *)notification;
338{
339 // NSLog(@"stopping update timer");
340 [self silence: nil];
341 [self _stopUpdateTimer];
342}
343
344@end
345
346@implementation PSAlarmSetController (NSControlSubclassNotifications)
347
348// called because we're the delegate
349
350- (void)controlTextDidChange:(NSNotification *)notification;
351{
352 // NSLog(@"UPDATING FROM controlTextDidChange: %@", [notification object]);
353 [self update: [notification object]];
354}
355
356@end
357
358@implementation PSAlarmSetController (NJRVoicePopUpButtonDelegate)
359
360- (NSString *)voicePopUpButton:(NJRVoicePopUpButton *)sender previewStringForVoice:(NSString *)voice;
361{
362 return [messageField stringValue];
363}
364
365@end
Note: See TracBrowser for help on using the repository browser.