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 | #import "NSWindowCollectionBehavior.h"
|
---|
21 |
|
---|
22 | @interface PSApplication (Private)
|
---|
23 | - (void)_updateDockTile:(PSTimer *)timer;
|
---|
24 | @end
|
---|
25 |
|
---|
26 | @implementation PSApplication
|
---|
27 |
|
---|
28 | - (void)finishLaunching;
|
---|
29 | {
|
---|
30 | appIconImage = [[NSImage imageNamed: @"NSApplicationIcon"] retain];
|
---|
31 | [[NSNotificationCenter defaultCenter] addObserver: [PSAlarmAlertController class] selector: @selector(controllerWithTimerExpiredNotification:) name: PSAlarmTimerExpiredNotification object: nil];
|
---|
32 | [[NSNotificationCenter defaultCenter] addObserver: self selector: @selector(nextAlarmDidChange:) name: PSAlarmsNextAlarmDidChangeNotification object: nil];
|
---|
33 | // XXX exception handling
|
---|
34 | [PSAlarms setUp];
|
---|
35 | [self setDelegate: self];
|
---|
36 | [PSPreferencesController readPreferences];
|
---|
37 | [super finishLaunching];
|
---|
38 | }
|
---|
39 |
|
---|
40 | #pragma mark actions
|
---|
41 |
|
---|
42 | - (IBAction)showHelp:(id)sender;
|
---|
43 | {
|
---|
44 | [NJRReadMeController readMeControllerWithRTFDocument: [[NSBundle mainBundle] pathForResource: @"Read Me" ofType: @"rtfd"]];
|
---|
45 | }
|
---|
46 |
|
---|
47 | - (IBAction)stopAlerts:(id)sender;
|
---|
48 | {
|
---|
49 | [PSAlarmAlertController stopAlerts: sender];
|
---|
50 | }
|
---|
51 |
|
---|
52 | - (IBAction)orderFrontSetAlarmPanel:(id)sender;
|
---|
53 | {
|
---|
54 | NSWindow *window = [alarmSetController window];
|
---|
55 | if ([window respondsToSelector: @selector(setCollectionBehavior:)]) { // 10.5-only
|
---|
56 | // XXX bug workaround - NSWindowCollectionBehaviorMoveToActiveSpace is what we want, but it doesn't work correctly, probably because we have a "chicken and egg" problem as the panel isn't visible when the app is hidden
|
---|
57 | [window setCollectionBehavior: NSWindowCollectionBehaviorCanJoinAllSpaces];
|
---|
58 | [alarmSetController showWindow: self];
|
---|
59 | [window performSelector: @selector(setCollectionBehavior:) withObject:
|
---|
60 | (id)NSWindowCollectionBehaviorDefault afterDelay: 0];
|
---|
61 | [NSApp activateIgnoringOtherApps: YES]; // XXX causes title bar to flash
|
---|
62 | return;
|
---|
63 | }
|
---|
64 | [NSApp activateIgnoringOtherApps: YES];
|
---|
65 | [alarmSetController showWindow: self];
|
---|
66 | }
|
---|
67 |
|
---|
68 | - (IBAction)orderFrontAlarmsPanel:(id)sender;
|
---|
69 | {
|
---|
70 | [NSApp activateIgnoringOtherApps: YES];
|
---|
71 | if (alarmsController == nil) {
|
---|
72 | alarmsController = [[PSAlarmsController alloc] init];
|
---|
73 | }
|
---|
74 | [alarmsController showWindow: self];
|
---|
75 | }
|
---|
76 |
|
---|
77 | - (IBAction)orderFrontPreferencesPanel:(id)sender;
|
---|
78 | {
|
---|
79 | if (!preferencesController) {
|
---|
80 | preferencesController = [[PSPreferencesController alloc] init];
|
---|
81 | }
|
---|
82 | [preferencesController showWindow: self];
|
---|
83 | }
|
---|
84 |
|
---|
85 | #pragma mark Spaces interaction
|
---|
86 |
|
---|
87 | - (void)orderOutSetAlarmPanelIfHidden;
|
---|
88 | {
|
---|
89 | // prevent set alarm panel from "yanking" focus from an alarm notification, thereby obscuring the notification
|
---|
90 | if ([NSApp isActive])
|
---|
91 | return;
|
---|
92 |
|
---|
93 | NSWindow *window = [alarmSetController window];
|
---|
94 | if (![window isVisible])
|
---|
95 | return;
|
---|
96 |
|
---|
97 | [window orderOut: self];
|
---|
98 | }
|
---|
99 |
|
---|
100 | #pragma mark update timer
|
---|
101 |
|
---|
102 | - (void)_resetUpdateTimer;
|
---|
103 | {
|
---|
104 | if (dockUpdateTimer != nil) {
|
---|
105 | [dockUpdateTimer invalidate];
|
---|
106 | [dockUpdateTimer release];
|
---|
107 | dockUpdateInterval = 0;
|
---|
108 | dockUpdateTimer = nil;
|
---|
109 | }
|
---|
110 | }
|
---|
111 |
|
---|
112 | - (void)_setUpdateTimerForInterval:(NSTimeInterval)interval alarm:(PSAlarm *)alarm repeats:(BOOL)repeats;
|
---|
113 | {
|
---|
114 | dockUpdateTimer = [PSTimer scheduledTimerWithTimeInterval: interval target: self selector: @selector(_updateDockTile:) userInfo: alarm repeats: repeats];
|
---|
115 | [dockUpdateTimer retain];
|
---|
116 | dockUpdateInterval = interval; // because [timer timeInterval] always returns 0 once set
|
---|
117 | }
|
---|
118 |
|
---|
119 | - (void)_updateDockTile:(PSTimer *)timer;
|
---|
120 | {
|
---|
121 | PSAlarm *alarm = [timer userInfo];
|
---|
122 | NSTimeInterval timeRemaining;
|
---|
123 | NSString *tileString;
|
---|
124 | if (timer == nil) alarm = [[PSAlarms allAlarms] nextAlarm];
|
---|
125 | if (alarm == nil) return;
|
---|
126 | tileString = [alarm timeRemainingString];
|
---|
127 | timeRemaining = [alarm timeRemaining]; // want to err on the side of timeRemaining being smaller, otherwise ÇexpiredÈ can appear
|
---|
128 | {
|
---|
129 | NSMutableDictionary *atts = [NSMutableDictionary dictionary];
|
---|
130 | NSSize imageSize = [appIconImage size];
|
---|
131 | NSImage *tile = [[NSImage alloc] initWithSize: imageSize];
|
---|
132 | NSSize textSize;
|
---|
133 | NSPoint textOrigin;
|
---|
134 | NSRect frameRect;
|
---|
135 | float fontSize = 37;
|
---|
136 |
|
---|
137 | do {
|
---|
138 | fontSize -= 1;
|
---|
139 | [atts setObject: [NSFont boldSystemFontOfSize: fontSize] forKey: NSFontAttributeName];
|
---|
140 | textSize = [tileString sizeWithAttributes: atts];
|
---|
141 | } while (textSize.width > imageSize.width - 8);
|
---|
142 |
|
---|
143 | textOrigin = NSMakePoint(imageSize.width / 2 - textSize.width / 2,
|
---|
144 | imageSize.height / 2 - textSize.height / 2);
|
---|
145 | frameRect = NSInsetRect(NSMakeRect(textOrigin.x, textOrigin.y, textSize.width, textSize.height), -4, -2);
|
---|
146 |
|
---|
147 | [tile lockFocus];
|
---|
148 | // draw the grayed-out app icon
|
---|
149 | [appIconImage dissolveToPoint: NSZeroPoint fraction: 0.5f];
|
---|
150 | // draw the frame
|
---|
151 | [[NSColor colorWithCalibratedWhite: 0.1f alpha: 0.5f] set];
|
---|
152 | NSRectFill(frameRect);
|
---|
153 | // draw a gray two-pixel text shadow
|
---|
154 | [atts setObject: [NSColor grayColor] forKey: NSForegroundColorAttributeName];
|
---|
155 | textOrigin.x++; textOrigin.y--;
|
---|
156 | [tileString drawAtPoint: textOrigin withAttributes: atts];
|
---|
157 | textOrigin.x++; textOrigin.y--;
|
---|
158 | [tileString drawAtPoint: textOrigin withAttributes: atts];
|
---|
159 | // draw white text
|
---|
160 | textOrigin.x -= 2; textOrigin.y += 2;
|
---|
161 | [atts setObject: [NSColor whiteColor] forKey: NSForegroundColorAttributeName];
|
---|
162 | [tileString drawAtPoint: textOrigin withAttributes: atts];
|
---|
163 |
|
---|
164 | [tile unlockFocus];
|
---|
165 | [NSApp setApplicationIconImage: tile];
|
---|
166 | [tile release];
|
---|
167 | }
|
---|
168 | // NSLog(@"_updateDockTile > time remaining %@ (%.6lf), last time interval %.6lf", tileString, timeRemaining, dockUpdateInterval);
|
---|
169 | if (timeRemaining > 61) {
|
---|
170 | NSTimeInterval nextUpdate = ((unsigned long long)timeRemaining) % 60;
|
---|
171 | if (nextUpdate <= 1) nextUpdate = 60;
|
---|
172 | [self _resetUpdateTimer];
|
---|
173 | [self _setUpdateTimerForInterval: nextUpdate alarm: alarm repeats: NO];
|
---|
174 | // NSLog(@"_updateDockTile > set timer for %.0lf seconds", nextUpdate);
|
---|
175 | } else if (timer == nil || dockUpdateInterval > 1) {
|
---|
176 | [self _resetUpdateTimer];
|
---|
177 | [self _setUpdateTimerForInterval: 1 alarm: alarm repeats: YES];
|
---|
178 | // NSLog(@"_updateDockTile > set timer for 1 second");
|
---|
179 | } else if (timeRemaining <= 1) {
|
---|
180 | [self _resetUpdateTimer];
|
---|
181 | }
|
---|
182 | }
|
---|
183 |
|
---|
184 | - (void)nextAlarmDidChange:(NSNotification *)notification;
|
---|
185 | {
|
---|
186 | PSAlarm *nextAlarm = [notification object];
|
---|
187 | // NSLog(@"nextAlarmDidChange: %@", nextAlarm);
|
---|
188 | [self _resetUpdateTimer];
|
---|
189 | if (nextAlarm == nil) {
|
---|
190 | [NSApp setApplicationIconImage: appIconImage];
|
---|
191 | } else {
|
---|
192 | [self _updateDockTile: nil];
|
---|
193 | }
|
---|
194 | }
|
---|
195 |
|
---|
196 | @end
|
---|
197 |
|
---|
198 | @implementation PSApplication (NSApplicationDelegate)
|
---|
199 |
|
---|
200 | - (BOOL)applicationShouldHandleReopen:(NSApplication *)sender hasVisibleWindows:(BOOL)flag;
|
---|
201 | {
|
---|
202 | // XXX sometimes alarmsExpiring is NO (?), and we display the alarm set controller on top of an expiring alarm, try to reproduce
|
---|
203 | if (!flag && ![[PSAlarms allAlarms] alarmsExpiring] && [NSApp modalWindow] == nil)
|
---|
204 | [alarmSetController showWindow: self];
|
---|
205 | return YES;
|
---|
206 | }
|
---|
207 |
|
---|
208 | - (NSMenu *)applicationDockMenu:(NSApplication *)sender;
|
---|
209 | {
|
---|
210 | NSMenu *dockMenu = [[NSMenu alloc] initWithTitle: @""];
|
---|
211 | PSAlarms *alarms = [PSAlarms allAlarms];
|
---|
212 | PSAlarm *nextAlarm = [alarms nextAlarm];
|
---|
213 | NSMenuItem *item;
|
---|
214 | if (nextAlarm == nil) {
|
---|
215 | [dockMenu addItemWithTitle: @"No Pending Alarms" action: nil keyEquivalent: @""];
|
---|
216 | } else {
|
---|
217 | [dockMenu addItemWithTitle: @"Next Alarm" action: nil keyEquivalent: @""];
|
---|
218 | [dockMenu addItemWithTitle: [NSString stringWithFormat: @" %@", [nextAlarm message]] action: nil keyEquivalent: @""];
|
---|
219 | [dockMenu addItemWithTitle: [NSString stringWithFormat: @" %@ %@", [nextAlarm shortDateString], [nextAlarm timeString]] action: nil keyEquivalent: @""];
|
---|
220 | [dockMenu addItemWithTitle: [NSString stringWithFormat: @" Remaining: %@", [nextAlarm timeRemainingString]] action: nil keyEquivalent: @""];
|
---|
221 | }
|
---|
222 | [dockMenu addItem: [NSMenuItem separatorItem]];
|
---|
223 | item = [dockMenu addItemWithTitle: NSLocalizedString(@"Set Alarm...", "Dock menu item") action: @selector(orderFrontSetAlarmPanel:) keyEquivalent: @""];
|
---|
224 | [item setTarget: self];
|
---|
225 | item = [dockMenu addItemWithTitle: [NSString stringWithFormat: NSLocalizedString(@"All Alarms (%d)", "Dock menu item (%d replaced by number of alarms)"), [alarms alarmCount]] action: @selector(orderFrontAlarmsPanel:) keyEquivalent: @""];
|
---|
226 | [item setTarget: self];
|
---|
227 | return [dockMenu autorelease];
|
---|
228 | }
|
---|
229 |
|
---|
230 | @end
|
---|
231 |
|
---|
232 | @implementation PSApplication (NSApplicationNotifications)
|
---|
233 |
|
---|
234 | - (void)applicationDidFinishLaunching:(NSNotification *)notification;
|
---|
235 | {
|
---|
236 | // XXX import panel will not be frontmost window if you switch to another app while Pester is launching; Mac OS X bug?
|
---|
237 | PSAlarms *allAlarms = [PSAlarms allAlarms];
|
---|
238 | unsigned version1AlarmCount = [allAlarms countOfVersion1Alarms];
|
---|
239 | if (version1AlarmCount > 0) {
|
---|
240 | 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.",
|
---|
241 | @"Import", @"Discard", NSLocalizedString(@"Don't Import", "Pester <= 1.1a3 format alarms button"),
|
---|
242 | version1AlarmCount, version1AlarmCount == 1 ? @"" : @"s");
|
---|
243 | switch (answer) {
|
---|
244 | case NSAlertDefaultReturn:
|
---|
245 | @try {
|
---|
246 | [allAlarms importVersion1Alarms];
|
---|
247 | } @catch (NSException *exception) {
|
---|
248 | 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]);
|
---|
249 | return;
|
---|
250 | }
|
---|
251 | case NSAlertAlternateReturn:
|
---|
252 | [allAlarms discardVersion1Alarms];
|
---|
253 | break;
|
---|
254 | case NSAlertOtherReturn:
|
---|
255 | break;
|
---|
256 | }
|
---|
257 | }
|
---|
258 | }
|
---|
259 |
|
---|
260 | - (void)applicationWillTerminate:(NSNotification *)notification;
|
---|
261 | {
|
---|
262 | [NJRSoundManager restoreSavedDefaultOutputVolume];
|
---|
263 | [NSApp setApplicationIconImage: appIconImage];
|
---|
264 | }
|
---|
265 |
|
---|
266 | // calendar window (running in modal session) will appear even when app is in background; shouldn't
|
---|
267 | - (void)applicationWillBecomeActive:(NSNotification *)notification;
|
---|
268 | {
|
---|
269 | NSWindow *modalWindow = [NSApp modalWindow];
|
---|
270 | if (modalWindow != nil) [modalWindow makeKeyAndOrderFront: nil];
|
---|
271 | }
|
---|
272 |
|
---|
273 | - (void)applicationWillResignActive:(NSNotification *)notification;
|
---|
274 | {
|
---|
275 | NSWindow *modalWindow = [NSApp modalWindow];
|
---|
276 | if (modalWindow != nil) [modalWindow orderOut: nil];
|
---|
277 | }
|
---|
278 |
|
---|
279 | @end
|
---|