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

Last change on this file since 580 was 522, checked in by Nicholas Riley, 16 years ago

Spaces fixes.

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