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