source: trunk/Cocoa/Pester/Source/PSApplication.m @ 364

Last change on this file since 364 was 364, checked in by Nicholas Riley, 13 years ago

English.lproj/Alarms.nib: Specify alternating row coloring in the nib,
now we're 10.4+.

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

English.lproj/Localizable.strings: Quote alarm message in pretty
description (used in tooltip). Change voice error now it no longer
incorporates OSStatus.

English.lproj/MainMenu.nib: Add speech prefs again; turn repetitions
field into a NJRValidatingField and hook up its delegate.

Info-Pester.plist: Updated for 1.1b6.

NJRHotKey.m: Switch to new Objective-C exception style.

NJRIntervalField.[hm]: Now a subclass of NJRValidatingField.

NJRTableDelegate.m: Get rid of our own tooltip support as NSTableView
now supports them (though with a minor visual glitch on the first
tooltip).

NJRTableView.[hm]: Remove tooltip support. Remove alternating row
coloring support.

NJRValidatingField.[hm]: Contains validation sheet stuff from
NJRIntervalField.

NJRVoicePopUpButton.[hm]: Switch to NSSpeechSynthesizer.

PSAlarm.m: Quote alarm message in pretty description (used in
tooltip). Fix repeating alarms not restoring as repeating if they
didn't expire while Pester was not running. No longer set timer on
Pester 1.0 alarm import, to help make importing atomic.

PSAlarmSetController.[hm]: Use NJRValidatingField for repetitions
field. Switch to new Objective-C exception style. Fix validation
issues on in/at changing. Temporary changes to restore speech support
and allow the sound popup to be removed entirely from the nib (rather
than being dragged out of the visible area, as it was in 1.1b5).
Changes for NSSpeechSynthesizer, which uses different voice names.

PSAlarms.m: Switch to new Objective-C exception style. Fix
duplication and error handling in Pester 1.0 alarm import, making
atomic.

PSAlarmsController.m: Use new tooltip support (since it's implemented
in the delegate rather than the data source, we have to proxy it).

PSAlerts.m: Wrap initialization in exception block so we don't leak.

PSApplication.m: Switch to new Objective-C exception style.

PSMediaAlert.m: Clamp repetitions at 1..99 so the user can't type an
invalid value, then quit and have it saved.

PSSpeechAlert.[hm]: Switch to NSSpeechSynthesizer. Throw an
intelligible exception if the voice is unavailable.

PSTimer.m: Switch to new Objective-C exception style.

Pester.xcodeproj: Remove VERSION generation; rename targets to be more
understandable.

Read Me.rtfd: Updated for 1.1b6.

SUSpeaker.[hm]: Gone in switch to NSSpeechSynthesizer.

VERSION: Gone - we use agvtool for everything now.

Updates/release-notes.html: Updated for 1.1b6.

Updates/updates.xml: Updated for 1.1b6.

package-Pester.sh: Use agvtool to get version. Atomically update
file on Web server to avoid partial downloads.

