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

Last change on this file since 621 was 621, checked in by Nicholas Riley, 9 years ago

Date autocomplete (rough).

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