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

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

Remove ellipsis from "All Alarms" menu item in the Dock as well.

File size: 11.2 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#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
Note: See TracBrowser for help on using the repository browser.