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

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

Alarms.nib: Removed horizontal scroll bar. Turned on grid. Set delegate to NJRTableDelegate instead of PSAlarmSetController.

NJRTableDelegate: In general, made functional (was previously unused). Fixed MyCompanyName. Changed ORDER_BY_CONTEXT to use key-value coding instead of assuming data consists of a dictionary of dictionaries. Added sorting support (reorderedData, replaces oData) with autosave support for sort context. Added _positionTypeSelectDisplay, which adjusts position and justification of type select display control based on the current sort column. Added support for reverse sorting in type select string. Use table data source instead of sorted data so text matches as displayed (this will break with non-text cells...).

NJRTableView: Adapted from iTableView (Jaguar table alternate table background color), TableTester (most everything else) and NJROutlineView (keyDown, moveToBeginning/EndOfDocument). Support for type selection, delete shortcut for row deletion, and iTunes-alike background colors and frame.

NSCharacterSet-NJRExtensions: Moved _typeSelectSet from NJROutlineView as it's now shared with NJRTableView. Still need to factor NJROutlineView as embedded in HostLauncher some day.

PSAlarm: Reorganized, renamed and categorized methods. Added time accessor for the benefit of sorting. Renamed compare: to compareDate: for clarity. Added compareMessage:, though it's currently unused. Renamed cancel to cancelTimer for clarity.

PSAlarmSetController: More fun with initial first responder on window show/hide; still need to work around bug properly (subclass NSComboBox?) and fix it for real. As is, works for OS X 10.1.

PSAlarms: Added alarms accessor, returning alarm array. Fixed memory leak on successful alarm removal (oops). Added removeAlarms:, needed with sorted alarm list.

PSAlarmsController: Set window resize increment. Changes to table delegate methods to use reordered alarm list. Register for NSTableViewSelectionDidChangeNotification now we're no longer the table view delegate. Fixed autoselection in alarmsChanged by using data reordering support in NJRTableView. Implement NJRTableViewDataSource to permit deletion from table view.

Pester.pbproj: Added new files.

Read Me.rtfd: Added TableTester/iTableView acknowledgements. Updated release notes.

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