File size: 10.0 KB
Line 
1//
2//  PSApplication.m
3//  Pester
4//
5//  Created by Nicholas Riley on Fri Oct 11 2002.
6//  Copyright (c) 2002 Nicholas Riley. All rights reserved.
7//
8
9#import "PSApplication.h"
10#import "PSAlarmSetController.h"
11#import "PSAlarmAlertController.h"
12#import "PSAlarmsController.h"
13#import "PSPreferencesController.h"
14#import "NJRReadMeController.h"
15#import "NJRSoundManager.h"
16#import "PSAlarm.h"
17#import "PSAlarms.h"
18#import "PSTimer.h"
19#import "NJRHotKey.h"
20
21#import <Carbon/Carbon.h>
22
23@implementation PSApplication
24
25- (void)finishLaunching;
26{
27    appIconImage = [[NSImage imageNamed: @"NSApplicationIcon"] retain];
28    [[NSNotificationCenter defaultCenter] addObserver: [PSAlarmAlertController class] selector: @selector(controllerWithTimerExpiredNotification:) name: PSAlarmTimerExpiredNotification object: nil];
29    [[NSNotificationCenter defaultCenter] addObserver: self selector: @selector(nextAlarmDidChange:) name: PSAlarmsNextAlarmDidChangeNotification object: nil];
30    // XXX exception handling
31    [PSAlarms setUp];
32    [self setDelegate: self];
33    [PSPreferencesController readPreferences];
34    [super finishLaunching];
35}
36
37#pragma mark actions
38
39- (IBAction)showHelp:(id)sender;
40{
41    [NJRReadMeController readMeControllerWithRTFDocument: [[NSBundle mainBundle] pathForResource: @"Read Me" ofType: @"rtfd"]];
42}
43
44- (IBAction)stopAlerts:(id)sender;
45{
46    [PSAlarmAlertController stopAlerts: sender];
47}       
48
49- (IBAction)orderFrontSetAlarmPanel:(id)sender;
50{
51    [NSApp activateIgnoringOtherApps: YES];
52    [alarmSetController showWindow: self];
53}
54
55- (IBAction)orderFrontAlarmsPanel:(id)sender;
56{
57    [NSApp activateIgnoringOtherApps: YES];
58    if (alarmsController == nil) {
59        alarmsController = [[PSAlarmsController alloc] init];
60    }
61    [alarmsController showWindow: self];
62}
63
64- (IBAction)orderFrontPreferencesPanel:(id)sender;
65{
66    if (!preferencesController)  {
67        preferencesController = [[PSPreferencesController alloc] init];
68    }
69    [preferencesController showWindow: self];
70}
71
72#pragma mark update timer
73
74- (void)_resetUpdateTimer;
75{
76    if (dockUpdateTimer != nil) {
77        [dockUpdateTimer invalidate];
78        [dockUpdateTimer release];
79        dockUpdateInterval = 0;
80        dockUpdateTimer = nil;
81    }
82}
83
84- (void)_setUpdateTimerForInterval:(NSTimeInterval)interval alarm:(PSAlarm *)alarm repeats:(BOOL)repeats;
85{
86    dockUpdateTimer = [PSTimer scheduledTimerWithTimeInterval: interval target: self selector: @selector(_updateDockTile:) userInfo: alarm repeats: repeats];
87    [dockUpdateTimer retain];
88    dockUpdateInterval = interval; // because [timer timeInterval] always returns 0 once set
89}
90
91- (void)_updateDockTile:(PSTimer *)timer;
92{
93    PSAlarm *alarm = [timer userInfo];
94    NSTimeInterval timeRemaining;
95    NSString *tileString;
96    if (timer == nil) alarm = [[PSAlarms allAlarms] nextAlarm];
97    if (alarm == nil) return;
98    tileString = [alarm timeRemainingString];
99    timeRemaining = [alarm timeRemaining]; // want to err on the side of timeRemaining being smaller, otherwise ÇexpiredÈ can appear
100    {
101        NSMutableDictionary *atts = [NSMutableDictionary dictionary];
102        NSSize imageSize = [appIconImage size];
103        NSImage *tile = [[NSImage alloc] initWithSize: imageSize];
104        NSSize textSize;
105        NSPoint textOrigin;
106        NSRect frameRect;
107        float fontSize = 37;
108       
109        do {
110            fontSize -= 1;
111            [atts setObject: [NSFont boldSystemFontOfSize: fontSize] forKey: NSFontAttributeName];
112            textSize = [tileString sizeWithAttributes: atts];
113        } while (textSize.width > imageSize.width - 8);
114
115        textOrigin = NSMakePoint(imageSize.width / 2 - textSize.width / 2,
116                                 imageSize.height / 2 - textSize.height / 2);
117        frameRect = NSInsetRect(NSMakeRect(textOrigin.x, textOrigin.y, textSize.width, textSize.height), -4, -2);
118       
119        [tile lockFocus];
120        // draw the grayed-out app icon
121        [appIconImage dissolveToPoint: NSZeroPoint fraction: 0.5];
122        // draw the frame
123        [[NSColor colorWithCalibratedWhite: 0.1 alpha: 0.5] set];
124        NSRectFill(frameRect);
125        // draw a gray two-pixel text shadow
126        [atts setObject: [NSColor grayColor] forKey: NSForegroundColorAttributeName];
127        textOrigin.x++; textOrigin.y--;
128        [tileString drawAtPoint: textOrigin withAttributes: atts];
129        textOrigin.x++; textOrigin.y--;
130        [tileString drawAtPoint: textOrigin withAttributes: atts];
131        // draw white text
132        textOrigin.x -= 2; textOrigin.y += 2;
133        [atts setObject: [NSColor whiteColor] forKey: NSForegroundColorAttributeName];
134        [tileString drawAtPoint: textOrigin withAttributes: atts];
135       
136        [tile unlockFocus];
137        [NSApp setApplicationIconImage: tile];
138        [tile release];
139    }
140    // NSLog(@"_updateDockTile > time remaining %@ (%.6lf), last time interval %.6lf", tileString, timeRemaining, dockUpdateInterval);
141    if (timeRemaining > 61) {
142        NSTimeInterval nextUpdate = ((unsigned long long)timeRemaining) % 60;
143        if (nextUpdate <= 1) nextUpdate = 60;
144        [self _resetUpdateTimer];
145        [self _setUpdateTimerForInterval: nextUpdate alarm: alarm repeats: NO];
146        // NSLog(@"_updateDockTile > set timer for %.0lf seconds", nextUpdate);
147    } else if (timer == nil || dockUpdateInterval > 1) {
148        [self _resetUpdateTimer];
149        [self _setUpdateTimerForInterval: 1 alarm: alarm repeats: YES];
150        // NSLog(@"_updateDockTile > set timer for 1 second");
151    } else if (timeRemaining <= 1) {
152        [self _resetUpdateTimer];
153    }
154}
155
156- (void)nextAlarmDidChange:(NSNotification *)notification;
157{
158    PSAlarm *nextAlarm = [notification object];
159    // NSLog(@"nextAlarmDidChange: %@", nextAlarm);
160    [self _resetUpdateTimer];
161    if (nextAlarm == nil) {
162        [NSApp setApplicationIconImage: appIconImage];
163    } else {
164        [self _updateDockTile: nil];
165    }
166}
167
168@end
169
170@implementation PSApplication (NSApplicationDelegate)
171
172- (BOOL)applicationShouldHandleReopen:(NSApplication *)sender hasVisibleWindows:(BOOL)flag;
173{
174    // XXX sometimes alarmsExpiring is NO (?), and we display the alarm set controller on top of an expiring alarm, try to reproduce
175    if (!flag && ![[PSAlarms allAlarms] alarmsExpiring] && [NSApp modalWindow] == nil)
176        [alarmSetController showWindow: self];
177    return YES;
178}
179
180- (NSMenu *)applicationDockMenu:(NSApplication *)sender;
181{
182    NSMenu *dockMenu = [[NSMenu alloc] initWithTitle: @""];
183    PSAlarms *alarms = [PSAlarms allAlarms];
184    PSAlarm *nextAlarm = [alarms nextAlarm];
185    NSMenuItem *item;
186    if (nextAlarm == nil) {
187        [dockMenu addItemWithTitle: @"No Pending Alarms" action: nil keyEquivalent: @""];
188    } else {
189        [dockMenu addItemWithTitle: @"Next Alarm" action: nil keyEquivalent: @""];
190        [dockMenu addItemWithTitle: [NSString stringWithFormat: @"   %@", [nextAlarm message]] action: nil keyEquivalent: @""];
191        [dockMenu addItemWithTitle: [NSString stringWithFormat: @"   %@ %@", [nextAlarm shortDateString], [nextAlarm timeString]] action: nil keyEquivalent: @""];
192        [dockMenu addItemWithTitle: [NSString stringWithFormat: @"   Remaining: %@", [nextAlarm timeRemainingString]] action: nil keyEquivalent: @""];
193    }
194    [dockMenu addItem: [NSMenuItem separatorItem]];
195    item = [dockMenu addItemWithTitle: NSLocalizedString(@"Set Alarm...", "Dock menu item") action: @selector(orderFrontSetAlarmPanel:) keyEquivalent: @""];
196    [item setTarget: self];
197    item = [dockMenu addItemWithTitle: [NSString stringWithFormat: NSLocalizedString(@"All Alarms (%d)...", "Dock menu item (%d replaced by number of alarms)"), [alarms alarmCount]] action: @selector(orderFrontAlarmsPanel:) keyEquivalent: @""];
198    [item setTarget: self];
199    return [dockMenu autorelease];
200}
201
202@end
203
204@implementation PSApplication (NSApplicationNotifications)
205
206- (void)applicationDidFinishLaunching:(NSNotification *)notification;
207{
208    // XXX import panel will not be frontmost window if you switch to another app while Pester is launching; Mac OS X bug?
209    PSAlarms *allAlarms = [PSAlarms allAlarms];
210    unsigned version1AlarmCount = [allAlarms countOfVersion1Alarms];
211    if (version1AlarmCount > 0) {
212        int answer = NSRunAlertPanel(@"Import alarms from older Pester version?", @"Pester found %u alarm%@ created with an older version. These alarms must be converted for use with this version of Pester, and will be unavailable in previous versions after conversion. New alarms created with this version of Pester will not appear in Pester version 1.1a3 or earlier.",
213                                     @"Import", @"Discard", NSLocalizedString(@"Don't Import", "Pester <= 1.1a3 format alarms button"),
214                                     version1AlarmCount, version1AlarmCount == 1 ? @"" : @"s");
215        switch (answer) {
216            case NSAlertDefaultReturn:
217                @try {
218                    [allAlarms importVersion1Alarms];
219                } @catch (NSException *exception) {
220                    NSRunAlertPanel(@"Error occurred importing alarms", @"Pester was unable to convert some alarms created with an older version. Those alarms which could be read have been converted. The previous-format alarms have been retained; try using an older version of Pester to read them.\n\n%@", nil, nil, nil, [exception reason]);
221                    return;
222                }
223            case NSAlertAlternateReturn:
224                [allAlarms discardVersion1Alarms];
225                break;
226            case NSAlertOtherReturn:
227                break;
228        }
229    }
230}
231
232- (void)applicationWillTerminate:(NSNotification *)notification;
233{
234    [NJRSoundManager restoreSavedDefaultOutputVolume];
235    [NSApp setApplicationIconImage: appIconImage];
236}
237
238// calendar window (running in modal session) will appear even when app is in background; shouldn't
239- (void)applicationWillBecomeActive:(NSNotification *)notification;
240{
241    NSWindow *modalWindow = [NSApp modalWindow];
242    if (modalWindow != nil) [modalWindow makeKeyAndOrderFront: nil];
243}
244
245- (void)applicationWillResignActive:(NSNotification *)notification;
246{
247    NSWindow *modalWindow = [NSApp modalWindow];
248    if (modalWindow != nil) [modalWindow orderOut: nil];
249}
250
251@end
Note: See TracBrowser for help on using the repository browser.