Core/Source/GrowlPreferencePane.m
author Peter Hosey
Tue Apr 07 11:34:52 2009 -0700 (2009-04-07)
changeset 4188 71e205ca6b20
parent 4153 899d3019a60a
child 4189 30c4b669cad3
permissions -rw-r--r--
Updated the Growl Bug Submission button to go to our new bug-reporting page (http://growl.info/reportabug.php) instead of our long-deceased Trac. Fixes #301955.
     1 //
     2 //  GrowlPreferencePane.m
     3 //  Growl
     4 //
     5 //  Created by Karl Adam on Wed Apr 21 2004.
     6 //  Copyright 2004-2006 The Growl Project. All rights reserved.
     7 //
     8 // This file is under the BSD License, refer to License.txt for details
     9 
    10 #import "GrowlPreferencePane.h"
    11 #import "GrowlPreferencesController.h"
    12 #import "GrowlDefinesInternal.h"
    13 #import "GrowlDefines.h"
    14 #import "GrowlTicketController.h"
    15 #import "GrowlApplicationTicket.h"
    16 #import "GrowlPlugin.h"
    17 #import "GrowlPluginController.h"
    18 #import "GrowlVersionUtilities.h"
    19 #import "GrowlBrowserEntry.h"
    20 #import "NSStringAdditions.h"
    21 #import "TicketsArrayController.h"
    22 #import "ACImageAndTextCell.h"
    23 #import <ApplicationServices/ApplicationServices.h>
    24 #include <SystemConfiguration/SystemConfiguration.h>
    25 #include "CFGrowlAdditions.h"
    26 #include "GrowlPositionPicker.h"
    27 
    28 #include <Carbon/Carbon.h>
    29 
    30 #define PING_TIMEOUT		3
    31 
    32 //This is the frame of the preference view that we should get back.
    33 #define DISPLAY_PREF_FRAME NSMakeRect(16.0f, 58.0f, 354.0f, 289.0f)
    34 
    35 @interface NSNetService(TigerCompatibility)
    36 
    37 - (void) resolveWithTimeout:(NSTimeInterval)timeout;
    38 
    39 @end
    40 
    41 @implementation GrowlPreferencePane
    42 
    43 - (id) initWithBundle:(NSBundle *)bundle {
    44 	//	Check that we're running Panther
    45 	//	if a user with a previous OS version tries to launch us - switch out the pane.
    46 
    47 	NSApp = [NSApplication sharedApplication];
    48 	if (![NSApp respondsToSelector:@selector(replyToOpenOrPrint:)]) {
    49 		if (NSRunInformationalAlertPanel(NSLocalizedStringFromTableInBundle(@"System requirements not met", nil, bundle, "Title for the dialogue shown if attempting to run Growl on 10.2 or earlier"),
    50 										 NSLocalizedStringFromTableInBundle(@"Mac OS X 10.3 \"Panther\" or greater is required.", nil, bundle, nil), 
    51 										 NSLocalizedStringFromTableInBundle(@"Quit", nil, bundle, "Quit button title"), 
    52 										 NSLocalizedStringFromTableInBundle(@"Upgrade Mac OS X...", nil, bundle, "Button title"), 
    53 										 nil) == NSAlertAlternateReturn) {
    54 			[[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:@"http://www.apple.com/macosx/"]];
    55 		}
    56 		[NSApp terminate:nil];
    57 	}
    58 
    59 	if ((self = [super initWithBundle:bundle])) {
    60 		pid = getpid();
    61 		loadedPrefPanes = [[NSMutableArray alloc] init];
    62 		preferencesController = [GrowlPreferencesController sharedController];
    63 
    64 		NSNotificationCenter *nc = [NSDistributedNotificationCenter defaultCenter];
    65 		[nc addObserver:self selector:@selector(growlLaunched:)   name:GROWL_IS_READY object:nil];
    66 		[nc addObserver:self selector:@selector(growlTerminated:) name:GROWL_SHUTDOWN object:nil];
    67 		[nc addObserver:self selector:@selector(reloadPrefs:)     name:GrowlPreferencesChanged object:nil];
    68 
    69 		CFStringRef file = (CFStringRef)[bundle pathForResource:@"GrowlDefaults" ofType:@"plist"];
    70 		CFURLRef fileURL = CFURLCreateWithFileSystemPath(kCFAllocatorDefault, file, kCFURLPOSIXPathStyle, /*isDirectory*/ false);
    71 		NSDictionary *defaultDefaults = (NSDictionary *)createPropertyListFromURL((NSURL *)fileURL, kCFPropertyListImmutable, NULL, NULL);
    72 		CFRelease(fileURL);
    73 		if (defaultDefaults) {
    74 			[preferencesController registerDefaults:defaultDefaults];
    75 			[defaultDefaults release];
    76 		}
    77 	}
    78 
    79 	return self;
    80 }
    81 
    82 - (void) dealloc {
    83 	[[NSDistributedNotificationCenter defaultCenter] removeObserver:self];
    84 	[browser         release];
    85 	[services        release];
    86 	[pluginPrefPane  release];
    87 	[loadedPrefPanes release];
    88 	[tickets         release];
    89 	[plugins         release];
    90 	[currentPlugin   release];
    91 	CFRelease(customHistArray);
    92 	[versionCheckURL release];
    93 	[growlWebSiteURL release];
    94 	[growlForumURL release];
    95 	[growlBugSubmissionURL release];
    96 	[growlDonateURL release];
    97 	CFRelease(images);
    98 	[super dealloc];
    99 }
   100 
   101 - (void) awakeFromNib {
   102 	ACImageAndTextCell *imageTextCell = [[[ACImageAndTextCell alloc] init] autorelease];
   103 
   104 	[ticketsArrayController addObserver:self forKeyPath:@"selection" options:0 context:nil];
   105 	[displayPluginsArrayController addObserver:self forKeyPath:@"selection" options:0 context:nil];
   106 
   107 	[self setCanRemoveTicket:NO];
   108 
   109 	browser = [[NSNetServiceBrowser alloc] init];
   110 
   111 	// create a deep mutable copy of the forward destinations
   112 	NSArray *destinations = [preferencesController objectForKey:GrowlForwardDestinationsKey];
   113 	NSEnumerator *destEnum = [destinations objectEnumerator];
   114 	NSMutableArray *theServices = [[NSMutableArray alloc] initWithCapacity:[destinations count]];
   115 	NSDictionary *destination;
   116 	while ((destination = [destEnum nextObject])) {
   117 		GrowlBrowserEntry *entry = [[GrowlBrowserEntry alloc] initWithDictionary:destination];
   118 		[entry setOwner:self];
   119 		[theServices addObject:entry];
   120 		[entry release];
   121 	}
   122 	[self setServices:theServices];
   123 	[theServices release];
   124 
   125 	[browser setDelegate:self];
   126 	[browser searchForServicesOfType:@"_growl._tcp." inDomain:@""];
   127 
   128 	[self setupAboutTab];
   129 
   130 	if ([preferencesController isGrowlMenuEnabled] && ![GrowlPreferencePane isGrowlMenuRunning])
   131 		[preferencesController enableGrowlMenu];
   132 
   133 	growlWebSiteURL = [[NSURL alloc] initWithString:@"http://growl.info"];
   134 	growlForumURL = [[NSURL alloc] initWithString:@"http://forums.cocoaforge.com/viewforum.php?f=6"];
   135 	growlBugSubmissionURL = [[NSURL alloc] initWithString:@"http://growl.info/reportabug.php"];
   136 	growlDonateURL = [[NSURL alloc] initWithString:@"http://growl.info/donate.php"];
   137 
   138 	customHistArray = CFArrayCreateMutable(kCFAllocatorDefault, 3, &kCFTypeArrayCallBacks);
   139 	id value = [preferencesController objectForKey:GrowlCustomHistKey1];
   140 	if (value) {
   141 		CFArrayAppendValue(customHistArray, value);
   142 		value = [preferencesController objectForKey:GrowlCustomHistKey2];
   143 		if (value) {
   144 			CFArrayAppendValue(customHistArray, value);
   145 			value = [preferencesController objectForKey:GrowlCustomHistKey3];
   146 			if (value)
   147 				CFArrayAppendValue(customHistArray, value);
   148 		}
   149 	}
   150 	[self updateLogPopupMenu];
   151 	int typePref = [preferencesController integerForKey:GrowlLogTypeKey];
   152 	[logFileType selectCellAtRow:typePref column:0];
   153 
   154 	[growlApplications setDoubleAction:@selector(tableViewDoubleClick:)];
   155 	[growlApplications setTarget:self];
   156 	
   157 	// bind the global position picker programmatically since its a custom view, register for notification so we can handle updating manually
   158 	[globalPositionPicker bind:@"selectedPosition" toObject:preferencesController withKeyPath:@"selectedPosition" options:nil];
   159 	[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(updatePosition:) name:GrowlPositionPickerChangedSelectionNotification object:globalPositionPicker];
   160 
   161 	// bind the app level position picker programmatically since its a custom view, register for notification so we can handle updating manually
   162 	[appPositionPicker bind:@"selectedPosition" toObject:ticketsArrayController withKeyPath:@"selection.selectedPosition" options:nil];
   163 	[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(updatePosition:) name:GrowlPositionPickerChangedSelectionNotification object:appPositionPicker];
   164 	
   165 	[applicationNameAndIconColumn setDataCell:imageTextCell];
   166 	[networkTableView reloadData];
   167 	
   168 	// Select the default style if possible. 
   169 	{
   170 		id arrangedObjects = [displayPluginsArrayController arrangedObjects];
   171 		int count = [arrangedObjects count];
   172 		NSString *defaultDisplayPluginName = [[self preferencesController] defaultDisplayPluginName];
   173 		int defaultStyleRow = NSNotFound;
   174 		for (int i = 0; i < count; i++) {
   175 			if ([[[arrangedObjects objectAtIndex:i] valueForKey:@"CFBundleName"] isEqualToString:defaultDisplayPluginName]) {
   176 				defaultStyleRow = i;
   177 				break;
   178 			}
   179 		}
   180 
   181 		if (defaultStyleRow != NSNotFound) {
   182 			/* Wait until the next run loop; otherwise everything isn't finished loading and we throw an exception.
   183 			* This is setting the view for the Displays tab, which isn't initially visible, so the user won't see
   184 			* the flicker. I'm don't know why this is necessary. -evands
   185 			*/
   186 			[self performSelector:@selector(selectRow:)
   187 					   withObject:[NSIndexSet indexSetWithIndex:defaultStyleRow]
   188 					   afterDelay:0];
   189 		}
   190 		
   191 		[[NSNotificationCenter defaultCenter] addObserver:self
   192 												 selector:@selector(translateSeparatorsInMenu:)
   193 													 name:NSPopUpButtonWillPopUpNotification
   194 												    object:soundMenuButton];
   195 
   196 		[[NSNotificationCenter defaultCenter] addObserver:self
   197 												 selector:@selector(translateSeparatorsInMenu:)
   198 													 name:NSPopUpButtonWillPopUpNotification
   199 												    object:displayMenuButton];
   200 
   201 		[[NSNotificationCenter defaultCenter] addObserver:self
   202 												 selector:@selector(translateSeparatorsInMenu:)
   203 													 name:NSPopUpButtonWillPopUpNotification
   204 												    object:notificationDisplayMenuButton];
   205 	}
   206 }
   207 
   208 - (void)selectRow:(NSIndexSet *)indexSet
   209 {
   210 	[displayPluginsTable selectRowIndexes:indexSet byExtendingSelection:NO];
   211 }
   212 
   213 - (void) mainViewDidLoad {
   214 	[[NSDistributedNotificationCenter defaultCenter] addObserver:self
   215 														selector:@selector(appRegistered:)
   216 															name:GROWL_APP_REGISTRATION_CONF
   217 														  object:nil];
   218 }
   219 
   220 #pragma mark -
   221 
   222 /*!
   223  * @brief Returns the bundle version of the Growl.prefPane bundle.
   224  */
   225 - (NSString *) bundleVersion {
   226 	return [[self bundle] objectForInfoDictionaryKey:(NSString *)kCFBundleVersionKey];
   227 }
   228 
   229 /*!
   230  * @brief Checks if a newer version of Growl is available at the Growl download site.
   231  *
   232  * The version.xml file is a property list which contains version numbers and
   233  * download URLs for several components.
   234  */
   235 - (IBAction) checkVersion:(id)sender {
   236 #pragma unused(sender)
   237 	[growlVersionProgress startAnimation:self];
   238 
   239 	if (!versionCheckURL)
   240 		versionCheckURL = [[NSURL alloc] initWithString:@"http://growl.info/version.xml"];
   241 
   242 	NSBundle *bundle = [self bundle];
   243 	NSDictionary *infoDict = [bundle infoDictionary];
   244 	NSString *currVersionNumber = [infoDict objectForKey:(NSString *)kCFBundleVersionKey];
   245 	NSDictionary *productVersionDict = [[NSDictionary alloc] initWithContentsOfURL:versionCheckURL];
   246 	NSString *executableName = [infoDict objectForKey:(NSString *)kCFBundleExecutableKey];
   247 	NSString *latestVersionNumber = [productVersionDict objectForKey:executableName];
   248 
   249 	CFURLRef downloadURL = CFURLCreateWithString(kCFAllocatorDefault,
   250 		(CFStringRef)[productVersionDict objectForKey:[executableName stringByAppendingString:@"DownloadURL"]], NULL);
   251 	/*
   252 	 NSLog([[[NSBundle bundleWithIdentifier:GROWL_PREFPANE_BUNDLE_IDENTIFIER] infoDictionary] objectForKey:(NSString *)kCFBundleExecutableKey] );
   253 	 NSLog(currVersionNumber);
   254 	 NSLog(latestVersionNumber);
   255 	 */
   256 
   257 	// do nothing--be quiet if there is no active connection or if the
   258 	// version number could not be downloaded
   259 	if (latestVersionNumber && (compareVersionStringsTranslating1_0To0_5(latestVersionNumber, currVersionNumber) > 0))
   260 		NSBeginAlertSheet(/*title*/ NSLocalizedStringFromTableInBundle(@"Update Available", nil, bundle, @""),
   261 						  /*defaultButton*/ nil, // use default localized button title ("OK" in English)
   262 						  /*alternateButton*/ NSLocalizedStringFromTableInBundle(@"Cancel", nil, bundle, @""),
   263 						  /*otherButton*/ nil,
   264 						  /*docWindow*/ nil,
   265 						  /*modalDelegate*/ self,
   266 						  /*didEndSelector*/ NULL,
   267 						  /*didDismissSelector*/ @selector(downloadSelector:returnCode:contextInfo:),
   268 						  /*contextInfo*/ (void *)downloadURL,
   269 						  /*msg*/ NSLocalizedStringFromTableInBundle(@"A newer version of Growl is available online. Would you like to download it now?", nil, [self bundle], @""));
   270 	else
   271 		CFRelease(downloadURL);
   272 
   273 	[productVersionDict release];
   274 
   275 	[growlVersionProgress stopAnimation:self];
   276 }
   277 
   278 - (void) downloadSelector:(NSWindow *)sheet returnCode:(int)returnCode contextInfo:(void *)contextInfo {
   279 #pragma unused(sheet)
   280 	CFURLRef downloadURL = (CFURLRef)contextInfo;
   281 	if (returnCode == NSAlertDefaultReturn)
   282 		[[NSWorkspace sharedWorkspace] openURL:(NSURL *)downloadURL];
   283 	CFRelease(downloadURL);
   284 }
   285 
   286 /*!
   287  * @brief Returns if GrowlMenu is currently running.
   288  */
   289 + (BOOL) isGrowlMenuRunning {
   290 	return [[GrowlPreferencesController sharedController] isRunning:@"com.Growl.MenuExtra"];
   291 }
   292 
   293 //subclassed from NSPreferencePane; called before the pane is displayed.
   294 - (void) willSelect {
   295 	NSString *lastVersion = [preferencesController objectForKey:LastKnownVersionKey];
   296 	NSString *currentVersion = [self bundleVersion];
   297 	if (!(lastVersion && [lastVersion isEqualToString:currentVersion])) {
   298 		if ([preferencesController isGrowlRunning]) {
   299 			[preferencesController setGrowlRunning:NO noMatterWhat:NO];
   300 			[preferencesController setGrowlRunning:YES noMatterWhat:YES];
   301 		}
   302 		[preferencesController setObject:currentVersion forKey:LastKnownVersionKey];
   303 	}
   304 
   305 	[self checkGrowlRunning];
   306 }
   307 
   308 - (void) didSelect {
   309 	[self reloadPreferences:nil];
   310 }
   311 
   312 /*!
   313  * @brief copy images to avoid resizing the original images stored in the tickets.
   314  */
   315 - (void) cacheImages {
   316 	if (images)
   317 		CFArrayRemoveAllValues(images);
   318 	else
   319 		images = CFArrayCreateMutable(kCFAllocatorDefault, 0, &kCFTypeArrayCallBacks);
   320 
   321 	NSEnumerator *enumerator = [[ticketsArrayController content] objectEnumerator];
   322 	GrowlApplicationTicket *ticket;
   323 	while ((ticket = [enumerator nextObject])) {
   324 		NSImage *icon = [[ticket icon] copy];
   325 		[icon setScalesWhenResized:YES];
   326 		[icon setSize:NSMakeSize(32.0f, 32.0f)];
   327 		CFArrayAppendValue(images, icon);
   328 		[icon release];
   329 	}
   330 }
   331 
   332 - (NSMutableArray *) tickets {
   333 	return tickets;
   334 }
   335 
   336 //using setTickets: will tip off the controller (KVO).
   337 //use this to set the tickets secretly.
   338 - (void) setTicketsWithoutTellingAnybody:(NSArray *)theTickets {
   339 	if (theTickets != tickets) {
   340 		if (tickets)
   341 			[tickets setArray:theTickets];
   342 		else
   343 			tickets = [theTickets mutableCopy];
   344 	}
   345 }
   346 
   347 //we don't need to do any special extra magic here - just being setTickets: is enough to tip off the controller.
   348 - (void) setTickets:(NSArray *)theTickets {
   349 	[self setTicketsWithoutTellingAnybody:theTickets];
   350 }
   351 
   352 - (void) removeFromTicketsAtIndex:(int)indexToRemove {
   353 	NSIndexSet *indices = [NSIndexSet indexSetWithIndex:indexToRemove];
   354 	[self willChange:NSKeyValueChangeInsertion valuesAtIndexes:indices forKey:@"tickets"];
   355 
   356 	[tickets removeObjectAtIndex:indexToRemove];
   357 
   358 	[self didChange:NSKeyValueChangeInsertion valuesAtIndexes:indices forKey:@"tickets"];
   359 }
   360 
   361 - (void) insertInTickets:(GrowlApplicationTicket *)newTicket {
   362 	NSIndexSet *indices = [NSIndexSet indexSetWithIndex:[tickets count]];
   363 	[self willChange:NSKeyValueChangeInsertion valuesAtIndexes:indices forKey:@"tickets"];
   364 
   365 	[tickets addObject:newTicket];
   366 
   367 	[self didChange:NSKeyValueChangeInsertion valuesAtIndexes:indices forKey:@"tickets"];
   368 }
   369 
   370 - (void) reloadDisplayPluginView {
   371 	NSArray *selectedPlugins = [displayPluginsArrayController selectedObjects];
   372 	unsigned numPlugins = [plugins count];
   373 	[currentPlugin release];
   374 	if (numPlugins > 0U && selectedPlugins && [selectedPlugins count] > 0U)
   375 		currentPlugin = [[selectedPlugins objectAtIndex:0U] retain];
   376 	else
   377 		currentPlugin = nil;
   378 
   379 	NSString *currentPluginName = [currentPlugin objectForKey:(NSString *)kCFBundleNameKey];
   380 	currentPluginController = (GrowlPlugin *)[pluginController pluginInstanceWithName:currentPluginName];
   381 	[self loadViewForDisplay:currentPluginName];
   382 	[displayAuthor setStringValue:[currentPlugin objectForKey:@"GrowlPluginAuthor"]];
   383 	[displayVersion setStringValue:[currentPlugin objectForKey:(NSString *)kCFBundleNameKey]];
   384 }
   385 
   386 /*!
   387  * @brief Called when a distributed GrowlPreferencesChanged notification is received.
   388  */
   389 - (void) reloadPrefs:(NSNotification *)notification {
   390 	// ignore notifications which are sent by ourselves
   391 	NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
   392 
   393 	NSNumber *pidValue = [[notification userInfo] objectForKey:@"pid"];
   394 	if (!pidValue || [pidValue intValue] != pid)
   395 		[self reloadPreferences:[notification object]];
   396 	
   397 	[pool release];
   398 }
   399 
   400 - (void) updatePosition:(NSNotification *)notification {
   401 	if([notification object] == globalPositionPicker) {
   402 		[preferencesController setInteger:[globalPositionPicker selectedPosition] forKey:GROWL_POSITION_PREFERENCE_KEY];
   403 	}
   404 	else if([notification object] == appPositionPicker) {
   405 		// a cheap hack around selection not providing a workable object
   406 		NSArray *selection = [ticketsArrayController selectedObjects];
   407 		if ([selection count] > 0)
   408 			[[selection objectAtIndex:0] setSelectedPosition:[appPositionPicker selectedPosition]];
   409 	}
   410 }
   411 
   412 /*!
   413  * @brief Reloads the preferences and updates the GUI accordingly.
   414  */
   415 - (void) reloadPreferences:(NSString *)object {
   416 	if (!object || [object isEqualToString:@"GrowlTicketChanged"]) {
   417 		GrowlTicketController *ticketController = [GrowlTicketController sharedController];
   418 		[ticketController loadAllSavedTickets];
   419 		[self setTickets:[[ticketController allSavedTickets] allValues]];
   420 		[self cacheImages];
   421 	}
   422 
   423 	[self setDisplayPlugins:[[GrowlPluginController sharedController] registeredPluginNamesArrayForType:GROWL_VIEW_EXTENSION]];
   424 
   425 #ifdef THIS_CODE_WAS_REMOVED_AND_I_DONT_KNOW_WHY
   426 	if (!object || [object isEqualToString:@"GrowlTicketChanged"])
   427 		[self setTickets:[[ticketController allSavedTickets] allValues]];
   428 
   429 	[preferencesController setSquelchMode:[preferencesController squelchMode]];
   430 	[preferencesController setGrowlMenuEnabled:[preferencesController isGrowlMenuEnabled]];
   431 
   432 	[self cacheImages];
   433 #endif
   434 
   435 	// If Growl is enabled, ensure the helper app is launched
   436 	if ([preferencesController boolForKey:GrowlEnabledKey])
   437 		[preferencesController launchGrowl:NO];
   438 
   439 	if ([plugins count] > 0U)
   440 		[self reloadDisplayPluginView];
   441 	else
   442 		[self loadViewForDisplay:nil];
   443 }
   444 
   445 - (BOOL) growlIsRunning {
   446 	return growlIsRunning;
   447 }
   448 
   449 - (void) setGrowlIsRunning:(BOOL)flag {
   450 	growlIsRunning = flag;
   451 }
   452 
   453 - (void) updateRunningStatus {
   454 	[startStopGrowl setEnabled:YES];
   455 	NSBundle *bundle = [self bundle];
   456 	[startStopGrowl setTitle:
   457 		growlIsRunning ? NSLocalizedStringFromTableInBundle(@"Stop Growl",nil,bundle,@"")
   458 					   : NSLocalizedStringFromTableInBundle(@"Start Growl",nil,bundle,@"")];
   459 	[growlRunningStatus setStringValue:
   460 		growlIsRunning ? NSLocalizedStringFromTableInBundle(@"Growl is running.",nil,bundle,@"")
   461 					   : NSLocalizedStringFromTableInBundle(@"Growl is stopped.",nil,bundle,@"")];
   462 	[growlRunningProgress stopAnimation:self];
   463 }
   464 
   465 - (void) observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object
   466 						change:(NSDictionary *)change context:(void *)context {
   467 #pragma unused(change, context)
   468 	if ([keyPath isEqualToString:@"selection"]) {
   469 		if ((object == ticketsArrayController))
   470 			[self setCanRemoveTicket:(activeTableView == growlApplications) && [ticketsArrayController canRemove]];
   471 		else if (object == displayPluginsArrayController)
   472 			[self reloadDisplayPluginView];
   473 	}
   474 }
   475 
   476 - (void) writeForwardDestinations {
   477 	NSMutableArray *destinations = [[NSMutableArray alloc] initWithCapacity:[services count]];
   478 	NSEnumerator *enumerator = [services objectEnumerator];
   479 	GrowlBrowserEntry *entry;
   480 	while ((entry = [enumerator nextObject]))
   481 		[destinations addObject:[entry properties]];
   482 	[preferencesController setObject:destinations forKey:GrowlForwardDestinationsKey];
   483 	[destinations release];
   484 }
   485 
   486 #pragma mark -
   487 #pragma mark Bindings accessors (not for programmatic use)
   488 
   489 - (GrowlPluginController *) pluginController {
   490 	if (!pluginController)
   491 		pluginController = [GrowlPluginController sharedController];
   492 
   493 	return pluginController;
   494 }
   495 - (GrowlPreferencesController *) preferencesController {
   496 	if (!preferencesController)
   497 		preferencesController = [GrowlPreferencesController sharedController];
   498 
   499 	return preferencesController;
   500 }
   501 
   502 - (NSArray *) sounds {
   503 	NSMutableArray *soundNames = [[NSMutableArray alloc] init];
   504 	
   505 	NSArray *paths = [NSArray arrayWithObjects:@"/System/Library/Sounds",
   506 												@"/Library/Sounds",
   507 											   [NSString stringWithFormat:@"%@/Library/Sounds", NSHomeDirectory()],
   508 											   nil];
   509 
   510 	NSString *directory;
   511 	NSEnumerator *dirEnumerator = [paths objectEnumerator];
   512 	while ((directory = [dirEnumerator nextObject])) {
   513 		BOOL isDirectory = NO;
   514 		
   515 		if ([[NSFileManager defaultManager] fileExistsAtPath:directory isDirectory:&isDirectory]) {
   516 			if (isDirectory) {
   517 				[soundNames addObject:@"-"];
   518 				
   519 				NSArray *files = [[NSFileManager defaultManager] directoryContentsAtPath:directory];
   520 
   521 				NSString *filename = nil;
   522 				NSEnumerator *fileEnumerator = [files objectEnumerator];
   523 				while ((filename = [fileEnumerator nextObject])) {
   524 					NSString *file = [filename stringByDeletingPathExtension];
   525 			
   526 					if (![file isEqualToString:@".DS_Store"])
   527 						[soundNames addObject:file];
   528 				}
   529 			}
   530 		}
   531 	}
   532 	
   533 	return [soundNames autorelease];
   534 }
   535 
   536 - (void)translateSeparatorsInMenu:(NSNotification *)notification
   537 {
   538 	NSPopUpButton * button = [notification object];
   539 	
   540 	NSMenu *menu = [button menu];
   541 	
   542 	int itemIndex = 0;
   543 	
   544 	while ((itemIndex = [menu indexOfItemWithTitle:@"-"]) != -1) {
   545 		[menu removeItemAtIndex:itemIndex];
   546 		[menu insertItem:[NSMenuItem separatorItem] atIndex:itemIndex];
   547 	}
   548 }
   549 
   550 #pragma mark Growl running state
   551 
   552 /*!
   553  * @brief Launches GrowlHelperApp.
   554  */
   555 - (void) launchGrowl {
   556 	// Don't allow the button to be clicked while we update
   557 	[startStopGrowl setEnabled:NO];
   558 	[growlRunningProgress startAnimation:self];
   559 
   560 	// Update our status visible to the user
   561 	[growlRunningStatus setStringValue:NSLocalizedStringFromTableInBundle(@"Launching Growl...",nil,[self bundle],@"")];
   562 
   563 	[preferencesController setGrowlRunning:YES noMatterWhat:NO];
   564 
   565 	// After 4 seconds force a status update, in case Growl didn't start/stop
   566 	[self performSelector:@selector(checkGrowlRunning)
   567 			   withObject:nil
   568 			   afterDelay:4.0];
   569 }
   570 
   571 /*!
   572  * @brief Terminates running GrowlHelperApp instances.
   573  */
   574 - (void) terminateGrowl {
   575 	// Don't allow the button to be clicked while we update
   576 	[startStopGrowl setEnabled:NO];
   577 	[growlRunningProgress startAnimation:self];
   578 
   579 	// Update our status visible to the user
   580 	[growlRunningStatus setStringValue:NSLocalizedStringFromTableInBundle(@"Terminating Growl...",nil,[self bundle],@"")];
   581 
   582 	// Ask the Growl Helper App to shutdown
   583 	[preferencesController setGrowlRunning:NO noMatterWhat:NO];
   584 
   585 	// After 4 seconds force a status update, in case growl didn't start/stop
   586 	[self performSelector:@selector(checkGrowlRunning)
   587 			   withObject:nil
   588 			   afterDelay:4.0];
   589 }
   590 
   591 #pragma mark "General" tab pane
   592 
   593 - (IBAction) startStopGrowl:(id) sender {
   594 #pragma unused(sender)
   595 	// Make sure growlIsRunning is correct
   596 	if (growlIsRunning != [preferencesController isGrowlRunning]) {
   597 		// Nope - lets just flip it and update status
   598 		[self setGrowlIsRunning:!growlIsRunning];
   599 		[self updateRunningStatus];
   600 		return;
   601 	}
   602 
   603 	// Our desired state is a toggle of the current state;
   604 	if (growlIsRunning)
   605 		[self terminateGrowl];
   606 	else
   607 		[self launchGrowl];
   608 }
   609 
   610 
   611 /*
   612 - (IBAction) customFileChosen:(id)sender {
   613 	int selected = [sender indexOfSelectedItem];
   614 	if ((selected == [sender numberOfItems] - 1) || (selected == -1)) {
   615 		NSSavePanel *sp = [NSSavePanel savePanel];
   616 		[sp setRequiredFileType:@"log"];
   617 		[sp setCanSelectHiddenExtension:YES];
   618 
   619 		int runResult = [sp runModalForDirectory:nil file:@""];
   620 		NSString *saveFilename = [sp filename];
   621 		if (runResult == NSFileHandlingPanelOKButton) {
   622 			unsigned saveFilenameIndex = NSNotFound;
   623 			unsigned                 i = CFArrayGetCount(customHistArray);
   624 			if (i) {
   625 				while (--i) {
   626 					if ([(id)CFArrayGetValueAtIndex(customHistArray, i) isEqual:saveFilename]) {
   627 						saveFilenameIndex = i;
   628 						break;
   629 					}
   630 				}
   631 			}
   632 			if (saveFilenameIndex == NSNotFound) {
   633 				if (CFArrayGetCount(customHistArray) == 3U)
   634 					CFArrayRemoveValueAtIndex(customHistArray, 2);
   635 			} else
   636 				CFArrayRemoveValueAtIndex(customHistArray, saveFilenameIndex);
   637 			CFArrayInsertValueAtIndex(customHistArray, 0, saveFilename);
   638 		}
   639 	} else {
   640 		CFStringRef temp = CFRetain(CFArrayGetValueAtIndex(customHistArray, selected));
   641 		CFArrayRemoveValueAtIndex(customHistArray, selected);
   642 		CFArrayInsertValueAtIndex(customHistArray, 0, temp);
   643 		CFRelease(temp);
   644 	}
   645 
   646 	unsigned numHistItems = CFArrayGetCount(customHistArray);
   647 	if (numHistItems) {
   648 		id s = (id)CFArrayGetValueAtIndex(customHistArray, 0);
   649 		[preferencesController setObject:s forKey:GrowlCustomHistKey1];
   650 
   651 		if ((numHistItems > 1U) && (s = (id)CFArrayGetValueAtIndex(customHistArray, 1)))
   652 			[preferencesController setObject:s forKey:GrowlCustomHistKey2];
   653 
   654 		if ((numHistItems > 2U) && (s = (id)CFArrayGetValueAtIndex(customHistArray, 2)))
   655 			[preferencesController setObject:s forKey:GrowlCustomHistKey3];
   656 
   657 		//[[logFileType cellAtRow:1 column:0] setEnabled:YES];
   658 		[logFileType selectCellAtRow:1 column:0];
   659 	}
   660 
   661 	[self updateLogPopupMenu];
   662 }*/
   663 
   664 - (void) updateLogPopupMenu {
   665 	[customMenuButton removeAllItems];
   666 
   667 	int numHistItems = CFArrayGetCount(customHistArray);
   668 	for (int i = 0U; i < numHistItems; i++) {
   669 		NSArray *pathComponentry = [[(NSString *)CFArrayGetValueAtIndex(customHistArray, i) stringByAbbreviatingWithTildeInPath] pathComponents];
   670 		unsigned numPathComponents = [pathComponentry count];
   671 		if (numPathComponents > 2U) {
   672 			unichar ellipsis = 0x2026;
   673 			NSMutableString *arg = [[NSMutableString alloc] initWithCharacters:&ellipsis length:1U];
   674 			[arg appendString:@"/"];
   675 			[arg appendString:[pathComponentry objectAtIndex:(numPathComponents - 2U)]];
   676 			[arg appendString:@"/"];
   677 			[arg appendString:[pathComponentry objectAtIndex:(numPathComponents - 1U)]];
   678 			[customMenuButton insertItemWithTitle:arg atIndex:i];
   679 			[arg release];
   680 		} else
   681 			[customMenuButton insertItemWithTitle:[(NSString *)CFArrayGetValueAtIndex(customHistArray, i) stringByAbbreviatingWithTildeInPath] atIndex:i];
   682 	}
   683 	// No separator if there's no file list yet
   684 	if (numHistItems > 0)
   685 		[[customMenuButton menu] addItem:[NSMenuItem separatorItem]];
   686 	[customMenuButton addItemWithTitle:NSLocalizedStringFromTableInBundle(@"Browse menu item title", nil, [self bundle], nil)];
   687 	//select first item, if any
   688 	[customMenuButton selectItemAtIndex:numHistItems ? 0 : -1];
   689 }
   690 
   691 #pragma mark "Applications" tab pane
   692 
   693 - (BOOL) canRemoveTicket {
   694 	return canRemoveTicket;
   695 }
   696 
   697 - (void) setCanRemoveTicket:(BOOL)flag {
   698 	canRemoveTicket = flag;
   699 }
   700 
   701 - (void) deleteTicket:(id)sender {
   702 #pragma unused(sender)
   703 	NSString *appName = [[[ticketsArrayController selectedObjects] objectAtIndex:0U] applicationName];
   704 	NSAlert *alert = [NSAlert alertWithMessageText:[NSString stringWithFormat:NSLocalizedStringFromTableInBundle(@"Are you sure you want to remove %@?", nil, [self bundle], nil), appName]
   705 									 defaultButton:NSLocalizedStringFromTableInBundle(@"Remove", nil, [self bundle], "Button title for removing something")
   706 								   alternateButton:NSLocalizedStringFromTableInBundle(@"Cancel", nil, [self bundle], "Button title for canceling")
   707 									   otherButton:nil
   708 						 informativeTextWithFormat:[NSString stringWithFormat:
   709 													NSLocalizedStringFromTableInBundle(@"This will remove all Growl settings for %@.", nil, [self bundle], ""), appName]];
   710 	[alert setIcon:[[[NSImage alloc] initWithContentsOfFile:[[self bundle] pathForImageResource:@"growl-icon"]] autorelease]];
   711 	[alert beginSheetModalForWindow:[[NSApplication sharedApplication] keyWindow] modalDelegate:self didEndSelector:@selector(deleteCallbackDidEnd:returnCode:contextInfo:) contextInfo:nil];
   712 }
   713 
   714 // this method is used as our callback to determine whether or not to delete the ticket
   715 -(void) deleteCallbackDidEnd:(NSAlert *)alert returnCode:(int)returnCode contextInfo:(void *)eventID {
   716 #pragma unused(alert)
   717 #pragma unused(eventID)
   718 	if (returnCode == NSAlertDefaultReturn) {
   719 		GrowlApplicationTicket *ticket = [[ticketsArrayController selectedObjects] objectAtIndex:0U];
   720 		NSString *path = [ticket path];
   721 
   722 		if ([[NSFileManager defaultManager] removeFileAtPath:path handler:nil]) {
   723 			CFNumberRef pidValue = CFNumberCreate(kCFAllocatorDefault, kCFNumberIntType, &pid);
   724 			CFStringRef keys[2] = { CFSTR("TicketName"), CFSTR("pid") };
   725 			CFTypeRef   values[2] = { [ticket applicationName], pidValue };
   726 			CFDictionaryRef userInfo = CFDictionaryCreate(kCFAllocatorDefault, (const void **)keys, (const void **)values, 2, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
   727 			CFRelease(pidValue);
   728 			CFNotificationCenterPostNotification(CFNotificationCenterGetDistributedCenter(),
   729 												 (CFStringRef)GrowlPreferencesChanged,
   730 												 CFSTR("GrowlTicketDeleted"),
   731 												 userInfo, false);
   732 			CFRelease(userInfo);
   733 			unsigned idx = [tickets indexOfObject:ticket];
   734 			CFArrayRemoveValueAtIndex(images, idx);
   735 
   736 			unsigned oldSelectionIndex = [ticketsArrayController selectionIndex];
   737 
   738 			///	Hmm... This doesn't work for some reason....
   739 			//	Even though the same method definitely (?) probably works in the appRegistered: method...
   740 
   741 			//	[self removeFromTicketsAtIndex:	[ticketsArrayController selectionIndex]];
   742 
   743 			NSMutableArray *newTickets = [tickets mutableCopy];
   744 			[newTickets removeObject:ticket];
   745 			[self setTickets:newTickets];
   746 			[newTickets release];
   747 
   748 			if (oldSelectionIndex >= [tickets count])
   749 				oldSelectionIndex = [tickets count] - 1;
   750 			[self cacheImages];
   751 			[ticketsArrayController setSelectionIndex:oldSelectionIndex];
   752 		}
   753 	}
   754 }
   755 
   756 -(IBAction)playSound:(id)sender
   757 {
   758 	if([sender indexOfSelectedItem] > 0) // The 0 item is "None"
   759 		[[NSSound soundNamed:[[sender selectedItem] title]] play];
   760 }
   761 
   762 #pragma mark "Display" tab pane
   763 
   764 - (IBAction) showDisabledDisplays:(id)sender {
   765 #pragma unused(sender)
   766 	[disabledDisplaysList setString:[[pluginController disabledPlugins] componentsJoinedByString:@"\n"]];
   767 	
   768 	[NSApp beginSheet:disabledDisplaysSheet 
   769 	   modalForWindow:[[self mainView] window]
   770 		modalDelegate:nil
   771 	   didEndSelector:nil
   772 		  contextInfo:nil];
   773 }
   774 
   775 - (IBAction) endDisabledDisplays:(id)sender {
   776 #pragma unused(sender)
   777 	[NSApp endSheet:disabledDisplaysSheet];
   778 	[disabledDisplaysSheet orderOut:disabledDisplaysSheet];
   779 }
   780 
   781 // Returns a boolean based on whether any disabled displays are present, used for the 'hidden' binding of the button on the tab
   782 - (BOOL)hasDisabledDisplays {
   783 	return [pluginController disabledPluginsPresent];
   784 }
   785 
   786 // Popup buttons that post preview notifications support suppressing the preview with the Option key
   787 - (IBAction) showPreview:(id)sender {
   788 	if(([sender isKindOfClass:[NSPopUpButton class]]) && (GetCurrentKeyModifiers() & optionKey))
   789 		return;
   790 	
   791 	NSDictionary *pluginToUse = currentPlugin;
   792 	
   793 #warning This does not work if the popup button is not using the exact same order as displayPluginsArrayController - a default or separator item breaks it
   794 	if([sender isKindOfClass:[NSPopUpButton class]]) 
   795 		pluginToUse = [[displayPluginsArrayController content] objectAtIndex:[(NSPopUpButton *)sender indexOfSelectedItem]];
   796 			
   797 	[[NSDistributedNotificationCenter defaultCenter] postNotificationName:GrowlPreview
   798 																   object:[pluginToUse objectForKey:GrowlPluginInfoKeyName]];
   799 }
   800 
   801 - (void) loadViewForDisplay:(NSString *)displayName {
   802 	NSView *newView = nil;
   803 	NSPreferencePane *prefPane = nil, *oldPrefPane = nil;
   804 
   805 	if (pluginPrefPane)
   806 		oldPrefPane = pluginPrefPane;
   807 
   808 	if (displayName) {
   809 		// Old plugins won't support the new protocol. Check first
   810 		if ([currentPluginController respondsToSelector:@selector(preferencePane)])
   811 			prefPane = [currentPluginController preferencePane];
   812 
   813 		if (prefPane == pluginPrefPane) {
   814 			// Don't bother swapping anything
   815 			return;
   816 		} else {
   817 			[pluginPrefPane release];
   818 			pluginPrefPane = [prefPane retain];
   819 			[oldPrefPane willUnselect];
   820 		}
   821 		if (pluginPrefPane) {
   822 			if ([loadedPrefPanes containsObject:pluginPrefPane]) {
   823 				newView = [pluginPrefPane mainView];
   824 			} else {
   825 				newView = [pluginPrefPane loadMainView];
   826 				[loadedPrefPanes addObject:pluginPrefPane];
   827 			}
   828 			[pluginPrefPane willSelect];
   829 		}
   830 	} else {
   831 		[pluginPrefPane release];
   832 		pluginPrefPane = nil;
   833 	}
   834 	if (!newView)
   835 		newView = displayDefaultPrefView;
   836 	if (displayPrefView != newView) {
   837 		// Make sure the new view is framed correctly
   838 		[newView setFrame:DISPLAY_PREF_FRAME];
   839 		[[displayPrefView superview] replaceSubview:displayPrefView with:newView];
   840 		displayPrefView = newView;
   841 
   842 		if (pluginPrefPane) {
   843 			[pluginPrefPane didSelect];
   844 			// Hook up key view chain
   845 			[displayPluginsTable setNextKeyView:[pluginPrefPane firstKeyView]];
   846 			[[pluginPrefPane lastKeyView] setNextKeyView:previewButton];
   847 			//[[displayPluginsTable window] makeFirstResponder:[pluginPrefPane initialKeyView]];
   848 		} else {
   849 			[displayPluginsTable setNextKeyView:previewButton];
   850 		}
   851 
   852 		if (oldPrefPane)
   853 			[oldPrefPane didUnselect];
   854 	}
   855 }
   856 
   857 #pragma mark About Tab
   858 
   859 - (void) setupAboutTab {
   860 	[aboutVersionString setStringValue:[NSString stringWithFormat:@"%@ %@", 
   861 										[[self bundle] objectForInfoDictionaryKey:@"CFBundleName"], 
   862 										[[self bundle] objectForInfoDictionaryKey:@"CFBundleShortVersionString"]]];
   863 	[aboutBoxTextView readRTFDFromFile:[[self bundle] pathForResource:@"About" ofType:@"rtf"]];
   864 }
   865 
   866 - (IBAction) openGrowlWebSite:(id)sender {
   867 #pragma unused(sender)
   868 	[[NSWorkspace sharedWorkspace] openURL:growlWebSiteURL];
   869 }
   870 
   871 - (IBAction) openGrowlForum:(id)sender {
   872 #pragma unused(sender)
   873 	[[NSWorkspace sharedWorkspace] openURL:growlForumURL];
   874 }
   875 
   876 - (IBAction) openGrowlBugSubmissionPage:(id)sender {
   877 #pragma unused(sender)
   878 	[[NSWorkspace sharedWorkspace] openURL:growlBugSubmissionURL];
   879 }
   880 
   881 - (IBAction) openGrowlDonate:(id)sender {
   882  #pragma unused(sender)
   883 	[[NSWorkspace sharedWorkspace] openURL:growlDonateURL];
   884 }
   885 #pragma mark TableView data source methods
   886 
   887 - (int) numberOfRowsInTableView:(NSTableView*)tableView {
   888 	if(tableView == networkTableView) {
   889 		return [[self services] count];
   890 	}
   891 	return 0;
   892 }
   893 - (void) tableViewDidClickInBody:(NSTableView *)tableView {
   894 	activeTableView = tableView;
   895 	[self setCanRemoveTicket:(activeTableView == growlApplications) && [ticketsArrayController canRemove]];
   896 }
   897 
   898 - (void)tableView:(NSTableView *)aTableView setObjectValue:(id)anObject forTableColumn:(NSTableColumn *)aTableColumn row:(int)rowIndex {
   899 #pragma unused(aTableView)
   900 	if(aTableColumn == servicePasswordColumn) {
   901 		[[services objectAtIndex:rowIndex] setPassword:anObject];
   902 	}
   903 
   904 }
   905 
   906 - (id) tableView:(NSTableView *)aTableView objectValueForTableColumn:(NSTableColumn *)aTableColumn row:(int)rowIndex {
   907 #pragma unused(aTableView)
   908 	// we check to make sure we have the image + text column and then set its image manually
   909 	if (aTableColumn == applicationNameAndIconColumn) {
   910 		NSArray *arrangedTickets = [ticketsArrayController arrangedObjects];
   911 		unsigned idx = [tickets indexOfObject:[arrangedTickets objectAtIndex:rowIndex]];
   912 		[[aTableColumn dataCellForRow:rowIndex] setImage:(NSImage *)CFArrayGetValueAtIndex(images,idx)];
   913 	} else if (aTableColumn == servicePasswordColumn) {
   914 		return [[services objectAtIndex:rowIndex] password];
   915 	}
   916 
   917 	return nil;
   918 }
   919 
   920 - (IBAction) tableViewDoubleClick:(id)sender {
   921 	if ([ticketsArrayController selectionIndex] != NSNotFound) {
   922 		[applicationsTab selectLastTabViewItem:sender];
   923 		[configurationTab selectFirstTabViewItem:sender];
   924 	}
   925 }
   926 
   927 #pragma mark NSNetServiceBrowser Delegate Methods
   928 
   929 - (void) netServiceBrowser:(NSNetServiceBrowser *)aNetServiceBrowser didFindService:(NSNetService *)aNetService moreComing:(BOOL)moreComing {
   930 #pragma unused(aNetServiceBrowser)
   931 	// check if a computer with this name has already been added
   932 	NSString *name = [aNetService name];
   933 	NSEnumerator *enumerator = [services objectEnumerator];
   934 	GrowlBrowserEntry *entry;
   935 	while ((entry = [enumerator nextObject])) {
   936 		if ([[entry computerName] isEqualToString:name]) {
   937 			[entry setActive:YES];
   938 			return;
   939 		}
   940 	}
   941 
   942 	// don't add the local machine
   943 	CFStringRef localHostName = SCDynamicStoreCopyComputerName(/*store*/ NULL,
   944 															   /*nameEncoding*/ NULL);
   945 	CFComparisonResult isLocalHost = CFStringCompare(localHostName, (CFStringRef)name, 0);
   946 	CFRelease(localHostName);
   947 	if (isLocalHost == kCFCompareEqualTo)
   948 		return;
   949 
   950 	// add a new entry at the end
   951 	entry = [[GrowlBrowserEntry alloc] initWithComputerName:name];
   952 	[self willChangeValueForKey:@"services"];
   953 	[services addObject:entry];
   954 	[self didChangeValueForKey:@"services"];
   955 	[entry release];
   956 
   957 	if (!moreComing)
   958 		[self writeForwardDestinations];
   959 }
   960 
   961 - (void) netServiceBrowser:(NSNetServiceBrowser *)aNetServiceBrowser didRemoveService:(NSNetService *)aNetService moreComing:(BOOL)moreComing {
   962 #pragma unused(aNetServiceBrowser)
   963 	NSEnumerator *serviceEnum = [services objectEnumerator];
   964 	GrowlBrowserEntry *currentEntry;
   965 	NSString *name = [aNetService name];
   966 
   967 	while ((currentEntry = [serviceEnum nextObject])) {
   968 		if ([[currentEntry computerName] isEqualToString:name]) {
   969 			[currentEntry setActive:NO];
   970 			break;
   971 		}
   972 	}
   973 
   974 	if (!moreComing)
   975 		[self writeForwardDestinations];
   976 }
   977 
   978 #pragma mark Bonjour
   979 
   980 - (void) resolveService:(id)sender {
   981 	NSLog(@"What calls resolveService:?");
   982 }
   983 
   984 - (NSMutableArray *) services {
   985 	return services;
   986 }
   987 
   988 - (void) setServices:(NSMutableArray *)theServices {
   989 	if (theServices != services) {
   990 		if (theServices) {
   991 			if (services)
   992 				[services setArray:theServices];
   993 			else
   994 				services = [theServices retain];
   995 		} else {
   996 			[services release];
   997 			services = nil;
   998 		}
   999 	}
  1000 }
  1001 
  1002 - (unsigned) countOfServices {
  1003 	return [services count];
  1004 }
  1005 
  1006 - (id) objectInServicesAtIndex:(unsigned)idx {
  1007 	return [services objectAtIndex:idx];
  1008 }
  1009 
  1010 - (void) insertObject:(id)anObject inServicesAtIndex:(unsigned)idx {
  1011 	[services insertObject:anObject atIndex:idx];
  1012 }
  1013 
  1014 - (void) replaceObjectInServicesAtIndex:(unsigned)idx withObject:(id)anObject {
  1015 	[services replaceObjectAtIndex:idx withObject:anObject];
  1016 }
  1017 
  1018 #pragma mark Detecting Growl
  1019 
  1020 - (void) checkGrowlRunning {
  1021 	[self setGrowlIsRunning:[preferencesController isGrowlRunning]];
  1022 	[self updateRunningStatus];
  1023 }
  1024 
  1025 #pragma mark "Display Options" tab pane
  1026 
  1027 - (NSArray *) displayPlugins {
  1028 	return plugins;
  1029 }
  1030 
  1031 - (void) setDisplayPlugins:(NSArray *)thePlugins {
  1032 	if (thePlugins != plugins) {
  1033 		[plugins release];
  1034 		plugins = [thePlugins retain];
  1035 	}
  1036 }
  1037 
  1038 #pragma mark -
  1039 
  1040 /*!
  1041  * @brief Refresh preferences when a new application registers with Growl
  1042  */
  1043 - (void) appRegistered: (NSNotification *) note {
  1044 	NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
  1045 
  1046 	NSString *app = [note object];
  1047 	GrowlApplicationTicket *newTicket = [[GrowlApplicationTicket alloc] initTicketForApplication:app];
  1048 
  1049 	/*
  1050 	 *	Because the tickets array is under KVObservation by the TicketsArrayController
  1051 	 *	We need to remove the ticket using the correct KVC method:
  1052 	 */
  1053 
  1054 	NSEnumerator *ticketEnumerator = [tickets objectEnumerator];
  1055 	GrowlApplicationTicket *ticket;
  1056 	int removalIndex = -1;
  1057 
  1058 	int	i = 0;
  1059 	while ((ticket = [ticketEnumerator nextObject])) {
  1060 		if ([[ticket applicationName] isEqualToString:app]) {
  1061 			removalIndex = i;
  1062 			break;
  1063 		}
  1064 		++i;
  1065 	}
  1066 
  1067 	if (removalIndex != -1)
  1068 		[self removeFromTicketsAtIndex:removalIndex];
  1069 	[self insertInTickets:newTicket];
  1070 	[newTicket release];
  1071 
  1072 	[self cacheImages];
  1073 	
  1074 	[pool release];
  1075 }
  1076 
  1077 - (void) growlLaunched:(NSNotification *)note {
  1078 #pragma unused(note)
  1079 	NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
  1080 
  1081 	[self setGrowlIsRunning:YES];
  1082 	[self updateRunningStatus];
  1083 	
  1084 	[pool release];
  1085 }
  1086 
  1087 - (void) growlTerminated:(NSNotification *)note {
  1088 #pragma unused(note)
  1089 	NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
  1090 
  1091 	[self setGrowlIsRunning:NO];
  1092 	[self updateRunningStatus];
  1093 	
  1094 	[pool release];
  1095 }
  1096 
  1097 @end