Fixed what was likely one of the major causes of Growl-to-Growl local network forwarding failing. Previously, when forwarding was configured, the destination address and port were stored as a preference and used forever-after. That's wrong when destinations aren't on static IPs, which is by far the most common situation.
We now do a proper resolve to get the IP and port of the advertised Growl TCP service.
(Note: This doesn't update the preference code to not store this information; the address information just won't be used. The network preferences pane could use a lot of work under the hood, as it's clearly not complete.)
2 // GrowlApplicationController.m
5 // Created by Karl Adam on Thu Apr 22 2004.
6 // Renamed from GrowlController by Mac-arena the Bored Zo on 2005-06-28.
7 // Copyright 2004-2006 The Growl Project. All rights reserved.
9 // This file is under the BSD License, refer to License.txt for details
11 #import "GrowlApplicationController.h"
12 #import "GrowlPreferencesController.h"
13 #import "GrowlApplicationTicket.h"
14 #import "GrowlApplicationNotification.h"
15 #import "GrowlTicketController.h"
16 #import "GrowlNotificationTicket.h"
17 #import "GrowlPathway.h"
18 #import "GrowlPathwayController.h"
19 #import "GrowlPropertyListFilePathway.h"
20 #import "GrowlPathUtilities.h"
21 #import "NSStringAdditions.h"
22 #import "GrowlDisplayPlugin.h"
23 #import "GrowlPluginController.h"
24 #import "GrowlIdleStatusController.h"
25 #import "GrowlDefines.h"
26 #import "GrowlVersionUtilities.h"
27 #import "SVNRevision.h"
29 #import "GrowlNotificationCenter.h"
30 #import "MD5Authenticator.h"
31 #include "CFGrowlAdditions.h"
32 #include "CFURLAdditions.h"
33 #include "CFDictionaryAdditions.h"
34 #include "CFMutableDictionaryAdditions.h"
36 #include <SystemConfiguration/SystemConfiguration.h>
37 #include <sys/errno.h>
39 #include <sys/socket.h>
40 #include <sys/fcntl.h>
41 #include <netinet/in.h>
43 // check every 24 hours
44 #define UPDATE_CHECK_INTERVAL 24.0*3600.0
46 //Notifications posted by GrowlApplicationController
47 #define UPDATE_AVAILABLE_NOTIFICATION @"Growl update available"
48 #define USER_WENT_IDLE_NOTIFICATION @"User went idle"
49 #define USER_RETURNED_NOTIFICATION @"User returned"
51 static OSStatus soundCompletionCallbackProc(SystemSoundActionID actionID, void *refcon);
53 extern CFRunLoopRef CFRunLoopGetMain(void);
55 @interface GrowlApplicationController (PRIVATE)
56 - (void) notificationClicked:(NSNotification *)notification;
57 - (void) notificationTimedOut:(NSNotification *)notification;
60 /*applications that go full-screen (games in particular) are expected to capture
61 * whatever display(s) they're using.
62 *we [will] use this to notice, and turn on auto-sticky or something (perhaps
63 * to be decided by the user), when this happens.
66 static BOOL isAnyDisplayCaptured(void) {
69 CGDisplayCount numDisplays;
70 CGDisplayErr err = CGGetActiveDisplayList(/*maxDisplays*/ 0U, /*activeDisplays*/ NULL, &numDisplays);
72 [[GrowlLog sharedController] writeToLog:@"Checking for captured displays: Could not count displays: %li", (long)err];
74 CGDirectDisplayID *displays = malloc(numDisplays * sizeof(CGDirectDisplayID));
75 CGGetActiveDisplayList(numDisplays, displays, /*numDisplays*/ NULL);
78 [[GrowlLog sharedController] writeToLog:@"Checking for captured displays: Could not allocate list of displays: %s", strerror(errno)];
80 for (CGDisplayCount i = 0U; i < numDisplays; ++i) {
81 if (CGDisplayIsCaptured(displays[i])) {
95 //static struct Version version = { 0U, 8U, 0U, releaseType_svn, 0U, };
96 #warning Having to update this struct manually is ugly. Use the info.plist.
97 #warning And once code is in to automagically update this from Info.plist, the documentation in GrowlVersionUtilities.h should also be updated.
98 static struct Version version = { 1U, 1U, 5U, releaseType_svn, 0U, };
99 //XXX - update these constants whenever the version changes
101 static void checkVersion(CFRunLoopTimerRef timer, void *context) {
102 #pragma unused(timer)
103 GrowlPreferencesController *preferences = [GrowlPreferencesController sharedController];
105 if (![preferences isBackgroundUpdateCheckEnabled])
108 GrowlApplicationController *appController = (GrowlApplicationController *)context;
109 NSURL *versionCheckURL = [appController versionCheckURL];
111 NSDictionary *productVersionDict = [[NSDictionary alloc] initWithContentsOfURL:versionCheckURL];
113 NSString *currVersionNumber = [GrowlApplicationController growlVersion];
114 NSString *latestVersionNumber = [productVersionDict objectForKey:@"Growl"];
116 NSString *downloadURLString = [productVersionDict objectForKey:@"GrowlDownloadURL"];
118 /* do nothing and be quiet if there is no active connection, if the
119 * version dictionary could not be downloaded, or if the version dictionary
120 * is missing either of these keys.
122 if (downloadURLString && latestVersionNumber) {
123 [preferences setObject:[NSDate date] forKey:LastUpdateCheckKey];
124 if (compareVersionStringsTranslating1_0To0_5(latestVersionNumber, currVersionNumber) > 0) {
125 CFStringRef title = CFCopyLocalizedString(CFSTR("Update Available"), /*comment*/ NULL);
126 CFStringRef description = CFCopyLocalizedString(CFSTR("A newer version of Growl is available online. Click here to download it now."), /*comment*/ NULL);
127 [GrowlApplicationBridge notifyWithTitle:(NSString *)title
128 description:(NSString *)description
129 notificationName:UPDATE_AVAILABLE_NOTIFICATION
130 iconData:[appController applicationIconDataForGrowl]
133 clickContext:downloadURLString
134 identifier:UPDATE_AVAILABLE_NOTIFICATION];
136 CFRelease(description);
140 [productVersionDict release];
143 @implementation GrowlApplicationController
145 + (GrowlApplicationController *) sharedController {
146 return [self sharedInstance];
149 - (id) initSingleton {
150 if ((self = [super initSingleton])) {
151 CSSM_RETURN crtn = cdsaInit();
153 NSLog(@"ERROR: Could not initialize CDSA.");
154 cssmPerror("cdsaInit", crtn);
159 // initialize GrowlPreferencesController before observing GrowlPreferencesChanged
160 GrowlPreferencesController *preferences = [GrowlPreferencesController sharedController];
162 NSDistributedNotificationCenter *NSDNC = [NSDistributedNotificationCenter defaultCenter];
164 [NSDNC addObserver:self
165 selector:@selector(preferencesChanged:)
166 name:GrowlPreferencesChanged
168 [NSDNC addObserver:self
169 selector:@selector(showPreview:)
172 [NSDNC addObserver:self
173 selector:@selector(shutdown:)
176 [NSDNC addObserver:self
177 selector:@selector(replyToPing:)
181 NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
183 selector:@selector(notificationClicked:)
184 name:GROWL_NOTIFICATION_CLICKED
187 selector:@selector(notificationTimedOut:)
188 name:GROWL_NOTIFICATION_TIMED_OUT
191 ticketController = [GrowlTicketController sharedController];
193 [GrowlApplicationBridge setGrowlDelegate:self];
195 [self versionDictionary];
197 NSString *file = [[NSBundle mainBundle] pathForResource:@"GrowlDefaults" ofType:@"plist"];
198 NSURL *fileURL = [NSURL fileURLWithPath:file];
199 NSDictionary *defaultDefaults = (NSDictionary *)createPropertyListFromURL((NSURL *)fileURL, kCFPropertyListImmutable, NULL, NULL);
200 if (defaultDefaults) {
201 [preferences registerDefaults:defaultDefaults];
202 [defaultDefaults release];
205 if ([GrowlPathUtilities runningHelperAppBundle] != [NSBundle mainBundle]) {
206 /*We are not the real GHA.
207 *We are another GHA that a pre-1.1.3 GAB has invoked to register an application by a plist file.
208 *This means that we should not start up the pathway controller; we should, instead, start up the plist-file pathway directly, and wait up to one second for -application:openFile: messages, and forward them to the plist-file pathway (as appropriate), and quit one second after the last one.
210 NSLog(@"%@", @"It appears that at least one other instance of Growl is running. This one will quit.");
213 //This class doesn't exist in the prefpane.
214 Class pathwayControllerClass = NSClassFromString(@"GrowlPathwayController");
215 if (pathwayControllerClass)
216 [pathwayControllerClass sharedController];
219 [self preferencesChanged:nil];
221 [[[NSWorkspace sharedWorkspace] notificationCenter] addObserver:self
222 selector:@selector(applicationLaunched:)
223 name:NSWorkspaceDidLaunchApplicationNotification
226 growlIcon = [[NSImage imageNamed:@"NSApplicationIcon"] retain];
228 GrowlIdleStatusController_init();
230 selector:@selector(idleStatus:)
231 name:@"GrowlIdleStatus"
234 NSDate *lastCheck = [preferences objectForKey:LastUpdateCheckKey];
235 NSDate *now = [NSDate date];
236 if (!lastCheck || [now timeIntervalSinceDate:lastCheck] > UPDATE_CHECK_INTERVAL) {
237 checkVersion(NULL, self);
240 CFRunLoopTimerContext context = {0, self, NULL, NULL, NULL};
241 updateTimer = CFRunLoopTimerCreate(kCFAllocatorDefault, [[lastCheck addTimeInterval:UPDATE_CHECK_INTERVAL] timeIntervalSinceReferenceDate], UPDATE_CHECK_INTERVAL, 0, 0, checkVersion, &context);
242 CFRunLoopAddTimer(CFRunLoopGetMain(), updateTimer, kCFRunLoopCommonModes);
244 // create and register GrowlNotificationCenter
245 growlNotificationCenter = [[GrowlNotificationCenter alloc] init];
246 growlNotificationCenterConnection = [[NSConnection alloc] initWithReceivePort:[NSPort port] sendPort:nil];
247 //[growlNotificationCenterConnection enableMultipleThreads];
248 [growlNotificationCenterConnection setRootObject:growlNotificationCenter];
249 if (![growlNotificationCenterConnection registerName:@"GrowlNotificationCenter"])
250 NSLog(@"WARNING: could not register GrowlNotificationCenter for interprocess access");
252 soundCompletionCallback = NewSystemSoundCompletionUPP(soundCompletionCallbackProc);
258 - (void) idleStatus:(NSNotification *)notification {
259 if ([[notification object] isEqualToString:@"Idle"]) {
260 GrowlPreferencesController *preferences = [GrowlPreferencesController sharedController];
262 NSNumber *value = [preferences objectForKey:@"IdleThreshold"];
263 NSString *description;
265 idleThreshold = (value ? [value intValue] : MACHINE_IDLE_THRESHOLD);
266 description = [NSString stringWithFormat:NSLocalizedString(@"No activity for more than %d seconds.", nil), idleThreshold];
267 if ([preferences stickyWhenAway])
268 description = [description stringByAppendingString:NSLocalizedString(@" New notifications will be sticky.", nil)];
270 [GrowlApplicationBridge notifyWithTitle:NSLocalizedString(@"User went idle", nil)
271 description:description
272 notificationName:USER_WENT_IDLE_NOTIFICATION
273 iconData:growlIconData
279 [GrowlApplicationBridge notifyWithTitle:NSLocalizedString(@"User returned", nil)
280 description:NSLocalizedString(@"User activity detected. New notifications will not be sticky by default.", nil)
281 notificationName:USER_RETURNED_NOTIFICATION
282 iconData:growlIconData
292 [mainThread release]; mainThread = nil;
293 Class pathwayControllerClass = NSClassFromString(@"GrowlPathwayController");
294 if (pathwayControllerClass)
295 [(id)[pathwayControllerClass sharedController] setServerEnabled:NO];
296 [destinations release]; destinations = nil;
297 [growlIcon release]; growlIcon = nil;
298 [defaultDisplayPlugin release]; defaultDisplayPlugin = nil;
300 [versionCheckURL release];
302 GrowlIdleStatusController_dealloc();
304 CFRunLoopTimerInvalidate(updateTimer);
305 CFRelease(updateTimer);
307 [[NSNotificationCenter defaultCenter] removeObserver:self name:nil object:nil];
309 [growlNotificationCenterConnection invalidate];
310 [growlNotificationCenterConnection release]; growlNotificationCenterConnection = nil;
311 [growlNotificationCenter release]; growlNotificationCenter = nil;
315 DisposeSystemSoundCompletionUPP(soundCompletionCallback);
322 - (void) showPreview:(NSNotification *) note {
323 NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
324 NSString *displayName = [note object];
325 GrowlDisplayPlugin *displayPlugin = (GrowlDisplayPlugin *)[[GrowlPluginController sharedController] displayPluginInstanceWithName:displayName author:nil version:nil type:nil];
327 NSString *desc = [[NSString alloc] initWithFormat:NSLocalizedString(@"This is a preview of the %@ display", "Preview message shown when clicking Preview in the system preferences pane. %@ becomes the name of the display style being used."), displayName];
328 NSNumber *priority = [[NSNumber alloc] initWithInt:0];
329 NSNumber *sticky = [[NSNumber alloc] initWithBool:NO];
330 NSDictionary *info = [[NSDictionary alloc] initWithObjectsAndKeys:
331 @"Growl", GROWL_APP_NAME,
332 @"Preview", GROWL_NOTIFICATION_NAME,
333 NSLocalizedString(@"Preview", "Title of the Preview notification shown to demonstrate Growl displays"), GROWL_NOTIFICATION_TITLE,
334 desc, GROWL_NOTIFICATION_DESCRIPTION,
335 priority, GROWL_NOTIFICATION_PRIORITY,
336 sticky, GROWL_NOTIFICATION_STICKY,
337 growlIcon, GROWL_NOTIFICATION_ICON,
342 GrowlApplicationNotification *notification = [[GrowlApplicationNotification alloc] initWithDictionary:info];
344 [displayPlugin displayNotification:notification];
345 [notification release];
350 * @brief Get address data for a Growl server
352 * @param name The name of the server
353 * @result An NSData which contains a (struct sockaddr *)'s data. This may actually be a sockaddr_in or a sockaddr_in6.
355 - (NSData *)addressDataForGrowlServerWithName:(NSString *)name
357 NSNetService *service = [[[NSNetService alloc] initWithDomain:@"local." type:@"_growl._tcp." name:name] autorelease];
359 /* No such service exists. The computer is probably offline. */
363 /* Work for 8 seconds to resolve the net service to an IP and port. We should be running
364 * on a thread, so blocking is fine.
366 [service scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:@"PrivateGrowlMode"];
367 [service resolveWithTimeout:8.0];
368 CFAbsoluteTime deadline = CFAbsoluteTimeGetCurrent() + 8.0;
369 CFTimeInterval remaining;
370 while ((remaining = (deadline - CFAbsoluteTimeGetCurrent())) > 0 && [[service addresses] count] == 0) {
371 CFRunLoopRunInMode((CFStringRef)@"PrivateGrowlMode", remaining, true);
375 NSArray *addresses = [service addresses];
376 if (![addresses count]) {
381 return [addresses objectAtIndex:0];
384 - (void) forwardDictionary:(NSDictionary *)dict withSelector:(SEL)forwardMethod {
385 NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
387 NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
388 NSNumber *requestTimeout = [defaults objectForKey:@"ForwardingRequestTimeout"];
389 NSNumber *replyTimeout = [defaults objectForKey:@"ForwardingReplyTimeout"];
390 NSEnumerator *enumerator = [destinations objectEnumerator];
392 while ((entry = [enumerator nextObject])) {
393 if (getBooleanForKey(entry, @"use") && getBooleanForKey(entry, @"active")) {
394 /* Note: This assumes that all forwarding destinations are within the local network.
395 * When domain names and IPs can be used, this needs to change.
397 NSData *destAddress = [self addressDataForGrowlServerWithName:[entry objectForKey:@"computer"]];
399 /* No destination address. Nothing to see here; move along. */
401 NSLog(@"Could not obtain destination address for %@", [entry objectForKey:@"computer"]);
405 NSString *password = [entry objectForKey:@"password"];
407 /* Send via DO if possible */
408 NSSocketPort *serverPort = [[NSSocketPort alloc]
409 initRemoteWithProtocolFamily:AF_INET
410 socketType:SOCK_STREAM
412 address:destAddress];
414 NSConnection *connection = [[NSConnection alloc] initWithReceivePort:nil
415 sendPort:serverPort];
416 MD5Authenticator *auth = [[MD5Authenticator alloc] initWithPassword:password];
417 [connection setDelegate:auth];
419 if (requestTimeout && [requestTimeout respondsToSelector:@selector(floatValue)])
420 [connection setRequestTimeout:[requestTimeout floatValue]];
421 if (replyTimeout && [replyTimeout respondsToSelector:@selector(floatValue)])
422 [connection setReplyTimeout:[replyTimeout floatValue]];
425 NSDistantObject *theProxy = [connection rootProxy];
426 [theProxy setProtocolForProxy:@protocol(GrowlNotificationProtocol)];
427 NSProxy <GrowlNotificationProtocol> *growlProxy = (NSProxy <GrowlNotificationProtocol> *)theProxy;
428 [growlProxy performSelector:forwardMethod withObject:dict];
429 } @catch (NSException *e) {
430 NSString *addressString = createStringWithAddressData(destAddress);
431 NSString *hostName = createHostNameForAddressData(destAddress);
432 if ([[e name] isEqualToString:NSFailedAuthenticationException]) {
433 NSLog(@"Authentication failed while forwarding to %@ (%@)",
434 addressString, hostName);
436 NSLog(@"Warning: Exception %@ while forwarding Growl registration or notification (%@) to %@ (%@). Is that system on and connected?",
437 e, NSStringFromSelector(forwardMethod), addressString, hostName);
439 [addressString release];
443 [connection invalidate];
444 [serverPort invalidate];
445 [serverPort release];
446 [connection release];
455 - (void) forwardNotification:(NSDictionary *)dict {
456 [self forwardDictionary:dict withSelector:@selector(postNotificationWithDictionary:)];
459 - (void) forwardRegistration:(NSDictionary *)dict {
460 [self forwardDictionary:dict withSelector:@selector(registerApplicationWithDictionary:)];
463 #pragma mark Retrieving sounds
465 - (OSStatus) getFSRef:(out FSRef *)outRef forSoundNamed:(NSString *)soundName {
468 NSArray *soundTypes = [NSSound soundUnfilteredFileTypes];
470 //Throw away all the HFS types, leaving only filename extensions.
471 NSPredicate *noHFSTypesPredicate = [NSPredicate predicateWithFormat:@"NOT (self BEGINSWITH \"'\")"];
472 soundTypes = [soundTypes filteredArrayUsingPredicate:noHFSTypesPredicate];
474 //If there are no types left, abort.
475 if ([soundTypes count] == 0U)
476 return unknownFormatErr;
478 //We only want the filename extensions, not the HFS types.
479 //Also, we want the longest one last so that we can use lastObject's length to allocate the buffer.
480 NSSortDescriptor *sortDesc = [[[NSSortDescriptor alloc] initWithKey:@"length" ascending:YES] autorelease];
481 NSArray *sortDescs = [NSArray arrayWithObject:sortDesc];
482 soundTypes = [soundTypes sortedArrayUsingDescriptors:sortDescs];
484 NSMutableArray *filenames = [NSMutableArray arrayWithCapacity:[soundTypes count]];
485 NSEnumerator *soundTypeEnum;
487 soundTypeEnum = [soundTypes objectEnumerator];
488 while ((soundType = [soundTypeEnum nextObject])) {
489 [filenames addObject:[soundName stringByAppendingPathExtension:soundType]];
492 NSEnumerator *filenamesEnum;
495 //The additions are for appending '.' plus the longest filename extension.
496 size_t filenameLen = [soundName length] + 1U + [[soundTypes lastObject] length];
497 unichar *filenameBuf = malloc(filenameLen * sizeof(unichar));
498 if (!filenameBuf) return memFullErr;
503 err = FSFindFolder(kUserDomain, kSystemSoundsFolderType, kDontCreateFolder, &folderRef);
505 //Folder exists. If it didn't, FSFindFolder would have returned fnfErr.
506 filenamesEnum = [filenames objectEnumerator];
507 while ((filename = [filenamesEnum nextObject])) {
508 [filename getCharacters:filenameBuf];
509 err = FSMakeFSRefUnicode(&folderRef, [filename length], filenameBuf, kTextEncodingUnknown, outRef);
518 err = FSFindFolder(kLocalDomain, kSystemSoundsFolderType, kDontCreateFolder, &folderRef);
520 //Folder exists. If it didn't, FSFindFolder would have returned fnfErr.
521 filenamesEnum = [filenames objectEnumerator];
522 while ((filename = [filenamesEnum nextObject])) {
523 [filename getCharacters:filenameBuf];
524 err = FSMakeFSRefUnicode(&folderRef, [filename length], filenameBuf, kTextEncodingUnknown, outRef);
534 err = FSFindFolder(kSystemDomain, kSystemSoundsFolderType, kDontCreateFolder, &folderRef);
536 //Folder exists. If it didn't, FSFindFolder would have returned fnfErr.
537 filenamesEnum = [filenames objectEnumerator];
538 while ((filename = [filenamesEnum nextObject])) {
539 [filename getCharacters:filenameBuf];
540 err = FSMakeFSRefUnicode(&folderRef, [filename length], filenameBuf, kTextEncodingUnknown, outRef);
554 #pragma mark Dispatching notifications
556 - (void) dispatchNotificationWithDictionary:(NSDictionary *) dict {
557 NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
559 [[GrowlLog sharedController] writeNotificationDictionaryToLog:dict];
561 // Make sure this notification is actually registered
562 NSString *appName = [dict objectForKey:GROWL_APP_NAME];
563 GrowlApplicationTicket *ticket = [ticketController ticketForApplicationName:appName];
564 NSString *notificationName = [dict objectForKey:GROWL_NOTIFICATION_NAME];
565 if (!ticket || ![ticket isNotificationAllowed:notificationName]) {
566 // Either the app isn't registered or the notification is turned off
567 // We should do nothing
572 NSMutableDictionary *aDict = [dict mutableCopy];
575 Class NSImageClass = [NSImage class];
576 Class NSDataClass = [NSData class];
578 id image = [aDict objectForKey:GROWL_NOTIFICATION_ICON];
580 if ([image isKindOfClass:NSImageClass])
582 else if ([image isKindOfClass:NSDataClass])
583 icon = [[NSImage alloc] initWithData:image];
586 icon = [[ticket icon] copy];
589 [aDict setObject:icon forKey:GROWL_NOTIFICATION_ICON];
592 //We get here when no image existed, and if an NSData existed, an image could not be created from it.
593 //In the latter case, we don't need to keep that non-image NSData around.
594 [aDict removeObjectForKey:GROWL_NOTIFICATION_ICON];
597 // If app icon present, convert to NSImage
599 image = [aDict objectForKey:GROWL_NOTIFICATION_APP_ICON];
601 if ([image isKindOfClass:NSImageClass])
603 else if ([image isKindOfClass:NSDataClass])
604 icon = [[NSImage alloc] initWithData:image];
607 [aDict setObject:icon forKey:GROWL_NOTIFICATION_APP_ICON];
610 [aDict removeObjectForKey:GROWL_NOTIFICATION_APP_ICON];
612 // To avoid potential exceptions, make sure we have both text and title
613 if (![aDict objectForKey:GROWL_NOTIFICATION_DESCRIPTION])
614 [aDict setObject:@"" forKey:GROWL_NOTIFICATION_DESCRIPTION];
615 if (![aDict objectForKey:GROWL_NOTIFICATION_TITLE])
616 [aDict setObject:@"" forKey:GROWL_NOTIFICATION_TITLE];
618 //Retrieve and set the the priority of the notification
619 GrowlNotificationTicket *notification = [ticket notificationTicketForName:notificationName];
620 int priority = [notification priority];
622 if (priority == GrowlPriorityUnset) {
623 value = [dict objectForKey:GROWL_NOTIFICATION_PRIORITY];
625 value = [NSNumber numberWithInt:0];
627 value = [NSNumber numberWithInt:priority];
628 [aDict setObject:value forKey:GROWL_NOTIFICATION_PRIORITY];
630 GrowlPreferencesController *preferences = [GrowlPreferencesController sharedController];
632 // Retrieve and set the sticky bit of the notification
633 int sticky = [notification sticky];
635 setBooleanForKey(aDict, GROWL_NOTIFICATION_STICKY, sticky);
636 else if ([preferences stickyWhenAway] && !getBooleanForKey(aDict, GROWL_NOTIFICATION_STICKY))
637 setBooleanForKey(aDict, GROWL_NOTIFICATION_STICKY, GrowlIdleStatusController_isIdle());
639 BOOL saveScreenshot = [[NSUserDefaults standardUserDefaults] boolForKey:GROWL_SCREENSHOT_MODE];
640 setBooleanForKey(aDict, GROWL_SCREENSHOT_MODE, saveScreenshot);
641 setBooleanForKey(aDict, @"ClickHandlerEnabled", [ticket clickHandlersEnabled]);
643 if (![preferences squelchMode]) {
644 GrowlDisplayPlugin *display = [notification displayPlugin];
647 display = [ticket displayPlugin];
650 if (!defaultDisplayPlugin) {
651 NSString *displayPluginName = [[GrowlPreferencesController sharedController] defaultDisplayPluginName];
652 defaultDisplayPlugin = [(GrowlDisplayPlugin *)[[GrowlPluginController sharedController] displayPluginInstanceWithName:displayPluginName author:nil version:nil type:nil] retain];
653 if (!defaultDisplayPlugin) {
654 //User's selected default display has gone AWOL. Change to the default default.
655 NSString *file = [[NSBundle mainBundle] pathForResource:@"GrowlDefaults" ofType:@"plist"];
656 NSURL *fileURL = [NSURL fileURLWithPath:file];
657 NSDictionary *defaultDefaults = (NSDictionary *)createPropertyListFromURL((NSURL *)fileURL, kCFPropertyListImmutable, NULL, NULL);
658 if (defaultDefaults) {
659 displayPluginName = [defaultDefaults objectForKey:GrowlDisplayPluginKey];
660 if (!displayPluginName)
661 GrowlLog_log(@"No default display specified in default preferences! Perhaps your Growl installation is corrupted?");
663 defaultDisplayPlugin = (GrowlDisplayPlugin *)[[[GrowlPluginController sharedController] displayPluginDictionaryWithName:displayPluginName author:nil version:nil type:nil] pluginInstance];
665 //Now fix the user's preferences to forget about the missing display plug-in.
666 [preferences setObject:displayPluginName forKey:GrowlDisplayPluginKey];
669 [defaultDefaults release];
673 display = defaultDisplayPlugin;
676 GrowlApplicationNotification *appNotification = [[GrowlApplicationNotification alloc] initWithDictionary:aDict];
677 [display displayNotification:appNotification];
678 [appNotification release];
680 NSString *soundName = [notification sound];
682 NSError *error = nil;
683 NSDictionary *userInfo;
686 OSStatus err = [self getFSRef:&soundRef forSoundNamed:soundName];
688 SystemSoundActionID actionID;
689 err = SystemSoundGetActionID(&soundRef, &actionID);
691 err = SystemSoundSetCompletionRoutine(actionID, CFRunLoopGetCurrent(), /*runLoopMode*/ NULL, soundCompletionCallback, /*refcon*/ NULL);
692 SystemSoundPlay(actionID);
695 userInfo = [NSDictionary dictionaryWithObjectsAndKeys:
696 [NSString stringWithFormat:NSLocalizedString(@"Could not load and play sound file named \"%@\": %s", /*comment*/ nil), soundName, GetMacOSStatusCommentString(err)], NSLocalizedDescriptionKey,
700 userInfo = [NSDictionary dictionaryWithObjectsAndKeys:
701 [NSString stringWithFormat:NSLocalizedString(@"Could not find sound file named \"%@\": %s", /*comment*/ nil), soundName, GetMacOSStatusCommentString(err)], NSLocalizedDescriptionKey,
706 error = [NSError errorWithDomain:NSOSStatusErrorDomain code:err userInfo:userInfo];
707 [NSApp presentError:error];
712 // send to DO observers
713 [growlNotificationCenter notifyObservers:aDict];
717 // forward to remote destinations
718 if (enableForward && ![dict objectForKey:GROWL_REMOTE_ADDRESS]) {
719 if ([NSThread currentThread] == mainThread)
720 [NSThread detachNewThreadSelector:@selector(forwardNotification:)
724 [self forwardNotification:dict];
730 - (BOOL) registerApplicationWithDictionary:(NSDictionary *)userInfo {
731 [[GrowlLog sharedController] writeRegistrationDictionaryToLog:userInfo];
733 NSString *appName = [userInfo objectForKey:GROWL_APP_NAME];
735 GrowlApplicationTicket *newApp = [ticketController ticketForApplicationName:appName];
738 [newApp reregisterWithDictionary:userInfo];
740 newApp = [[[GrowlApplicationTicket alloc] initWithDictionary:userInfo] autorelease];
745 if (appName && newApp) {
746 if ([newApp hasChanged])
748 [ticketController addTicket:newApp];
750 if (enableForward && ![userInfo objectForKey:GROWL_REMOTE_ADDRESS]) {
751 if ([NSThread currentThread] == mainThread)
752 [NSThread detachNewThreadSelector:@selector(forwardRegistration:)
754 withObject:userInfo];
756 [self forwardRegistration:userInfo];
758 } else { //!(appName && newApp)
759 NSString *filename = [(appName ? appName : @"unknown-application") stringByAppendingPathExtension:GROWL_REG_DICT_EXTENSION];
761 //We'll be writing the file to ~/Library/Logs/Failed Growl registrations.
762 NSFileManager *mgr = [NSFileManager defaultManager];
763 NSString *userLibraryFolder = [NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSUserDomainMask, /*expandTilde*/ YES) lastObject];
764 NSString *logsFolder = [userLibraryFolder stringByAppendingPathComponent:@"Logs"];
765 [mgr createDirectoryAtPath:logsFolder attributes:nil];
766 NSString *failedTicketsFolder = [logsFolder stringByAppendingPathComponent:@"Failed Growl registrations"];
767 [mgr createDirectoryAtPath:failedTicketsFolder attributes:nil];
768 NSString *path = [failedTicketsFolder stringByAppendingPathComponent:filename];
770 //NSFileHandle will not create the file for us, so we must create it separately.
771 [mgr createFileAtPath:path contents:nil attributes:nil];
773 NSFileHandle *fh = [NSFileHandle fileHandleForWritingAtPath:path];
774 [fh seekToEndOfFile];
775 if ([fh offsetInFile]) //we are not at the beginning of the file
776 [fh writeData:[@"\n---\n\n" dataUsingEncoding:NSUTF8StringEncoding]];
777 [fh writeData:[[[userInfo description] stringByAppendingString:@"\n"] dataUsingEncoding:NSUTF8StringEncoding]];
780 if (!appName) appName = @"with no name";
782 NSLog(@"Failed application registration for application %@; wrote failed registration dictionary %p to %@", appName, userInfo, path);
789 #pragma mark Version of Growl
791 + (NSString *) growlVersion {
792 return [[NSBundle mainBundle] objectForInfoDictionaryKey:(NSString *)kCFBundleVersionKey];
795 - (NSDictionary *) versionDictionary {
797 if (version.releaseType == releaseType_svn)
798 version.development = strtoul(SVN_REVISION, /*endptr*/ NULL, 10);
800 NSNumber *major = [[NSNumber alloc] initWithUnsignedShort:version.major];
801 NSNumber *minor = [[NSNumber alloc] initWithUnsignedShort:version.minor];
802 NSNumber *incremental = [[NSNumber alloc] initWithUnsignedChar:version.incremental];
803 NSNumber *releaseType = [[NSNumber alloc] initWithUnsignedChar:version.releaseType];
804 NSNumber *development = [[NSNumber alloc] initWithUnsignedShort:version.development];
806 versionInfo = [[NSDictionary alloc] initWithObjectsAndKeys:
807 [GrowlApplicationController growlVersion], (NSString *)kCFBundleVersionKey,
809 major, @"Major version",
810 minor, @"Minor version",
811 incremental, @"Incremental version",
812 releaseTypeNames[version.releaseType], @"Release type name",
813 releaseType, @"Release type",
814 development, @"Development version",
820 [incremental release];
821 [releaseType release];
822 [development release];
827 //this method could be moved to Growl.framework, I think.
828 //pass nil to get GrowlHelperApp's version as a string.
829 - (NSString *)stringWithVersionDictionary:(NSDictionary *)d {
831 d = [self versionDictionary];
834 NSMutableString *result = [NSMutableString stringWithFormat:@"%@.%@",
835 [d objectForKey:@"Major version"],
836 [d objectForKey:@"Minor version"]];
839 NSNumber *incremental = [d objectForKey:@"Incremental version"];
840 if ([incremental unsignedShortValue])
841 [result appendFormat:@".%@", incremental];
843 NSString *releaseTypeName = [d objectForKey:@"Release type name"];
844 if ([releaseTypeName length]) {
845 //"" (release), "b4", " SVN 900"
846 [result appendFormat:@"%@%@", releaseTypeName, [d objectForKey:@"Development version"]];
852 - (NSURL *) versionCheckURL {
853 if (!versionCheckURL)
854 versionCheckURL = [[NSURL URLWithString:@"http://growl.info/version.xml"] retain];
855 return versionCheckURL;
858 #pragma mark Accessors
860 - (BOOL) quitAfterOpen {
861 return quitAfterOpen;
863 - (void) setQuitAfterOpen:(BOOL)flag {
864 quitAfterOpen = flag;
867 #pragma mark What NSThread should implement as a class method
869 - (NSThread *)mainThread {
873 #pragma mark Notifications (not the Growl kind)
875 - (void) preferencesChanged:(NSNotification *) note {
876 NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
878 //[note object] is the changed key. A nil key means reload our tickets.
879 id object = [note object];
881 if (!quitAfterOpen) {
882 if (!note || (object && [object isEqual:GrowlStartServerKey])) {
883 Class pathwayControllerClass = NSClassFromString(@"GrowlPathwayController");
884 if (pathwayControllerClass)
885 [(id)[pathwayControllerClass sharedController] setServerEnabledFromPreferences];
888 if (!note || (object && [object isEqual:GrowlUserDefaultsKey]))
889 [[GrowlPreferencesController sharedController] synchronize];
890 if (!note || (object && [object isEqual:GrowlEnabledKey]))
891 growlIsEnabled = [[GrowlPreferencesController sharedController] boolForKey:GrowlEnabledKey];
892 if (!note || (object && [object isEqual:GrowlEnableForwardKey]))
893 enableForward = [[GrowlPreferencesController sharedController] isForwardingEnabled];
894 if (!note || (object && [object isEqual:GrowlForwardDestinationsKey])) {
895 [destinations release];
896 destinations = [[[GrowlPreferencesController sharedController] objectForKey:GrowlForwardDestinationsKey] retain];
898 if (!note || !object)
899 [ticketController loadAllSavedTickets];
900 if (!note || (object && [object isEqual:GrowlDisplayPluginKey]))
902 [defaultDisplayPlugin release];
903 defaultDisplayPlugin = nil;
905 if ([object isEqual:@"GrowlTicketDeleted"]) {
906 NSString *ticketName = [[note userInfo] objectForKey:@"TicketName"];
907 [ticketController removeTicketForApplicationName:ticketName];
908 } else if ([object isEqual:@"GrowlTicketChanged"]) {
909 NSString *ticketName = [[note userInfo] objectForKey:@"TicketName"];
910 GrowlApplicationTicket *newTicket = [[GrowlApplicationTicket alloc] initTicketForApplication:ticketName];
912 [ticketController addTicket:newTicket];
915 } else if ((!quitAfterOpen) && [object isEqual:GrowlUDPPortKey]) {
916 Class pathwayControllerClass = NSClassFromString(@"GrowlPathwayController");
917 if (pathwayControllerClass) {
918 id pathwayController = [pathwayControllerClass sharedController];
919 [pathwayController setServerEnabled:NO];
920 [pathwayController setServerEnabled:YES];
928 - (void) shutdown:(NSNotification *) note {
930 [NSApp terminate:nil];
933 - (void) replyToPing:(NSNotification *) note {
935 NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
937 [[NSDistributedNotificationCenter defaultCenter] postNotificationName:GROWL_PONG
940 deliverImmediately:NO];
945 #pragma mark NSApplication Delegate Methods
947 - (BOOL) application:(NSApplication *)theApplication openFile:(NSString *)filename {
948 #pragma unused(theApplication)
950 NSString *pathExtension = [filename pathExtension];
952 if ([pathExtension isEqualToString:GROWL_REG_DICT_EXTENSION]) {
953 //If the auto-quit flag is set, it's probably because we are not the real GHAÑwe're some other GHA that a broken (pre-1.1.3) GAB opened this file with. If that's the case, find the real one and open the file with it.
954 BOOL registerItOurselves = YES;
955 NSString *realHelperAppBundlePath = nil;
958 //But, just to make sure we don't infinitely loop, make sure this isn't our own bundle.
959 NSString *ourBundlePath = [[NSBundle mainBundle] bundlePath];
960 realHelperAppBundlePath = [[GrowlPathUtilities runningHelperAppBundle] bundlePath];
961 if (![ourBundlePath isEqualToString:realHelperAppBundlePath])
962 registerItOurselves = NO;
965 if (registerItOurselves) {
966 //We are the real GHA.
967 //Have the property-list-file pathway process this registration dictionary file.
968 GrowlPropertyListFilePathway *pathway = [GrowlPropertyListFilePathway standardPathway];
969 [pathway application:theApplication openFile:filename];
971 //We're definitely not the real GHA, so pass it to the real GHA to be registered.
972 [[NSWorkspace sharedWorkspace] openFile:filename
973 withApplication:realHelperAppBundlePath];
976 GrowlPluginController *controller = [GrowlPluginController sharedController];
977 //the set returned by GrowlPluginController is case-insensitive. yay!
978 if ([[controller registeredPluginTypes] containsObject:pathExtension]) {
979 [controller installPluginFromPath:filename];
985 /*If Growl is not enabled and was not already running before
986 * (for example, via an autolaunch even though the user's last
987 * preference setting was to click "Stop Growl," setting enabled to NO),
988 * quit having registered; otherwise, remain running.
990 if (!growlIsEnabled && !growlFinishedLaunching) {
991 //Terminate after one second to give us time to process any other openFile: messages.
992 [NSObject cancelPreviousPerformRequestsWithTarget:NSApp
993 selector:@selector(terminate:)
995 [NSApp performSelector:@selector(terminate:)
1003 - (void) applicationWillFinishLaunching:(NSNotification *)aNotification {
1004 #pragma unused(aNotification)
1005 mainThread = [[NSThread currentThread] retain];
1007 BOOL printVersionAndExit = [[NSUserDefaults standardUserDefaults] boolForKey:@"PrintVersionAndExit"];
1008 if (printVersionAndExit) {
1009 printf("This is GrowlHelperApp version %s.\n"
1010 "PrintVersionAndExit was set to %hhi, so GrowlHelperApp will now exit.\n",
1011 [[self stringWithVersionDictionary:nil] UTF8String],
1012 printVersionAndExit);
1013 [NSApp terminate:nil];
1016 NSFileManager *fs = [NSFileManager defaultManager];
1018 NSString *destDir, *subDir;
1019 NSArray *searchPath = NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSUserDomainMask, /*expandTilde*/ YES);
1021 destDir = [searchPath objectAtIndex:0U]; //first == last == ~/Library
1022 [fs createDirectoryAtPath:destDir attributes:nil];
1023 destDir = [destDir stringByAppendingPathComponent:@"Application Support"];
1024 [fs createDirectoryAtPath:destDir attributes:nil];
1025 destDir = [destDir stringByAppendingPathComponent:@"Growl"];
1026 [fs createDirectoryAtPath:destDir attributes:nil];
1028 subDir = [destDir stringByAppendingPathComponent:@"Tickets"];
1029 [fs createDirectoryAtPath:subDir attributes:nil];
1030 subDir = [destDir stringByAppendingPathComponent:@"Plugins"];
1031 [fs createDirectoryAtPath:subDir attributes:nil];
1034 //Post a notification when we are done launching so the application bridge can inform participating applications
1035 - (void) applicationDidFinishLaunching:(NSNotification *)aNotification {
1036 #pragma unused(aNotification)
1037 [[NSDistributedNotificationCenter defaultCenter] postNotificationName:GROWL_IS_READY
1040 deliverImmediately:YES];
1041 growlFinishedLaunching = YES;
1043 if (quitAfterOpen) {
1044 //We provide a delay of 1 second to give NSApp time to send us application:openFile: messages for any .growlRegDict files the GrowlPropertyListFilePathway needs to process.
1045 [NSApp performSelector:@selector(terminate:)
1049 /*If Growl is not enabled and was not already running before
1050 * (for example, via an autolaunch even though the user's last
1051 * preference setting was to click "Stop Growl," setting enabled to NO),
1052 * quit having registered; otherwise, remain running.
1054 if (!growlIsEnabled)
1055 [NSApp terminate:nil];
1059 //Same as applicationDidFinishLaunching, called when we are asked to reopen (that is, we are already running)
1060 - (BOOL) applicationShouldHandleReopen:(NSApplication *)theApplication hasVisibleWindows:(BOOL)flag {
1061 #pragma unused(theApplication, flag)
1065 - (BOOL) applicationShouldTerminateAfterLastWindowClosed:(NSApplication *)theApplication {
1066 #pragma unused(theApplication)
1070 - (void) applicationWillTerminate:(NSNotification *)notification {
1071 #pragma unused(notification)
1072 [GrowlAbstractSingletonObject destroyAllSingletons]; //Release all our controllers
1075 #pragma mark Auto-discovery
1077 //called by NSWorkspace when an application launches.
1078 - (void) applicationLaunched:(NSNotification *)notification {
1079 NSDictionary *userInfo = [notification userInfo];
1084 NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
1085 NSString *appPath = [userInfo objectForKey:@"NSApplicationPath"];
1088 NSString *ticketPath = [NSBundle pathForResource:@"Growl Registration Ticket" ofType:GROWL_REG_DICT_EXTENSION inDirectory:appPath];
1090 CFURLRef ticketURL = CFURLCreateWithFileSystemPath(kCFAllocatorDefault, (CFStringRef)ticketPath, kCFURLPOSIXPathStyle, false);
1091 NSMutableDictionary *ticket = (NSMutableDictionary *)createPropertyListFromURL((NSURL *)ticketURL, kCFPropertyListMutableContainers, NULL, NULL);
1094 NSString *appName = [userInfo objectForKey:@"NSApplicationName"];
1096 //set the app's name in the dictionary, if it's not present already.
1097 if (![ticket objectForKey:GROWL_APP_NAME])
1098 [ticket setObject:appName forKey:GROWL_APP_NAME];
1100 if ([GrowlApplicationTicket isValidTicketDictionary:ticket]) {
1101 NSLog(@"Auto-discovered registration ticket in %@ (located at %@)", appName, appPath);
1103 /* set the app's location in the dictionary, avoiding costly
1106 NSURL *url = [[NSURL alloc] initFileURLWithPath:appPath];
1107 NSDictionary *file_data = createDockDescriptionWithURL(url);
1108 id location = file_data ? [NSDictionary dictionaryWithObject:file_data forKey:@"file-data"] : appPath;
1109 [file_data release];
1110 [ticket setObject:location forKey:GROWL_APP_LOCATION];
1113 //write the new ticket to disk, and be sure to launch this ticket instead of the one in the app bundle.
1114 CFUUIDRef uuid = CFUUIDCreate(kCFAllocatorDefault);
1115 CFStringRef uuidString = CFUUIDCreateString(kCFAllocatorDefault, uuid);
1117 ticketPath = [[NSTemporaryDirectory() stringByAppendingPathComponent:(NSString *)uuidString] stringByAppendingPathExtension:GROWL_REG_DICT_EXTENSION];
1118 CFRelease(uuidString);
1119 [ticket writeToFile:ticketPath atomically:NO];
1121 /* open the ticket with ourselves.
1122 * we need to use LS in order to launch it with this specific
1123 * GHA, rather than some other.
1125 CFURLRef myURL = (CFURLRef)copyCurrentProcessURL();
1126 NSArray *URLsToOpen = [NSArray arrayWithObject:[NSURL fileURLWithPath:ticketPath]];
1127 struct LSLaunchURLSpec spec = {
1129 .itemURLs = (CFArrayRef)URLsToOpen,
1130 .passThruParams = NULL,
1131 .launchFlags = kLSLaunchDontAddToRecents | kLSLaunchDontSwitch | kLSLaunchAsync,
1132 .asyncRefCon = NULL,
1134 OSStatus err = LSOpenFromURLSpec(&spec, /*outLaunchedURL*/ NULL);
1136 NSLog(@"The registration ticket for %@ could not be opened (LSOpenFromURLSpec returned %li). Pathname for the ticket file: %@", appName, (long)err, ticketPath);
1138 } else if ([GrowlApplicationTicket isKnownTicketVersion:ticket]) {
1139 NSLog(@"%@ (located at %@) contains an invalid registration ticket - developer, please consult Growl developer documentation (http://growl.info/documentation/developer/)", appName, appPath);
1141 NSNumber *versionNum = [ticket objectForKey:GROWL_TICKET_VERSION];
1143 NSLog(@"%@ (located at %@) contains a ticket whose format version (%i) is unrecognised by this version (%@) of Growl", appName, appPath, [versionNum intValue], [self stringWithVersionDictionary:nil]);
1145 NSLog(@"%@ (located at %@) contains a ticket with no format version number; Growl requires that a registration dictionary include a format version number, so that Growl knows whether it will understand the dictionary's format. This ticket will be ignored.", appName, appPath);
1149 CFRelease(ticketURL);
1156 #pragma mark Growl Application Bridge delegate
1158 * @brief Returns the application name Growl will use
1160 - (NSString *)applicationNameForGrowl
1165 - (NSDictionary *)registrationDictionaryForGrowl
1167 NSDictionary *descriptions = [NSDictionary dictionaryWithObjectsAndKeys:
1168 NSLocalizedString(@"A Growl update is available", nil), UPDATE_AVAILABLE_NOTIFICATION,
1169 NSLocalizedString(@"You are now considered idle by Growl", nil), USER_WENT_IDLE_NOTIFICATION,
1170 NSLocalizedString(@"You are no longer considered idle by Growl", nil), USER_RETURNED_NOTIFICATION,
1173 NSDictionary *humanReadableNames = [NSDictionary dictionaryWithObjectsAndKeys:
1174 NSLocalizedString(@"Growl update available", nil), UPDATE_AVAILABLE_NOTIFICATION,
1175 NSLocalizedString(@"User went idle", nil), USER_WENT_IDLE_NOTIFICATION,
1176 NSLocalizedString(@"User returned", nil), USER_RETURNED_NOTIFICATION,
1179 NSDictionary *growlReg = [NSDictionary dictionaryWithObjectsAndKeys:
1180 [NSArray arrayWithObjects:UPDATE_AVAILABLE_NOTIFICATION, USER_WENT_IDLE_NOTIFICATION, USER_RETURNED_NOTIFICATION, nil], GROWL_NOTIFICATIONS_ALL,
1181 [NSArray arrayWithObject:UPDATE_AVAILABLE_NOTIFICATION], GROWL_NOTIFICATIONS_DEFAULT,
1182 humanReadableNames, GROWL_NOTIFICATIONS_HUMAN_READABLE_NAMES,
1183 descriptions, GROWL_NOTIFICATIONS_DESCRIPTIONS,
1189 - (NSImage *)applicationIconDataForGrowl
1191 return [NSImage imageNamed:@"growl-icon"];
1194 - (void)growlNotificationWasClicked:(id)clickContext
1196 if (clickContext && [clickContext isKindOfClass:[NSString class]]) {
1197 NSURL *downloadURL = [NSURL URLWithString:clickContext];
1198 [[NSWorkspace sharedWorkspace] openURL:downloadURL];
1206 @implementation GrowlApplicationController (PRIVATE)
1208 #pragma mark Click feedback from displays
1210 /*click feedback comes here first. GAB picks up the DN and calls our
1211 * -growlNotificationWasClicked:/-growlNotificationTimedOut: with it if it's a
1215 - (void) notificationClicked:(NSNotification *)notification {
1216 NSString *appName, *growlNotificationClickedName;
1218 NSDictionary *clickInfo;
1219 NSDictionary *userInfo;
1221 userInfo = [notification userInfo];
1223 //Build the application-specific notification name
1224 appName = [notification object];
1225 if (getBooleanForKey(userInfo, @"ClickHandlerEnabled")) {
1226 suffix = GROWL_NOTIFICATION_CLICKED;
1229 * send GROWL_NOTIFICATION_TIMED_OUT instead, so that an application is
1230 * guaranteed to receive feedback for every notification.
1232 suffix = GROWL_NOTIFICATION_TIMED_OUT;
1234 NSNumber *pid = [userInfo objectForKey:GROWL_APP_PID];
1236 growlNotificationClickedName = [[NSString alloc] initWithFormat:@"%@-%@-%@",
1237 appName, pid, suffix];
1239 growlNotificationClickedName = [[NSString alloc] initWithFormat:@"%@%@",
1241 clickInfo = [[NSDictionary alloc] initWithObjectsAndKeys:
1242 [userInfo objectForKey:GROWL_KEY_CLICKED_CONTEXT], GROWL_KEY_CLICKED_CONTEXT,
1245 [[NSDistributedNotificationCenter defaultCenter] postNotificationName:growlNotificationClickedName
1248 deliverImmediately:YES];
1250 [clickInfo release];
1251 [growlNotificationClickedName release];
1254 - (void) notificationTimedOut:(NSNotification *)notification {
1255 NSString *appName, *growlNotificationTimedOutName;
1256 NSDictionary *clickInfo;
1257 NSDictionary *userInfo;
1259 userInfo = [notification userInfo];
1261 //Build the application-specific notification name
1262 appName = [notification object];
1263 NSNumber *pid = [userInfo objectForKey:GROWL_APP_PID];
1265 growlNotificationTimedOutName = [[NSString alloc] initWithFormat:@"%@-%@-%@",
1266 appName, pid, GROWL_NOTIFICATION_TIMED_OUT];
1268 growlNotificationTimedOutName = [[NSString alloc] initWithFormat:@"%@%@",
1269 appName, GROWL_NOTIFICATION_TIMED_OUT];
1270 clickInfo = [[NSDictionary alloc] initWithObjectsAndKeys:
1271 [userInfo objectForKey:GROWL_KEY_CLICKED_CONTEXT], GROWL_KEY_CLICKED_CONTEXT,
1274 [[NSDistributedNotificationCenter defaultCenter] postNotificationName:growlNotificationTimedOutName
1277 deliverImmediately:YES];
1279 [clickInfo release];
1280 [growlNotificationTimedOutName release];
1285 static OSStatus soundCompletionCallbackProc(SystemSoundActionID actionID, void *refcon) {
1286 #pragma unused(refcon)
1288 SystemSoundRemoveCompletionRoutine(actionID);
1290 return SystemSoundRemoveActionID(actionID);