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

Last change on this file since 622 was 622, checked in by Nicholas Riley, 14 years ago

Fix up date completion with field editor.

Still to do:

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