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

Last change on this file since 43 was 43, checked in by Nicholas Riley, 19 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.