Core/Source/GrowlPreferencePane.m
author Peter Hosey
Tue Apr 14 18:04:03 2009 -0700 (2009-04-14)
changeset 4189 30c4b669cad3
parent 4188 71e205ca6b20
child 4190 e0dba26e13a1
permissions -rw-r--r--
Fixed the off-by-two error in the displays pop-ups on the Applications tab (#251376).

Among the changes:
1. We no longer use Bindings to populate the pop-up menus. Instead, we populate them ourselves.
2. Likewise, we no longer use Bindings to set the pop-up menus' selections, and the pop-up menus do not talk directly to the relevant array controllers. Instead, the pop-ups send action messages to the preference pane, and each action method changes the application's or notification's display setting.
3. The Configure button and table view double-click action now both go through the same method.
4. The action method for activating the Applications tab (see #3) repopulates the displays pop-ups and updates their selections.
5. We now identify the selected display by its bundle name (in the representedObject property of the menu item), instead of by its index.
     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 @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 	int 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 		int count = [arrangedObjects count];
   178 		NSString *defaultDisplayPluginName = [[self preferencesController] defaultDisplayPluginName];
   179 		int defaultStyleRow = NSNotFound;
   180 		for (int 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 	[growlVersionProgress startAnimation:self];
   234 
   235 	if (!versionCheckURL)
   236 		versionCheckURL = [[NSURL alloc] initWithString:@"http://growl.info/version.xml"];
   237 
   238 	NSBundle *bundle = [self bundle];
   239 	NSDictionary *infoDict = [bundle infoDictionary];
   240 	NSString *currVersionNumber = [infoDict objectForKey:(NSString *)kCFBundleVersionKey];
   241 	NSDictionary *productVersionDict = [[NSDictionary alloc] initWithContentsOfURL:versionCheckURL];
   242 	NSString *executableName = [infoDict objectForKey:(NSString *)kCFBundleExecutableKey];
   243 	NSString *latestVersionNumber = [productVersionDict objectForKey:executableName];
   244 
   245 	CFURLRef downloadURL = CFURLCreateWithString(kCFAllocatorDefault,
   246 		(CFStringRef)[productVersionDict objectForKey:[executableName stringByAppendingString:@"DownloadURL"]], NULL);
   247 	/*
   248 	 NSLog([[[NSBundle bundleWithIdentifier:GROWL_PREFPANE_BUNDLE_IDENTIFIER] infoDictionary] objectForKey:(NSString *)kCFBundleExecutableKey] );
   249 	 NSLog(currVersionNumber);
   250 	 NSLog(latestVersionNumber);
   251 	 */
   252 
   253 	// do nothing--be quiet if there is no active connection or if the
   254 	// version number could not be downloaded
   255 	if (latestVersionNumber && (compareVersionStringsTranslating1_0To0_5(latestVersionNumber, currVersionNumber) > 0))
   256 		NSBeginAlertSheet(/*title*/ NSLocalizedStringFromTableInBundle(@"Update Available", nil, bundle, @""),
   257 						  /*defaultButton*/ nil, // use default localized button title ("OK" in English)
   258 						  /*alternateButton*/ NSLocalizedStringFromTableInBundle(@"Cancel", nil, bundle, @""),
   259 						  /*otherButton*/ nil,
   260 						  /*docWindow*/ nil,
   261 						  /*modalDelegate*/ self,
   262 						  /*didEndSelector*/ NULL,
   263 						  /*didDismissSelector*/ @selector(downloadSelector:returnCode:contextInfo:),
   264 						  /*contextInfo*/ (void *)downloadURL,
   265 						  /*msg*/ NSLocalizedStringFromTableInBundle(@"A newer version of Growl is available online. Would you like to download it now?", nil, [self bundle], @""));
   266 	else
   267 		CFRelease(downloadURL);
   268 
   269 	[productVersionDict release];
   270 
   271 	[growlVersionProgress stopAnimation:self];
   272 }
   273 
   274 - (void) downloadSelector:(NSWindow *)sheet returnCode:(int)returnCode contextInfo:(void *)contextInfo {
   275 #pragma unused(sheet)
   276 	CFURLRef downloadURL = (CFURLRef)contextInfo;
   277 	if (returnCode == NSAlertDefaultReturn)
   278 		[[NSWorkspace sharedWorkspace] openURL:(NSURL *)downloadURL];
   279 	CFRelease(downloadURL);
   280 }
   281 
   282 /*!
   283  * @brief Returns if GrowlMenu is currently running.
   284  */
   285 + (BOOL) isGrowlMenuRunning {
   286 	return [[GrowlPreferencesController sharedController] isRunning:@"com.Growl.MenuExtra"];
   287 }
   288 
   289 //subclassed from NSPreferencePane; called before the pane is displayed.
   290 - (void) willSelect {
   291 	NSString *lastVersion = [preferencesController objectForKey:LastKnownVersionKey];
   292 	NSString *currentVersion = [self bundleVersion];
   293 	if (!(lastVersion && [lastVersion isEqualToString:currentVersion])) {
   294 		if ([preferencesController isGrowlRunning]) {
   295 			[preferencesController setGrowlRunning:NO noMatterWhat:NO];
   296 			[preferencesController setGrowlRunning:YES noMatterWhat:YES];
   297 		}
   298 		[preferencesController setObject:currentVersion forKey:LastKnownVersionKey];
   299 	}
   300 
   301 	[self checkGrowlRunning];
   302 }
   303 
   304 - (void) didSelect {
   305 	[self reloadPreferences:nil];
   306 }
   307 
   308 /*!
   309  * @brief copy images to avoid resizing the original images stored in the tickets.
   310  */
   311 - (void) cacheImages {
   312 	if (images)
   313 		CFArrayRemoveAllValues(images);
   314 	else
   315 		images = CFArrayCreateMutable(kCFAllocatorDefault, 0, &kCFTypeArrayCallBacks);
   316 
   317 	NSEnumerator *enumerator = [[ticketsArrayController content] objectEnumerator];
   318 	GrowlApplicationTicket *ticket;
   319 	while ((ticket = [enumerator nextObject])) {
   320 		NSImage *icon = [[ticket icon] copy];
   321 		[icon setScalesWhenResized:YES];
   322 		[icon setSize:NSMakeSize(32.0f, 32.0f)];
   323 		CFArrayAppendValue(images, icon);
   324 		[icon release];
   325 	}
   326 }
   327 
   328 - (NSMutableArray *) tickets {
   329 	return tickets;
   330 }
   331 
   332 //using setTickets: will tip off the controller (KVO).
   333 //use this to set the tickets secretly.
   334 - (void) setTicketsWithoutTellingAnybody:(NSArray *)theTickets {
   335 	if (theTickets != tickets) {
   336 		if (tickets)
   337 			[tickets setArray:theTickets];
   338 		else
   339 			tickets = [theTickets mutableCopy];
   340 	}
   341 }
   342 
   343 //we don't need to do any special extra magic here - just being setTickets: is enough to tip off the controller.
   344 - (void) setTickets:(NSArray *)theTickets {
   345 	[self setTicketsWithoutTellingAnybody:theTickets];
   346 }
   347 
   348 - (void) removeFromTicketsAtIndex:(int)indexToRemove {
   349 	NSIndexSet *indices = [NSIndexSet indexSetWithIndex:indexToRemove];
   350 	[self willChange:NSKeyValueChangeInsertion valuesAtIndexes:indices forKey:@"tickets"];
   351 
   352 	[tickets removeObjectAtIndex:indexToRemove];
   353 
   354 	[self didChange:NSKeyValueChangeInsertion valuesAtIndexes:indices forKey:@"tickets"];
   355 }
   356 
   357 - (void) insertInTickets:(GrowlApplicationTicket *)newTicket {
   358 	NSIndexSet *indices = [NSIndexSet indexSetWithIndex:[tickets count]];
   359 	[self willChange:NSKeyValueChangeInsertion valuesAtIndexes:indices forKey:@"tickets"];
   360 
   361 	[tickets addObject:newTicket];
   362 
   363 	[self didChange:NSKeyValueChangeInsertion valuesAtIndexes:indices forKey:@"tickets"];
   364 }
   365 
   366 - (void) reloadDisplayPluginView {
   367 	NSArray *selectedPlugins = [displayPluginsArrayController selectedObjects];
   368 	unsigned numPlugins = [plugins count];
   369 	[currentPlugin release];
   370 	if (numPlugins > 0U && selectedPlugins && [selectedPlugins count] > 0U)
   371 		currentPlugin = [[selectedPlugins objectAtIndex:0U] retain];
   372 	else
   373 		currentPlugin = nil;
   374 
   375 	NSString *currentPluginName = [currentPlugin objectForKey:(NSString *)kCFBundleNameKey];
   376 	currentPluginController = (GrowlPlugin *)[pluginController pluginInstanceWithName:currentPluginName];
   377 	[self loadViewForDisplay:currentPluginName];
   378 	[displayAuthor setStringValue:[currentPlugin objectForKey:@"GrowlPluginAuthor"]];
   379 	[displayVersion setStringValue:[currentPlugin objectForKey:(NSString *)kCFBundleNameKey]];
   380 }
   381 
   382 /*!
   383  * @brief Called when a distributed GrowlPreferencesChanged notification is received.
   384  */
   385 - (void) reloadPrefs:(NSNotification *)notification {
   386 	// ignore notifications which are sent by ourselves
   387 	NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
   388 
   389 	NSNumber *pidValue = [[notification userInfo] objectForKey:@"pid"];
   390 	if (!pidValue || [pidValue intValue] != pid)
   391 		[self reloadPreferences:[notification object]];
   392 	
   393 	[pool release];
   394 }
   395 
   396 - (void) updatePosition:(NSNotification *)notification {
   397 	if([notification object] == globalPositionPicker) {
   398 		[preferencesController setInteger:[globalPositionPicker selectedPosition] forKey:GROWL_POSITION_PREFERENCE_KEY];
   399 	}
   400 	else if([notification object] == appPositionPicker) {
   401 		// a cheap hack around selection not providing a workable object
   402 		NSArray *selection = [ticketsArrayController selectedObjects];
   403 		if ([selection count] > 0)
   404 			[[selection objectAtIndex:0] setSelectedPosition:[appPositionPicker selectedPosition]];
   405 	}
   406 }
   407 
   408 /*!
   409  * @brief Reloads the preferences and updates the GUI accordingly.
   410  */
   411 - (void) reloadPreferences:(NSString *)object {
   412 	if (!object || [object isEqualToString:@"GrowlTicketChanged"]) {
   413 		GrowlTicketController *ticketController = [GrowlTicketController sharedController];
   414 		[ticketController loadAllSavedTickets];
   415 		[self setTickets:[[ticketController allSavedTickets] allValues]];
   416 		[self cacheImages];
   417 	}
   418 
   419 	[self setDisplayPlugins:[[GrowlPluginController sharedController] registeredPluginNamesArrayForType:GROWL_VIEW_EXTENSION]];
   420 
   421 #ifdef THIS_CODE_WAS_REMOVED_AND_I_DONT_KNOW_WHY
   422 	if (!object || [object isEqualToString:@"GrowlTicketChanged"])
   423 		[self setTickets:[[ticketController allSavedTickets] allValues]];
   424 
   425 	[preferencesController setSquelchMode:[preferencesController squelchMode]];
   426 	[preferencesController setGrowlMenuEnabled:[preferencesController isGrowlMenuEnabled]];
   427 
   428 	[self cacheImages];
   429 #endif
   430 
   431 	// If Growl is enabled, ensure the helper app is launched
   432 	if ([preferencesController boolForKey:GrowlEnabledKey])
   433 		[preferencesController launchGrowl:NO];
   434 
   435 	if ([plugins count] > 0U)
   436 		[self reloadDisplayPluginView];
   437 	else
   438 		[self loadViewForDisplay:nil];
   439 }
   440 
   441 - (BOOL) growlIsRunning {
   442 	return growlIsRunning;
   443 }
   444 
   445 - (void) setGrowlIsRunning:(BOOL)flag {
   446 	growlIsRunning = flag;
   447 }
   448 
   449 - (void) updateRunningStatus {
   450 	[startStopGrowl setEnabled:YES];
   451 	NSBundle *bundle = [self bundle];
   452 	[startStopGrowl setTitle:
   453 		growlIsRunning ? NSLocalizedStringFromTableInBundle(@"Stop Growl",nil,bundle,@"")
   454 					   : NSLocalizedStringFromTableInBundle(@"Start Growl",nil,bundle,@"")];
   455 	[growlRunningStatus setStringValue:
   456 		growlIsRunning ? NSLocalizedStringFromTableInBundle(@"Growl is running.",nil,bundle,@"")
   457 					   : NSLocalizedStringFromTableInBundle(@"Growl is stopped.",nil,bundle,@"")];
   458 	[growlRunningProgress stopAnimation:self];
   459 }
   460 
   461 - (void) observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object
   462 						change:(NSDictionary *)change context:(void *)context {
   463 #pragma unused(change, context)
   464 	if ([keyPath isEqualToString:@"selection"]) {
   465 		if ((object == ticketsArrayController))
   466 			[self setCanRemoveTicket:(activeTableView == growlApplications) && [ticketsArrayController canRemove]];
   467 		else if (object == displayPluginsArrayController)
   468 			[self reloadDisplayPluginView];
   469 	}
   470 }
   471 
   472 - (void) writeForwardDestinations {
   473 	NSMutableArray *destinations = [[NSMutableArray alloc] initWithCapacity:[services count]];
   474 	NSEnumerator *enumerator = [services objectEnumerator];
   475 	GrowlBrowserEntry *entry;
   476 	while ((entry = [enumerator nextObject]))
   477 		[destinations addObject:[entry properties]];
   478 	[preferencesController setObject:destinations forKey:GrowlForwardDestinationsKey];
   479 	[destinations release];
   480 }
   481 
   482 #pragma mark -
   483 #pragma mark Bindings accessors (not for programmatic use)
   484 
   485 - (GrowlPluginController *) pluginController {
   486 	if (!pluginController)
   487 		pluginController = [GrowlPluginController sharedController];
   488 
   489 	return pluginController;
   490 }
   491 - (GrowlPreferencesController *) preferencesController {
   492 	if (!preferencesController)
   493 		preferencesController = [GrowlPreferencesController sharedController];
   494 
   495 	return preferencesController;
   496 }
   497 
   498 - (NSArray *) sounds {
   499 	NSMutableArray *soundNames = [[NSMutableArray alloc] init];
   500 	
   501 	NSArray *paths = [NSArray arrayWithObjects:@"/System/Library/Sounds",
   502 												@"/Library/Sounds",
   503 											   [NSString stringWithFormat:@"%@/Library/Sounds", NSHomeDirectory()],
   504 											   nil];
   505 
   506 	NSString *directory;
   507 	NSEnumerator *dirEnumerator = [paths objectEnumerator];
   508 	while ((directory = [dirEnumerator nextObject])) {
   509 		BOOL isDirectory = NO;
   510 		
   511 		if ([[NSFileManager defaultManager] fileExistsAtPath:directory isDirectory:&isDirectory]) {
   512 			if (isDirectory) {
   513 				[soundNames addObject:@"-"];
   514 				
   515 				NSArray *files = [[NSFileManager defaultManager] directoryContentsAtPath:directory];
   516 
   517 				NSString *filename = nil;
   518 				NSEnumerator *fileEnumerator = [files objectEnumerator];
   519 				while ((filename = [fileEnumerator nextObject])) {
   520 					NSString *file = [filename stringByDeletingPathExtension];
   521 			
   522 					if (![file isEqualToString:@".DS_Store"])
   523 						[soundNames addObject:file];
   524 				}
   525 			}
   526 		}
   527 	}
   528 	
   529 	return [soundNames autorelease];
   530 }
   531 
   532 - (void)translateSeparatorsInMenu:(NSNotification *)notification
   533 {
   534 	NSPopUpButton * button = [notification object];
   535 	
   536 	NSMenu *menu = [button menu];
   537 	
   538 	int itemIndex = 0;
   539 	
   540 	while ((itemIndex = [menu indexOfItemWithTitle:@"-"]) != -1) {
   541 		[menu removeItemAtIndex:itemIndex];
   542 		[menu insertItem:[NSMenuItem separatorItem] atIndex:itemIndex];
   543 	}
   544 }
   545 
   546 #pragma mark Growl running state
   547 
   548 /*!
   549  * @brief Launches GrowlHelperApp.
   550  */
   551 - (void) launchGrowl {
   552 	// Don't allow the button to be clicked while we update
   553 	[startStopGrowl setEnabled:NO];
   554 	[growlRunningProgress startAnimation:self];
   555 
   556 	// Update our status visible to the user
   557 	[growlRunningStatus setStringValue:NSLocalizedStringFromTableInBundle(@"Launching Growl...",nil,[self bundle],@"")];
   558 
   559 	[preferencesController setGrowlRunning:YES noMatterWhat:NO];
   560 
   561 	// After 4 seconds force a status update, in case Growl didn't start/stop
   562 	[self performSelector:@selector(checkGrowlRunning)
   563 			   withObject:nil
   564 			   afterDelay:4.0];
   565 }
   566 
   567 /*!
   568  * @brief Terminates running GrowlHelperApp instances.
   569  */
   570 - (void) terminateGrowl {
   571 	// Don't allow the button to be clicked while we update
   572 	[startStopGrowl setEnabled:NO];
   573 	[growlRunningProgress startAnimation:self];
   574 
   575 	// Update our status visible to the user
   576 	[growlRunningStatus setStringValue:NSLocalizedStringFromTableInBundle(@"Terminating Growl...",nil,[self bundle],@"")];
   577 
   578 	// Ask the Growl Helper App to shutdown
   579 	[preferencesController setGrowlRunning:NO noMatterWhat:NO];
   580 
   581 	// After 4 seconds force a status update, in case growl didn't start/stop
   582 	[self performSelector:@selector(checkGrowlRunning)
   583 			   withObject:nil
   584 			   afterDelay:4.0];
   585 }
   586 
   587 #pragma mark "General" tab pane
   588 
   589 - (IBAction) startStopGrowl:(id) sender {
   590 #pragma unused(sender)
   591 	// Make sure growlIsRunning is correct
   592 	if (growlIsRunning != [preferencesController isGrowlRunning]) {
   593 		// Nope - lets just flip it and update status
   594 		[self setGrowlIsRunning:!growlIsRunning];
   595 		[self updateRunningStatus];
   596 		return;
   597 	}
   598 
   599 	// Our desired state is a toggle of the current state;
   600 	if (growlIsRunning)
   601 		[self terminateGrowl];
   602 	else
   603 		[self launchGrowl];
   604 }
   605 
   606 
   607 /*
   608 - (IBAction) customFileChosen:(id)sender {
   609 	int selected = [sender indexOfSelectedItem];
   610 	if ((selected == [sender numberOfItems] - 1) || (selected == -1)) {
   611 		NSSavePanel *sp = [NSSavePanel savePanel];
   612 		[sp setRequiredFileType:@"log"];
   613 		[sp setCanSelectHiddenExtension:YES];
   614 
   615 		int runResult = [sp runModalForDirectory:nil file:@""];
   616 		NSString *saveFilename = [sp filename];
   617 		if (runResult == NSFileHandlingPanelOKButton) {
   618 			unsigned saveFilenameIndex = NSNotFound;
   619 			unsigned                 i = CFArrayGetCount(customHistArray);
   620 			if (i) {
   621 				while (--i) {
   622 					if ([(id)CFArrayGetValueAtIndex(customHistArray, i) isEqual:saveFilename]) {
   623 						saveFilenameIndex = i;
   624 						break;
   625 					}
   626 				}
   627 			}
   628 			if (saveFilenameIndex == NSNotFound) {
   629 				if (CFArrayGetCount(customHistArray) == 3U)
   630 					CFArrayRemoveValueAtIndex(customHistArray, 2);
   631 			} else
   632 				CFArrayRemoveValueAtIndex(customHistArray, saveFilenameIndex);
   633 			CFArrayInsertValueAtIndex(customHistArray, 0, saveFilename);
   634 		}
   635 	} else {
   636 		CFStringRef temp = CFRetain(CFArrayGetValueAtIndex(customHistArray, selected));
   637 		CFArrayRemoveValueAtIndex(customHistArray, selected);
   638 		CFArrayInsertValueAtIndex(customHistArray, 0, temp);
   639 		CFRelease(temp);
   640 	}
   641 
   642 	unsigned numHistItems = CFArrayGetCount(customHistArray);
   643 	if (numHistItems) {
   644 		id s = (id)CFArrayGetValueAtIndex(customHistArray, 0);
   645 		[preferencesController setObject:s forKey:GrowlCustomHistKey1];
   646 
   647 		if ((numHistItems > 1U) && (s = (id)CFArrayGetValueAtIndex(customHistArray, 1)))
   648 			[preferencesController setObject:s forKey:GrowlCustomHistKey2];
   649 
   650 		if ((numHistItems > 2U) && (s = (id)CFArrayGetValueAtIndex(customHistArray, 2)))
   651 			[preferencesController setObject:s forKey:GrowlCustomHistKey3];
   652 
   653 		//[[logFileType cellAtRow:1 column:0] setEnabled:YES];
   654 		[logFileType selectCellAtRow:1 column:0];
   655 	}
   656 
   657 	[self updateLogPopupMenu];
   658 }*/
   659 
   660 - (void) updateLogPopupMenu {
   661 	[customMenuButton removeAllItems];
   662 
   663 	int numHistItems = CFArrayGetCount(customHistArray);
   664 	for (int i = 0U; i < numHistItems; i++) {
   665 		NSArray *pathComponentry = [[(NSString *)CFArrayGetValueAtIndex(customHistArray, i) stringByAbbreviatingWithTildeInPath] pathComponents];
   666 		unsigned numPathComponents = [pathComponentry count];
   667 		if (numPathComponents > 2U) {
   668 			unichar ellipsis = 0x2026;
   669 			NSMutableString *arg = [[NSMutableString alloc] initWithCharacters:&ellipsis length:1U];
   670 			[arg appendString:@"/"];
   671 			[arg appendString:[pathComponentry objectAtIndex:(numPathComponents - 2U)]];
   672 			[arg appendString:@"/"];
   673 			[arg appendString:[pathComponentry objectAtIndex:(numPathComponents - 1U)]];
   674 			[customMenuButton insertItemWithTitle:arg atIndex:i];
   675 			[arg release];
   676 		} else
   677 			[customMenuButton insertItemWithTitle:[(NSString *)CFArrayGetValueAtIndex(customHistArray, i) stringByAbbreviatingWithTildeInPath] atIndex:i];
   678 	}
   679 	// No separator if there's no file list yet
   680 	if (numHistItems > 0)
   681 		[[customMenuButton menu] addItem:[NSMenuItem separatorItem]];
   682 	[customMenuButton addItemWithTitle:NSLocalizedStringFromTableInBundle(@"Browse menu item title", nil, [self bundle], nil)];
   683 	//select first item, if any
   684 	[customMenuButton selectItemAtIndex:numHistItems ? 0 : -1];
   685 }
   686 
   687 #pragma mark "Applications" tab pane
   688 
   689 - (BOOL) canRemoveTicket {
   690 	return canRemoveTicket;
   691 }
   692 
   693 - (void) setCanRemoveTicket:(BOOL)flag {
   694 	canRemoveTicket = flag;
   695 }
   696 
   697 - (void) deleteTicket:(id)sender {
   698 #pragma unused(sender)
   699 	NSString *appName = [[[ticketsArrayController selectedObjects] objectAtIndex:0U] applicationName];
   700 	NSAlert *alert = [NSAlert alertWithMessageText:[NSString stringWithFormat:NSLocalizedStringFromTableInBundle(@"Are you sure you want to remove %@?", nil, [self bundle], nil), appName]
   701 									 defaultButton:NSLocalizedStringFromTableInBundle(@"Remove", nil, [self bundle], "Button title for removing something")
   702 								   alternateButton:NSLocalizedStringFromTableInBundle(@"Cancel", nil, [self bundle], "Button title for canceling")
   703 									   otherButton:nil
   704 						 informativeTextWithFormat:[NSString stringWithFormat:
   705 													NSLocalizedStringFromTableInBundle(@"This will remove all Growl settings for %@.", nil, [self bundle], ""), appName]];
   706 	[alert setIcon:[[[NSImage alloc] initWithContentsOfFile:[[self bundle] pathForImageResource:@"growl-icon"]] autorelease]];
   707 	[alert beginSheetModalForWindow:[[NSApplication sharedApplication] keyWindow] modalDelegate:self didEndSelector:@selector(deleteCallbackDidEnd:returnCode:contextInfo:) contextInfo:nil];
   708 }
   709 
   710 // this method is used as our callback to determine whether or not to delete the ticket
   711 -(void) deleteCallbackDidEnd:(NSAlert *)alert returnCode:(int)returnCode contextInfo:(void *)eventID {
   712 #pragma unused(alert)
   713 #pragma unused(eventID)
   714 	if (returnCode == NSAlertDefaultReturn) {
   715 		GrowlApplicationTicket *ticket = [[ticketsArrayController selectedObjects] objectAtIndex:0U];
   716 		NSString *path = [ticket path];
   717 
   718 		if ([[NSFileManager defaultManager] removeFileAtPath:path handler:nil]) {
   719 			CFNumberRef pidValue = CFNumberCreate(kCFAllocatorDefault, kCFNumberIntType, &pid);
   720 			CFStringRef keys[2] = { CFSTR("TicketName"), CFSTR("pid") };
   721 			CFTypeRef   values[2] = { [ticket applicationName], pidValue };
   722 			CFDictionaryRef userInfo = CFDictionaryCreate(kCFAllocatorDefault, (const void **)keys, (const void **)values, 2, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
   723 			CFRelease(pidValue);
   724 			CFNotificationCenterPostNotification(CFNotificationCenterGetDistributedCenter(),
   725 												 (CFStringRef)GrowlPreferencesChanged,
   726 												 CFSTR("GrowlTicketDeleted"),
   727 												 userInfo, false);
   728 			CFRelease(userInfo);
   729 			unsigned idx = [tickets indexOfObject:ticket];
   730 			CFArrayRemoveValueAtIndex(images, idx);
   731 
   732 			unsigned oldSelectionIndex = [ticketsArrayController selectionIndex];
   733 
   734 			///	Hmm... This doesn't work for some reason....
   735 			//	Even though the same method definitely (?) probably works in the appRegistered: method...
   736 
   737 			//	[self removeFromTicketsAtIndex:	[ticketsArrayController selectionIndex]];
   738 
   739 			NSMutableArray *newTickets = [tickets mutableCopy];
   740 			[newTickets removeObject:ticket];
   741 			[self setTickets:newTickets];
   742 			[newTickets release];
   743 
   744 			if (oldSelectionIndex >= [tickets count])
   745 				oldSelectionIndex = [tickets count] - 1;
   746 			[self cacheImages];
   747 			[ticketsArrayController setSelectionIndex:oldSelectionIndex];
   748 		}
   749 	}
   750 }
   751 
   752 -(IBAction)playSound:(id)sender
   753 {
   754 	if([sender indexOfSelectedItem] > 0) // The 0 item is "None"
   755 		[[NSSound soundNamed:[[sender selectedItem] title]] play];
   756 }
   757 
   758 - (IBAction) showApplicationConfigurationTab:(id)sender {
   759 	if ([ticketsArrayController selectionIndex] != NSNotFound) {
   760 		[self populateDisplaysPopUpButton:displayMenuButton nameOfSelectedDisplay:[[ticketsArrayController selection] valueForKey:@"displayPluginName"] includeDefaultMenuItem:YES];
   761 		[self populateDisplaysPopUpButton:notificationDisplayMenuButton nameOfSelectedDisplay:[[notificationsArrayController selection] valueForKey:@"displayPluginName"] includeDefaultMenuItem:YES];
   762 
   763 		[applicationsTab selectLastTabViewItem:sender];
   764 		[configurationTab selectFirstTabViewItem:sender];
   765 	}
   766 }
   767 
   768 - (IBAction) changeNameOfDisplayForApplication:(id)sender {
   769 	NSString *newDisplayPluginName = [[sender selectedItem] representedObject];
   770 	[[ticketsArrayController selectedObjects] setValue:newDisplayPluginName forKey:@"displayPluginName"];
   771 	[self showPreview:sender];
   772 }
   773 - (IBAction) changeNameOfDisplayForNotification:(id)sender {
   774 	NSString *newDisplayPluginName = [[sender selectedItem] representedObject];
   775 	[[notificationsArrayController selectedObjects] setValue:newDisplayPluginName forKey:@"displayPluginName"];
   776 	[self showPreview:sender];
   777 }
   778 
   779 #pragma mark "Display" tab pane
   780 
   781 - (IBAction) showDisabledDisplays:(id)sender {
   782 #pragma unused(sender)
   783 	[disabledDisplaysList setString:[[pluginController disabledPlugins] componentsJoinedByString:@"\n"]];
   784 	
   785 	[NSApp beginSheet:disabledDisplaysSheet 
   786 	   modalForWindow:[[self mainView] window]
   787 		modalDelegate:nil
   788 	   didEndSelector:nil
   789 		  contextInfo:nil];
   790 }
   791 
   792 - (IBAction) endDisabledDisplays:(id)sender {
   793 #pragma unused(sender)
   794 	[NSApp endSheet:disabledDisplaysSheet];
   795 	[disabledDisplaysSheet orderOut:disabledDisplaysSheet];
   796 }
   797 
   798 // Returns a boolean based on whether any disabled displays are present, used for the 'hidden' binding of the button on the tab
   799 - (BOOL)hasDisabledDisplays {
   800 	return [pluginController disabledPluginsPresent];
   801 }
   802 
   803 // Popup buttons that post preview notifications support suppressing the preview with the Option key
   804 - (IBAction) showPreview:(id)sender {
   805 	if(([sender isKindOfClass:[NSPopUpButton class]]) && (GetCurrentKeyModifiers() & optionKey))
   806 		return;
   807 	
   808 	NSDictionary *pluginToUse = currentPlugin;
   809 	NSString *pluginName = nil;
   810 	
   811 	if ([sender isKindOfClass:[NSPopUpButton class]]) {
   812 		NSPopUpButton *popUp = (NSPopUpButton *)sender;
   813 		if (sender == displayMenuButton || sender == notificationDisplayMenuButton)
   814 			pluginName = [[popUp selectedItem] representedObject];
   815 		else
   816 #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
   817 			pluginToUse = [[displayPluginsArrayController content] objectAtIndex:[popUp indexOfSelectedItem]];
   818 	}
   819 
   820 	if (!pluginName)
   821 		pluginName = [pluginToUse objectForKey:GrowlPluginInfoKeyName];
   822 			
   823 	[[NSDistributedNotificationCenter defaultCenter] postNotificationName:GrowlPreview
   824 																   object:pluginName];
   825 }
   826 
   827 - (void) loadViewForDisplay:(NSString *)displayName {
   828 	NSView *newView = nil;
   829 	NSPreferencePane *prefPane = nil, *oldPrefPane = nil;
   830 
   831 	if (pluginPrefPane)
   832 		oldPrefPane = pluginPrefPane;
   833 
   834 	if (displayName) {
   835 		// Old plugins won't support the new protocol. Check first
   836 		if ([currentPluginController respondsToSelector:@selector(preferencePane)])
   837 			prefPane = [currentPluginController preferencePane];
   838 
   839 		if (prefPane == pluginPrefPane) {
   840 			// Don't bother swapping anything
   841 			return;
   842 		} else {
   843 			[pluginPrefPane release];
   844 			pluginPrefPane = [prefPane retain];
   845 			[oldPrefPane willUnselect];
   846 		}
   847 		if (pluginPrefPane) {
   848 			if ([loadedPrefPanes containsObject:pluginPrefPane]) {
   849 				newView = [pluginPrefPane mainView];
   850 			} else {
   851 				newView = [pluginPrefPane loadMainView];
   852 				[loadedPrefPanes addObject:pluginPrefPane];
   853 			}
   854 			[pluginPrefPane willSelect];
   855 		}
   856 	} else {
   857 		[pluginPrefPane release];
   858 		pluginPrefPane = nil;
   859 	}
   860 	if (!newView)
   861 		newView = displayDefaultPrefView;
   862 	if (displayPrefView != newView) {
   863 		// Make sure the new view is framed correctly
   864 		[newView setFrame:DISPLAY_PREF_FRAME];
   865 		[[displayPrefView superview] replaceSubview:displayPrefView with:newView];
   866 		displayPrefView = newView;
   867 
   868 		if (pluginPrefPane) {
   869 			[pluginPrefPane didSelect];
   870 			// Hook up key view chain
   871 			[displayPluginsTable setNextKeyView:[pluginPrefPane firstKeyView]];
   872 			[[pluginPrefPane lastKeyView] setNextKeyView:previewButton];
   873 			//[[displayPluginsTable window] makeFirstResponder:[pluginPrefPane initialKeyView]];
   874 		} else {
   875 			[displayPluginsTable setNextKeyView:previewButton];
   876 		}
   877 
   878 		if (oldPrefPane)
   879 			[oldPrefPane didUnselect];
   880 	}
   881 }
   882 
   883 #pragma mark About Tab
   884 
   885 - (void) setupAboutTab {
   886 	[aboutVersionString setStringValue:[NSString stringWithFormat:@"%@ %@", 
   887 										[[self bundle] objectForInfoDictionaryKey:@"CFBundleName"], 
   888 										[[self bundle] objectForInfoDictionaryKey:@"CFBundleShortVersionString"]]];
   889 	[aboutBoxTextView readRTFDFromFile:[[self bundle] pathForResource:@"About" ofType:@"rtf"]];
   890 }
   891 
   892 - (IBAction) openGrowlWebSite:(id)sender {
   893 #pragma unused(sender)
   894 	[[NSWorkspace sharedWorkspace] openURL:growlWebSiteURL];
   895 }
   896 
   897 - (IBAction) openGrowlForum:(id)sender {
   898 #pragma unused(sender)
   899 	[[NSWorkspace sharedWorkspace] openURL:growlForumURL];
   900 }
   901 
   902 - (IBAction) openGrowlBugSubmissionPage:(id)sender {
   903 #pragma unused(sender)
   904 	[[NSWorkspace sharedWorkspace] openURL:growlBugSubmissionURL];
   905 }
   906 
   907 - (IBAction) openGrowlDonate:(id)sender {
   908  #pragma unused(sender)
   909 	[[NSWorkspace sharedWorkspace] openURL:growlDonateURL];
   910 }
   911 #pragma mark TableView data source methods
   912 
   913 - (int) numberOfRowsInTableView:(NSTableView*)tableView {
   914 	if(tableView == networkTableView) {
   915 		return [[self services] count];
   916 	}
   917 	return 0;
   918 }
   919 - (void) tableViewDidClickInBody:(NSTableView *)tableView {
   920 	activeTableView = tableView;
   921 	[self setCanRemoveTicket:(activeTableView == growlApplications) && [ticketsArrayController canRemove]];
   922 }
   923 
   924 - (void)tableView:(NSTableView *)aTableView setObjectValue:(id)anObject forTableColumn:(NSTableColumn *)aTableColumn row:(int)rowIndex {
   925 #pragma unused(aTableView)
   926 	if(aTableColumn == servicePasswordColumn) {
   927 		[[services objectAtIndex:rowIndex] setPassword:anObject];
   928 	}
   929 
   930 }
   931 
   932 - (id) tableView:(NSTableView *)aTableView objectValueForTableColumn:(NSTableColumn *)aTableColumn row:(int)rowIndex {
   933 #pragma unused(aTableView)
   934 	// we check to make sure we have the image + text column and then set its image manually
   935 	if (aTableColumn == applicationNameAndIconColumn) {
   936 		NSArray *arrangedTickets = [ticketsArrayController arrangedObjects];
   937 		unsigned idx = [tickets indexOfObject:[arrangedTickets objectAtIndex:rowIndex]];
   938 		[[aTableColumn dataCellForRow:rowIndex] setImage:(NSImage *)CFArrayGetValueAtIndex(images,idx)];
   939 	} else if (aTableColumn == servicePasswordColumn) {
   940 		return [[services objectAtIndex:rowIndex] password];
   941 	}
   942 
   943 	return nil;
   944 }
   945 
   946 - (IBAction) tableViewDoubleClick:(id)sender {
   947 	[self showApplicationConfigurationTab:sender];
   948 }
   949 
   950 #pragma mark NSNetServiceBrowser Delegate Methods
   951 
   952 - (void) netServiceBrowser:(NSNetServiceBrowser *)aNetServiceBrowser didFindService:(NSNetService *)aNetService moreComing:(BOOL)moreComing {
   953 #pragma unused(aNetServiceBrowser)
   954 	// check if a computer with this name has already been added
   955 	NSString *name = [aNetService name];
   956 	NSEnumerator *enumerator = [services objectEnumerator];
   957 	GrowlBrowserEntry *entry;
   958 	while ((entry = [enumerator nextObject])) {
   959 		if ([[entry computerName] isEqualToString:name]) {
   960 			[entry setActive:YES];
   961 			return;
   962 		}
   963 	}
   964 
   965 	// don't add the local machine
   966 	CFStringRef localHostName = SCDynamicStoreCopyComputerName(/*store*/ NULL,
   967 															   /*nameEncoding*/ NULL);
   968 	CFComparisonResult isLocalHost = CFStringCompare(localHostName, (CFStringRef)name, 0);
   969 	CFRelease(localHostName);
   970 	if (isLocalHost == kCFCompareEqualTo)
   971 		return;
   972 
   973 	// add a new entry at the end
   974 	entry = [[GrowlBrowserEntry alloc] initWithComputerName:name];
   975 	[self willChangeValueForKey:@"services"];
   976 	[services addObject:entry];
   977 	[self didChangeValueForKey:@"services"];
   978 	[entry release];
   979 
   980 	if (!moreComing)
   981 		[self writeForwardDestinations];
   982 }
   983 
   984 - (void) netServiceBrowser:(NSNetServiceBrowser *)aNetServiceBrowser didRemoveService:(NSNetService *)aNetService moreComing:(BOOL)moreComing {
   985 #pragma unused(aNetServiceBrowser)
   986 	NSEnumerator *serviceEnum = [services objectEnumerator];
   987 	GrowlBrowserEntry *currentEntry;
   988 	NSString *name = [aNetService name];
   989 
   990 	while ((currentEntry = [serviceEnum nextObject])) {
   991 		if ([[currentEntry computerName] isEqualToString:name]) {
   992 			[currentEntry setActive:NO];
   993 			break;
   994 		}
   995 	}
   996 
   997 	if (!moreComing)
   998 		[self writeForwardDestinations];
   999 }
  1000 
  1001 #pragma mark Bonjour
  1002 
  1003 - (void) resolveService:(id)sender {
  1004 	NSLog(@"What calls resolveService:?");
  1005 }
  1006 
  1007 - (NSMutableArray *) services {
  1008 	return services;
  1009 }
  1010 
  1011 - (void) setServices:(NSMutableArray *)theServices {
  1012 	if (theServices != services) {
  1013 		if (theServices) {
  1014 			if (services)
  1015 				[services setArray:theServices];
  1016 			else
  1017 				services = [theServices retain];
  1018 		} else {
  1019 			[services release];
  1020 			services = nil;
  1021 		}
  1022 	}
  1023 }
  1024 
  1025 - (unsigned) countOfServices {
  1026 	return [services count];
  1027 }
  1028 
  1029 - (id) objectInServicesAtIndex:(unsigned)idx {
  1030 	return [services objectAtIndex:idx];
  1031 }
  1032 
  1033 - (void) insertObject:(id)anObject inServicesAtIndex:(unsigned)idx {
  1034 	[services insertObject:anObject atIndex:idx];
  1035 }
  1036 
  1037 - (void) replaceObjectInServicesAtIndex:(unsigned)idx withObject:(id)anObject {
  1038 	[services replaceObjectAtIndex:idx withObject:anObject];
  1039 }
  1040 
  1041 #pragma mark Detecting Growl
  1042 
  1043 - (void) checkGrowlRunning {
  1044 	[self setGrowlIsRunning:[preferencesController isGrowlRunning]];
  1045 	[self updateRunningStatus];
  1046 }
  1047 
  1048 #pragma mark "Display Options" tab pane
  1049 
  1050 - (NSArray *) displayPlugins {
  1051 	return plugins;
  1052 }
  1053 
  1054 - (void) setDisplayPlugins:(NSArray *)thePlugins {
  1055 	if (thePlugins != plugins) {
  1056 		[plugins release];
  1057 		plugins = [thePlugins retain];
  1058 	}
  1059 }
  1060 
  1061 #pragma mark Display pop-up menus
  1062 
  1063 //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.
  1064 - (void) populateDisplaysPopUpButton:(NSPopUpButton *)popUp nameOfSelectedDisplay:(NSString *)nameOfSelectedDisplay includeDefaultMenuItem:(BOOL)includeDefault {
  1065 	NSMenu *menu = [popUp menu];
  1066 	NSString *nameOfDisplay = nil;
  1067 
  1068 	NSMenuItem *selectedItem = nil;
  1069 
  1070 	[popUp removeAllItems];
  1071 
  1072 	if (includeDefault) {
  1073 		NSMenuItem *item = [menu addItemWithTitle:NSLocalizedStringFromTableInBundle(@"Default", nil, [NSBundle bundleForClass:[self class]], /*comment*/ @"Title of menu item for default display")
  1074 										   action:NULL
  1075 									keyEquivalent:@""];
  1076 		[item setRepresentedObject:nil];
  1077 
  1078 		if (!nameOfSelectedDisplay)
  1079 			selectedItem = item;
  1080 
  1081 		[menu addItem:[NSMenuItem separatorItem]];
  1082 	}
  1083 
  1084 	NSEnumerator *displaysEnum = [[plugins sortedArrayUsingSelector:@selector(localizedCaseInsensitiveCompare:)] objectEnumerator];
  1085 	while ((nameOfDisplay = [displaysEnum nextObject])) {
  1086 		NSMenuItem *item = [menu addItemWithTitle:nameOfDisplay
  1087 										   action:NULL
  1088 									keyEquivalent:@""];
  1089 		[item setRepresentedObject:nameOfDisplay];
  1090 
  1091 		if (nameOfSelectedDisplay && [nameOfSelectedDisplay respondsToSelector:@selector(isEqualToString:)] && [nameOfSelectedDisplay isEqualToString:nameOfDisplay])
  1092 			selectedItem = item;
  1093 	}
  1094 
  1095 	[popUp selectItem:selectedItem];
  1096 }
  1097 
  1098 #pragma mark -
  1099 
  1100 /*!
  1101  * @brief Refresh preferences when a new application registers with Growl
  1102  */
  1103 - (void) appRegistered: (NSNotification *) note {
  1104 	NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
  1105 
  1106 	NSString *app = [note object];
  1107 	GrowlApplicationTicket *newTicket = [[GrowlApplicationTicket alloc] initTicketForApplication:app];
  1108 
  1109 	/*
  1110 	 *	Because the tickets array is under KVObservation by the TicketsArrayController
  1111 	 *	We need to remove the ticket using the correct KVC method:
  1112 	 */
  1113 
  1114 	NSEnumerator *ticketEnumerator = [tickets objectEnumerator];
  1115 	GrowlApplicationTicket *ticket;
  1116 	int removalIndex = -1;
  1117 
  1118 	int	i = 0;
  1119 	while ((ticket = [ticketEnumerator nextObject])) {
  1120 		if ([[ticket applicationName] isEqualToString:app]) {
  1121 			removalIndex = i;
  1122 			break;
  1123 		}
  1124 		++i;
  1125 	}
  1126 
  1127 	if (removalIndex != -1)
  1128 		[self removeFromTicketsAtIndex:removalIndex];
  1129 	[self insertInTickets:newTicket];
  1130 	[newTicket release];
  1131 
  1132 	[self cacheImages];
  1133 	
  1134 	[pool release];
  1135 }
  1136 
  1137 - (void) growlLaunched:(NSNotification *)note {
  1138 #pragma unused(note)
  1139 	NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
  1140 
  1141 	[self setGrowlIsRunning:YES];
  1142 	[self updateRunningStatus];
  1143 	
  1144 	[pool release];
  1145 }
  1146 
  1147 - (void) growlTerminated:(NSNotification *)note {
  1148 #pragma unused(note)
  1149 	NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
  1150 
  1151 	[self setGrowlIsRunning:NO];
  1152 	[self updateRunningStatus];
  1153 	
  1154 	[pool release];
  1155 }
  1156 
  1157 @end