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 "HgRevision.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, 2U, 0U, releaseType_development, 1U, };
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);
553 #pragma mark Dispatching notifications
555 - (void) dispatchNotificationWithDictionary:(NSDictionary *) dict {
556 NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
558 [[GrowlLog sharedController] writeNotificationDictionaryToLog:dict];
560 // Make sure this notification is actually registered
561 NSString *appName = [dict objectForKey:GROWL_APP_NAME];
562 GrowlApplicationTicket *ticket = [ticketController ticketForApplicationName:appName];
563 NSString *notificationName = [dict objectForKey:GROWL_NOTIFICATION_NAME];
564 if (!ticket || ![ticket isNotificationAllowed:notificationName]) {
565 // Either the app isn't registered or the notification is turned off
566 // We should do nothing
571 NSMutableDictionary *aDict = [dict mutableCopy];
574 Class NSImageClass = [NSImage class];
575 Class NSDataClass = [NSData class];
577 id image = [aDict objectForKey:GROWL_NOTIFICATION_ICON];
579 if ([image isKindOfClass:NSImageClass])
581 else if ([image isKindOfClass:NSDataClass])
582 icon = [[NSImage alloc] initWithData:image];
585 icon = [[ticket icon] copy];
588 [aDict setObject:icon forKey:GROWL_NOTIFICATION_ICON];
591 //We get here when no image existed, and if an NSData existed, an image could not be created from it.
592 //In the latter case, we don't need to keep that non-image NSData around.
593 [aDict removeObjectForKey:GROWL_NOTIFICATION_ICON];
596 // If app icon present, convert to NSImage
598 image = [aDict objectForKey:GROWL_NOTIFICATION_APP_ICON];
600 if ([image isKindOfClass:NSImageClass])
602 else if ([image isKindOfClass:NSDataClass])
603 icon = [[NSImage alloc] initWithData:image];
606 [aDict setObject:icon forKey:GROWL_NOTIFICATION_APP_ICON];
609 [aDict removeObjectForKey:GROWL_NOTIFICATION_APP_ICON];
611 // To avoid potential exceptions, make sure we have both text and title
612 if (![aDict objectForKey:GROWL_NOTIFICATION_DESCRIPTION])
613 [aDict setObject:@"" forKey:GROWL_NOTIFICATION_DESCRIPTION];
614 if (![aDict objectForKey:GROWL_NOTIFICATION_TITLE])
615 [aDict setObject:@"" forKey:GROWL_NOTIFICATION_TITLE];
617 //Retrieve and set the the priority of the notification
618 GrowlNotificationTicket *notification = [ticket notificationTicketForName:notificationName];
619 int priority = [notification priority];
621 if (priority == GrowlPriorityUnset) {
622 value = [dict objectForKey:GROWL_NOTIFICATION_PRIORITY];
624 value = [NSNumber numberWithInt:0];
626 value = [NSNumber numberWithInt:priority];
627 [aDict setObject:value forKey:GROWL_NOTIFICATION_PRIORITY];
629 GrowlPreferencesController *preferences = [GrowlPreferencesController sharedController];
631 // Retrieve and set the sticky bit of the notification
632 int sticky = [notification sticky];
634 setBooleanForKey(aDict, GROWL_NOTIFICATION_STICKY, sticky);
635 else if ([preferences stickyWhenAway] && !getBooleanForKey(aDict, GROWL_NOTIFICATION_STICKY))
636 setBooleanForKey(aDict, GROWL_NOTIFICATION_STICKY, GrowlIdleStatusController_isIdle());
638 BOOL saveScreenshot = [[NSUserDefaults standardUserDefaults] boolForKey:GROWL_SCREENSHOT_MODE];
639 setBooleanForKey(aDict, GROWL_SCREENSHOT_MODE, saveScreenshot);
640 setBooleanForKey(aDict, @"ClickHandlerEnabled", [ticket clickHandlersEnabled]);
642 if (![preferences squelchMode]) {
643 GrowlDisplayPlugin *display = [notification displayPlugin];
646 display = [ticket displayPlugin];
649 if (!defaultDisplayPlugin) {
650 NSString *displayPluginName = [[GrowlPreferencesController sharedController] defaultDisplayPluginName];
651 defaultDisplayPlugin = [(GrowlDisplayPlugin *)[[GrowlPluginController sharedController] displayPluginInstanceWithName:displayPluginName author:nil version:nil type:nil] retain];
652 if (!defaultDisplayPlugin) {
653 //User's selected default display has gone AWOL. Change to the default default.
654 NSString *file = [[NSBundle mainBundle] pathForResource:@"GrowlDefaults" ofType:@"plist"];
655 NSURL *fileURL = [NSURL fileURLWithPath:file];
656 NSDictionary *defaultDefaults = (NSDictionary *)createPropertyListFromURL((NSURL *)fileURL, kCFPropertyListImmutable, NULL, NULL);
657 if (defaultDefaults) {
658 displayPluginName = [defaultDefaults objectForKey:GrowlDisplayPluginKey];
659 if (!displayPluginName)
660 GrowlLog_log(@"No default display specified in default preferences! Perhaps your Growl installation is corrupted?");
662 defaultDisplayPlugin = (GrowlDisplayPlugin *)[[[GrowlPluginController sharedController] displayPluginDictionaryWithName:displayPluginName author:nil version:nil type:nil] pluginInstance];
664 //Now fix the user's preferences to forget about the missing display plug-in.
665 [preferences setObject:displayPluginName forKey:GrowlDisplayPluginKey];
668 [defaultDefaults release];
672 display = defaultDisplayPlugin;
675 GrowlApplicationNotification *appNotification = [[GrowlApplicationNotification alloc] initWithDictionary:aDict];
676 [display displayNotification:appNotification];
677 [appNotification release];
679 NSString *soundName = [notification sound];
681 NSError *error = nil;
682 NSDictionary *userInfo;
685 OSStatus err = [self getFSRef:&soundRef forSoundNamed:soundName];
687 SystemSoundActionID actionID;
688 err = SystemSoundGetActionID(&soundRef, &actionID);
690 err = SystemSoundSetCompletionRoutine(actionID, CFRunLoopGetCurrent(), /*runLoopMode*/ NULL, soundCompletionCallback, /*refcon*/ NULL);
691 SystemSoundPlay(actionID);
694 userInfo = [NSDictionary dictionaryWithObjectsAndKeys:
695 [NSString stringWithFormat:NSLocalizedString(@"Could not load and play sound file named \"%@\": %s", /*comment*/ nil), soundName, GetMacOSStatusCommentString(err)], NSLocalizedDescriptionKey,
699 userInfo = [NSDictionary dictionaryWithObjectsAndKeys:
700 [NSString stringWithFormat:NSLocalizedString(@"Could not find sound file named \"%@\": %s", /*comment*/ nil), soundName, GetMacOSStatusCommentString(err)], NSLocalizedDescriptionKey,
705 error = [NSError errorWithDomain:NSOSStatusErrorDomain code:err userInfo:userInfo];
706 [NSApp presentError:error];
711 // send to DO observers
712 [growlNotificationCenter notifyObservers:aDict];
716 // forward to remote destinations
717 if (enableForward && ![dict objectForKey:GROWL_REMOTE_ADDRESS]) {
718 if ([NSThread currentThread] == mainThread)
719 [NSThread detachNewThreadSelector:@selector(forwardNotification:)
723 [self forwardNotification:dict];
729 - (BOOL) registerApplicationWithDictionary:(NSDictionary *)userInfo {
730 [[GrowlLog sharedController] writeRegistrationDictionaryToLog:userInfo];
732 NSString *appName = [userInfo objectForKey:GROWL_APP_NAME];
734 GrowlApplicationTicket *newApp = [ticketController ticketForApplicationName:appName];
737 [newApp reregisterWithDictionary:userInfo];
739 newApp = [[[GrowlApplicationTicket alloc] initWithDictionary:userInfo] autorelease];
744 if (appName && newApp) {
745 if ([newApp hasChanged])
747 [ticketController addTicket:newApp];
749 if (enableForward && ![userInfo objectForKey:GROWL_REMOTE_ADDRESS]) {
750 if ([NSThread currentThread] == mainThread)
751 [NSThread detachNewThreadSelector:@selector(forwardRegistration:)
753 withObject:userInfo];
755 [self forwardRegistration:userInfo];
757 } else { //!(appName && newApp)
758 NSString *filename = [(appName ? appName : @"unknown-application") stringByAppendingPathExtension:GROWL_REG_DICT_EXTENSION];
760 //We'll be writing the file to ~/Library/Logs/Failed Growl registrations.
761 NSFileManager *mgr = [NSFileManager defaultManager];
762 NSString *userLibraryFolder = [NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSUserDomainMask, /*expandTilde*/ YES) lastObject];
763 NSString *logsFolder = [userLibraryFolder stringByAppendingPathComponent:@"Logs"];
764 [mgr createDirectoryAtPath:logsFolder attributes:nil];
765 NSString *failedTicketsFolder = [logsFolder stringByAppendingPathComponent:@"Failed Growl registrations"];
766 [mgr createDirectoryAtPath:failedTicketsFolder attributes:nil];
767 NSString *path = [failedTicketsFolder stringByAppendingPathComponent:filename];
769 //NSFileHandle will not create the file for us, so we must create it separately.
770 [mgr createFileAtPath:path contents:nil attributes:nil];
772 NSFileHandle *fh = [NSFileHandle fileHandleForWritingAtPath:path];
773 [fh seekToEndOfFile];
774 if ([fh offsetInFile]) //we are not at the beginning of the file
775 [fh writeData:[@"\n---\n\n" dataUsingEncoding:NSUTF8StringEncoding]];
776 [fh writeData:[[[userInfo description] stringByAppendingString:@"\n"] dataUsingEncoding:NSUTF8StringEncoding]];
779 if (!appName) appName = @"with no name";
781 NSLog(@"Failed application registration for application %@; wrote failed registration dictionary %p to %@", appName, userInfo, path);
788 #pragma mark Version of Growl
790 + (NSString *) growlVersion {
791 return [[NSBundle mainBundle] objectForInfoDictionaryKey:(NSString *)kCFBundleVersionKey];
794 - (NSDictionary *) versionDictionary {
796 if (version.releaseType == releaseType_svn)
797 version.development = (u_int32_t)strtoul(HG_REVISION, /*endptr*/ NULL, 10);
799 NSNumber *major = [[NSNumber alloc] initWithUnsignedShort:version.major];
800 NSNumber *minor = [[NSNumber alloc] initWithUnsignedShort:version.minor];
801 NSNumber *incremental = [[NSNumber alloc] initWithUnsignedChar:version.incremental];
802 NSNumber *releaseType = [[NSNumber alloc] initWithUnsignedChar:version.releaseType];
803 NSNumber *development = [[NSNumber alloc] initWithUnsignedShort:version.development];
805 versionInfo = [[NSDictionary alloc] initWithObjectsAndKeys:
806 [GrowlApplicationController growlVersion], (NSString *)kCFBundleVersionKey,
808 major, @"Major version",
809 minor, @"Minor version",
810 incremental, @"Incremental version",
811 releaseTypeNames[version.releaseType], @"Release type name",
812 releaseType, @"Release type",
813 development, @"Development version",
819 [incremental release];
820 [releaseType release];
821 [development release];
826 //this method could be moved to Growl.framework, I think.
827 //pass nil to get GrowlHelperApp's version as a string.
828 - (NSString *)stringWithVersionDictionary:(NSDictionary *)d {
830 d = [self versionDictionary];
833 NSMutableString *result = [NSMutableString stringWithFormat:@"%@.%@",
834 [d objectForKey:@"Major version"],
835 [d objectForKey:@"Minor version"]];
838 NSNumber *incremental = [d objectForKey:@"Incremental version"];
839 if ([incremental unsignedShortValue])
840 [result appendFormat:@".%@", incremental];
842 NSString *releaseTypeName = [d objectForKey:@"Release type name"];
843 if ([releaseTypeName length]) {
844 //"" (release), "b4", " SVN 900"
845 [result appendFormat:@"%@%@", releaseTypeName, [d objectForKey:@"Development version"]];
851 - (NSURL *) versionCheckURL {
852 if (!versionCheckURL)
853 versionCheckURL = [[NSURL URLWithString:@"http://growl.info/version.xml"] retain];
854 return versionCheckURL;
857 #pragma mark Accessors
859 - (BOOL) quitAfterOpen {
860 return quitAfterOpen;
862 - (void) setQuitAfterOpen:(BOOL)flag {
863 quitAfterOpen = flag;
866 #pragma mark What NSThread should implement as a class method
868 - (NSThread *)mainThread {
872 #pragma mark Notifications (not the Growl kind)
874 - (void) preferencesChanged:(NSNotification *) note {
875 NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
877 //[note object] is the changed key. A nil key means reload our tickets.
878 id object = [note object];
880 if (!quitAfterOpen) {
881 if (!note || (object && [object isEqual:GrowlStartServerKey])) {
882 Class pathwayControllerClass = NSClassFromString(@"GrowlPathwayController");
883 if (pathwayControllerClass)
884 [(id)[pathwayControllerClass sharedController] setServerEnabledFromPreferences];
887 if (!note || (object && [object isEqual:GrowlUserDefaultsKey]))
888 [[GrowlPreferencesController sharedController] synchronize];
889 if (!note || (object && [object isEqual:GrowlEnabledKey]))
890 growlIsEnabled = [[GrowlPreferencesController sharedController] boolForKey:GrowlEnabledKey];
891 if (!note || (object && [object isEqual:GrowlEnableForwardKey]))
892 enableForward = [[GrowlPreferencesController sharedController] isForwardingEnabled];
893 if (!note || (object && [object isEqual:GrowlForwardDestinationsKey])) {
894 [destinations release];
895 destinations = [[[GrowlPreferencesController sharedController] objectForKey:GrowlForwardDestinationsKey] retain];
897 if (!note || !object)
898 [ticketController loadAllSavedTickets];
899 if (!note || (object && [object isEqual:GrowlDisplayPluginKey]))
901 [defaultDisplayPlugin release];
902 defaultDisplayPlugin = nil;
904 if ([object isEqual:@"GrowlTicketDeleted"]) {
905 NSString *ticketName = [[note userInfo] objectForKey:@"TicketName"];
906 [ticketController removeTicketForApplicationName:ticketName];
907 } else if ([object isEqual:@"GrowlTicketChanged"]) {
908 NSString *ticketName = [[note userInfo] objectForKey:@"TicketName"];
909 GrowlApplicationTicket *newTicket = [[GrowlApplicationTicket alloc] initTicketForApplication:ticketName];
911 [ticketController addTicket:newTicket];
914 } else if ((!quitAfterOpen) && [object isEqual:GrowlUDPPortKey]) {
915 Class pathwayControllerClass = NSClassFromString(@"GrowlPathwayController");
916 if (pathwayControllerClass) {
917 id pathwayController = [pathwayControllerClass sharedController];
918 [pathwayController setServerEnabled:NO];
919 [pathwayController setServerEnabled:YES];
927 - (void) shutdown:(NSNotification *) note {
929 [NSApp terminate:nil];
932 - (void) replyToPing:(NSNotification *) note {
934 NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
936 [[NSDistributedNotificationCenter defaultCenter] postNotificationName:GROWL_PONG
939 deliverImmediately:NO];
944 #pragma mark NSApplication Delegate Methods
946 - (BOOL) application:(NSApplication *)theApplication openFile:(NSString *)filename {
947 #pragma unused(theApplication)
949 NSString *pathExtension = [filename pathExtension];
951 if ([pathExtension isEqualToString:GROWL_REG_DICT_EXTENSION]) {
952 //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.
953 BOOL registerItOurselves = YES;
954 NSString *realHelperAppBundlePath = nil;
957 //But, just to make sure we don't infinitely loop, make sure this isn't our own bundle.
958 NSString *ourBundlePath = [[NSBundle mainBundle] bundlePath];
959 realHelperAppBundlePath = [[GrowlPathUtilities runningHelperAppBundle] bundlePath];
960 if (![ourBundlePath isEqualToString:realHelperAppBundlePath])
961 registerItOurselves = NO;
964 if (registerItOurselves) {
965 //We are the real GHA.
966 //Have the property-list-file pathway process this registration dictionary file.
967 GrowlPropertyListFilePathway *pathway = [GrowlPropertyListFilePathway standardPathway];
968 [pathway application:theApplication openFile:filename];
970 //We're definitely not the real GHA, so pass it to the real GHA to be registered.
971 [[NSWorkspace sharedWorkspace] openFile:filename
972 withApplication:realHelperAppBundlePath];
975 GrowlPluginController *controller = [GrowlPluginController sharedController];
976 //the set returned by GrowlPluginController is case-insensitive. yay!
977 if ([[controller registeredPluginTypes] containsObject:pathExtension]) {
978 [controller installPluginFromPath:filename];
984 /*If Growl is not enabled and was not already running before
985 * (for example, via an autolaunch even though the user's last
986 * preference setting was to click "Stop Growl," setting enabled to NO),
987 * quit having registered; otherwise, remain running.
989 if (!growlIsEnabled && !growlFinishedLaunching) {
990 //Terminate after one second to give us time to process any other openFile: messages.
991 [NSObject cancelPreviousPerformRequestsWithTarget:NSApp
992 selector:@selector(terminate:)
994 [NSApp performSelector:@selector(terminate:)
1002 - (void) applicationWillFinishLaunching:(NSNotification *)aNotification {
1003 #pragma unused(aNotification)
1004 mainThread = [[NSThread currentThread] retain];
1006 BOOL printVersionAndExit = [[NSUserDefaults standardUserDefaults] boolForKey:@"PrintVersionAndExit"];
1007 if (printVersionAndExit) {
1008 printf("This is GrowlHelperApp version %s.\n"
1009 "PrintVersionAndExit was set to %hhi, so GrowlHelperApp will now exit.\n",
1010 [[self stringWithVersionDictionary:nil] UTF8String],
1011 printVersionAndExit);
1012 [NSApp terminate:nil];
1015 NSFileManager *fs = [NSFileManager defaultManager];
1017 NSString *destDir, *subDir;
1018 NSArray *searchPath = NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSUserDomainMask, /*expandTilde*/ YES);
1020 destDir = [searchPath objectAtIndex:0U]; //first == last == ~/Library
1021 [fs createDirectoryAtPath:destDir attributes:nil];
1022 destDir = [destDir stringByAppendingPathComponent:@"Application Support"];
1023 [fs createDirectoryAtPath:destDir attributes:nil];
1024 destDir = [destDir stringByAppendingPathComponent:@"Growl"];
1025 [fs createDirectoryAtPath:destDir attributes:nil];
1027 subDir = [destDir stringByAppendingPathComponent:@"Tickets"];
1028 [fs createDirectoryAtPath:subDir attributes:nil];
1029 subDir = [destDir stringByAppendingPathComponent:@"Plugins"];
1030 [fs createDirectoryAtPath:subDir attributes:nil];
1033 //Post a notification when we are done launching so the application bridge can inform participating applications
1034 - (void) applicationDidFinishLaunching:(NSNotification *)aNotification {
1035 #pragma unused(aNotification)
1036 [[NSDistributedNotificationCenter defaultCenter] postNotificationName:GROWL_IS_READY
1039 deliverImmediately:YES];
1040 growlFinishedLaunching = YES;
1042 if (quitAfterOpen) {
1043 //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.
1044 [NSApp performSelector:@selector(terminate:)
1048 /*If Growl is not enabled and was not already running before
1049 * (for example, via an autolaunch even though the user's last
1050 * preference setting was to click "Stop Growl," setting enabled to NO),
1051 * quit having registered; otherwise, remain running.
1053 if (!growlIsEnabled)
1054 [NSApp terminate:nil];
1058 //Same as applicationDidFinishLaunching, called when we are asked to reopen (that is, we are already running)
1059 - (BOOL) applicationShouldHandleReopen:(NSApplication *)theApplication hasVisibleWindows:(BOOL)flag {
1060 #pragma unused(theApplication, flag)
1064 - (BOOL) applicationShouldTerminateAfterLastWindowClosed:(NSApplication *)theApplication {
1065 #pragma unused(theApplication)
1069 - (void) applicationWillTerminate:(NSNotification *)notification {
1070 #pragma unused(notification)
1071 [GrowlAbstractSingletonObject destroyAllSingletons]; //Release all our controllers
1074 #pragma mark Auto-discovery
1076 //called by NSWorkspace when an application launches.
1077 - (void) applicationLaunched:(NSNotification *)notification {
1078 NSDictionary *userInfo = [notification userInfo];
1083 NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
1084 NSString *appPath = [userInfo objectForKey:@"NSApplicationPath"];
1087 NSString *ticketPath = [NSBundle pathForResource:@"Growl Registration Ticket" ofType:GROWL_REG_DICT_EXTENSION inDirectory:appPath];
1089 CFURLRef ticketURL = CFURLCreateWithFileSystemPath(kCFAllocatorDefault, (CFStringRef)ticketPath, kCFURLPOSIXPathStyle, false);
1090 NSMutableDictionary *ticket = (NSMutableDictionary *)createPropertyListFromURL((NSURL *)ticketURL, kCFPropertyListMutableContainers, NULL, NULL);
1093 NSString *appName = [userInfo objectForKey:@"NSApplicationName"];
1095 //set the app's name in the dictionary, if it's not present already.
1096 if (![ticket objectForKey:GROWL_APP_NAME])
1097 [ticket setObject:appName forKey:GROWL_APP_NAME];
1099 if ([GrowlApplicationTicket isValidTicketDictionary:ticket]) {
1100 NSLog(@"Auto-discovered registration ticket in %@ (located at %@)", appName, appPath);
1102 /* set the app's location in the dictionary, avoiding costly
1105 NSURL *url = [[NSURL alloc] initFileURLWithPath:appPath];
1106 NSDictionary *file_data = createDockDescriptionWithURL(url);
1107 id location = file_data ? [NSDictionary dictionaryWithObject:file_data forKey:@"file-data"] : appPath;
1108 [file_data release];
1109 [ticket setObject:location forKey:GROWL_APP_LOCATION];
1112 //write the new ticket to disk, and be sure to launch this ticket instead of the one in the app bundle.
1113 CFUUIDRef uuid = CFUUIDCreate(kCFAllocatorDefault);
1114 CFStringRef uuidString = CFUUIDCreateString(kCFAllocatorDefault, uuid);
1116 ticketPath = [[NSTemporaryDirectory() stringByAppendingPathComponent:(NSString *)uuidString] stringByAppendingPathExtension:GROWL_REG_DICT_EXTENSION];
1117 CFRelease(uuidString);
1118 [ticket writeToFile:ticketPath atomically:NO];
1120 /* open the ticket with ourselves.
1121 * we need to use LS in order to launch it with this specific
1122 * GHA, rather than some other.
1124 CFURLRef myURL = (CFURLRef)copyCurrentProcessURL();
1125 NSArray *URLsToOpen = [NSArray arrayWithObject:[NSURL fileURLWithPath:ticketPath]];
1126 struct LSLaunchURLSpec spec = {
1128 .itemURLs = (CFArrayRef)URLsToOpen,
1129 .passThruParams = NULL,
1130 .launchFlags = kLSLaunchDontAddToRecents | kLSLaunchDontSwitch | kLSLaunchAsync,
1131 .asyncRefCon = NULL,
1133 OSStatus err = LSOpenFromURLSpec(&spec, /*outLaunchedURL*/ NULL);
1135 NSLog(@"The registration ticket for %@ could not be opened (LSOpenFromURLSpec returned %li). Pathname for the ticket file: %@", appName, (long)err, ticketPath);
1137 } else if ([GrowlApplicationTicket isKnownTicketVersion:ticket]) {
1138 NSLog(@"%@ (located at %@) contains an invalid registration ticket - developer, please consult Growl developer documentation (http://growl.info/documentation/developer/)", appName, appPath);
1140 NSNumber *versionNum = [ticket objectForKey:GROWL_TICKET_VERSION];
1142 NSLog(@"%@ (located at %@) contains a ticket whose format version (%i) is unrecognised by this version (%@) of Growl", appName, appPath, [versionNum intValue], [self stringWithVersionDictionary:nil]);
1144 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);
1148 CFRelease(ticketURL);
1155 #pragma mark Growl Application Bridge delegate
1157 * @brief Returns the application name Growl will use
1159 - (NSString *)applicationNameForGrowl
1164 - (NSDictionary *)registrationDictionaryForGrowl
1166 NSDictionary *descriptions = [NSDictionary dictionaryWithObjectsAndKeys:
1167 NSLocalizedString(@"A Growl update is available", nil), UPDATE_AVAILABLE_NOTIFICATION,
1168 NSLocalizedString(@"You are now considered idle by Growl", nil), USER_WENT_IDLE_NOTIFICATION,
1169 NSLocalizedString(@"You are no longer considered idle by Growl", nil), USER_RETURNED_NOTIFICATION,
1172 NSDictionary *humanReadableNames = [NSDictionary dictionaryWithObjectsAndKeys:
1173 NSLocalizedString(@"Growl update available", nil), UPDATE_AVAILABLE_NOTIFICATION,
1174 NSLocalizedString(@"User went idle", nil), USER_WENT_IDLE_NOTIFICATION,
1175 NSLocalizedString(@"User returned", nil), USER_RETURNED_NOTIFICATION,
1178 NSDictionary *growlReg = [NSDictionary dictionaryWithObjectsAndKeys:
1179 [NSArray arrayWithObjects:UPDATE_AVAILABLE_NOTIFICATION, USER_WENT_IDLE_NOTIFICATION, USER_RETURNED_NOTIFICATION, nil], GROWL_NOTIFICATIONS_ALL,
1180 [NSArray arrayWithObject:UPDATE_AVAILABLE_NOTIFICATION], GROWL_NOTIFICATIONS_DEFAULT,
1181 humanReadableNames, GROWL_NOTIFICATIONS_HUMAN_READABLE_NAMES,
1182 descriptions, GROWL_NOTIFICATIONS_DESCRIPTIONS,
1188 - (NSImage *)applicationIconDataForGrowl
1190 return [NSImage imageNamed:@"growl-icon"];
1193 - (void)growlNotificationWasClicked:(id)clickContext
1195 if (clickContext && [clickContext isKindOfClass:[NSString class]]) {
1196 NSURL *downloadURL = [NSURL URLWithString:clickContext];
1197 [[NSWorkspace sharedWorkspace] openURL:downloadURL];
1205 @implementation GrowlApplicationController (PRIVATE)
1207 #pragma mark Click feedback from displays
1209 /*click feedback comes here first. GAB picks up the DN and calls our
1210 * -growlNotificationWasClicked:/-growlNotificationTimedOut: with it if it's a
1214 - (void) notificationClicked:(NSNotification *)notification {
1215 NSString *appName, *growlNotificationClickedName;
1217 NSDictionary *clickInfo;
1218 NSDictionary *userInfo;
1220 userInfo = [notification userInfo];
1222 //Build the application-specific notification name
1223 appName = [notification object];
1224 if (getBooleanForKey(userInfo, @"ClickHandlerEnabled")) {
1225 suffix = GROWL_NOTIFICATION_CLICKED;
1228 * send GROWL_NOTIFICATION_TIMED_OUT instead, so that an application is
1229 * guaranteed to receive feedback for every notification.
1231 suffix = GROWL_NOTIFICATION_TIMED_OUT;
1233 NSNumber *pid = [userInfo objectForKey:GROWL_APP_PID];
1235 growlNotificationClickedName = [[NSString alloc] initWithFormat:@"%@-%@-%@",
1236 appName, pid, suffix];
1238 growlNotificationClickedName = [[NSString alloc] initWithFormat:@"%@%@",
1240 clickInfo = [[NSDictionary alloc] initWithObjectsAndKeys:
1241 [userInfo objectForKey:GROWL_KEY_CLICKED_CONTEXT], GROWL_KEY_CLICKED_CONTEXT,
1244 [[NSDistributedNotificationCenter defaultCenter] postNotificationName:growlNotificationClickedName
1247 deliverImmediately:YES];
1249 [clickInfo release];
1250 [growlNotificationClickedName release];
1253 - (void) notificationTimedOut:(NSNotification *)notification {
1254 NSString *appName, *growlNotificationTimedOutName;
1255 NSDictionary *clickInfo;
1256 NSDictionary *userInfo;
1258 userInfo = [notification userInfo];
1260 //Build the application-specific notification name
1261 appName = [notification object];
1262 NSNumber *pid = [userInfo objectForKey:GROWL_APP_PID];
1264 growlNotificationTimedOutName = [[NSString alloc] initWithFormat:@"%@-%@-%@",
1265 appName, pid, GROWL_NOTIFICATION_TIMED_OUT];
1267 growlNotificationTimedOutName = [[NSString alloc] initWithFormat:@"%@%@",
1268 appName, GROWL_NOTIFICATION_TIMED_OUT];
1269 clickInfo = [[NSDictionary alloc] initWithObjectsAndKeys:
1270 [userInfo objectForKey:GROWL_KEY_CLICKED_CONTEXT], GROWL_KEY_CLICKED_CONTEXT,
1273 [[NSDistributedNotificationCenter defaultCenter] postNotificationName:growlNotificationTimedOutName
1276 deliverImmediately:YES];
1278 [clickInfo release];
1279 [growlNotificationTimedOutName release];
1284 static OSStatus soundCompletionCallbackProc(SystemSoundActionID actionID, void *refcon)
1286 #pragma unused(refcon)
1288 SystemSoundRemoveCompletionRoutine(actionID);
1290 return SystemSoundRemoveActionID(actionID);