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.
2 // GrowlPreferencePane.m
5 // Created by Karl Adam on Wed Apr 21 2004.
6 // Copyright 2004-2006 The Growl Project. All rights reserved.
8 // This file is under the BSD License, refer to License.txt for details
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"
28 #include <Carbon/Carbon.h>
30 #define PING_TIMEOUT 3
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)
35 @interface NSNetService(TigerCompatibility)
37 - (void) resolveWithTimeout:(NSTimeInterval)timeout;
41 @interface GrowlPreferencePane (PRIVATE)
43 - (void) populateDisplaysPopUpButton:(NSPopUpButton *)popUp nameOfSelectedDisplay:(NSString *)nameOfSelectedDisplay includeDefaultMenuItem:(BOOL)includeDefault;
47 @implementation GrowlPreferencePane
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.
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/"]];
62 [NSApp terminate:nil];
65 if ((self = [super initWithBundle:bundle])) {
67 loadedPrefPanes = [[NSMutableArray alloc] init];
68 preferencesController = [GrowlPreferencesController sharedController];
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];
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);
79 if (defaultDefaults) {
80 [preferencesController registerDefaults:defaultDefaults];
81 [defaultDefaults release];
89 [[NSDistributedNotificationCenter defaultCenter] removeObserver:self];
92 [pluginPrefPane release];
93 [loadedPrefPanes release];
96 [currentPlugin release];
97 CFRelease(customHistArray);
98 [versionCheckURL release];
99 [growlWebSiteURL release];
100 [growlForumURL release];
101 [growlBugSubmissionURL release];
102 [growlDonateURL release];
107 - (void) awakeFromNib {
108 ACImageAndTextCell *imageTextCell = [[[ACImageAndTextCell alloc] init] autorelease];
110 [ticketsArrayController addObserver:self forKeyPath:@"selection" options:0 context:nil];
111 [displayPluginsArrayController addObserver:self forKeyPath:@"selection" options:0 context:nil];
113 [self setCanRemoveTicket:NO];
115 browser = [[NSNetServiceBrowser alloc] init];
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];
128 [self setServices:theServices];
129 [theServices release];
131 [browser setDelegate:self];
132 [browser searchForServicesOfType:@"_growl._tcp." inDomain:@""];
134 [self setupAboutTab];
136 if ([preferencesController isGrowlMenuEnabled] && ![GrowlPreferencePane isGrowlMenuRunning])
137 [preferencesController enableGrowlMenu];
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"];
144 customHistArray = CFArrayCreateMutable(kCFAllocatorDefault, 3, &kCFTypeArrayCallBacks);
145 id value = [preferencesController objectForKey:GrowlCustomHistKey1];
147 CFArrayAppendValue(customHistArray, value);
148 value = [preferencesController objectForKey:GrowlCustomHistKey2];
150 CFArrayAppendValue(customHistArray, value);
151 value = [preferencesController objectForKey:GrowlCustomHistKey3];
153 CFArrayAppendValue(customHistArray, value);
156 [self updateLogPopupMenu];
157 int typePref = [preferencesController integerForKey:GrowlLogTypeKey];
158 [logFileType selectCellAtRow:typePref column:0];
160 [growlApplications setDoubleAction:@selector(tableViewDoubleClick:)];
161 [growlApplications setTarget:self];
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];
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];
171 [applicationNameAndIconColumn setDataCell:imageTextCell];
172 [networkTableView reloadData];
174 // Select the default style if possible.
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]) {
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
192 [self performSelector:@selector(selectRow:)
193 withObject:[NSIndexSet indexSetWithIndex:defaultStyleRow]
197 [[NSNotificationCenter defaultCenter] addObserver:self
198 selector:@selector(translateSeparatorsInMenu:)
199 name:NSPopUpButtonWillPopUpNotification
200 object:soundMenuButton];
204 - (void)selectRow:(NSIndexSet *)indexSet
206 [displayPluginsTable selectRowIndexes:indexSet byExtendingSelection:NO];
209 - (void) mainViewDidLoad {
210 [[NSDistributedNotificationCenter defaultCenter] addObserver:self
211 selector:@selector(appRegistered:)
212 name:GROWL_APP_REGISTRATION_CONF
219 * @brief Returns the bundle version of the Growl.prefPane bundle.
221 - (NSString *) bundleVersion {
222 return [[self bundle] objectForInfoDictionaryKey:(NSString *)kCFBundleVersionKey];
226 * @brief Checks if a newer version of Growl is available at the Growl download site.
228 * The version.xml file is a property list which contains version numbers and
229 * download URLs for several components.
231 - (IBAction) checkVersion:(id)sender {
232 #pragma unused(sender)
233 [growlVersionProgress startAnimation:self];
235 if (!versionCheckURL)
236 versionCheckURL = [[NSURL alloc] initWithString:@"http://growl.info/version.xml"];
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];
245 CFURLRef downloadURL = CFURLCreateWithString(kCFAllocatorDefault,
246 (CFStringRef)[productVersionDict objectForKey:[executableName stringByAppendingString:@"DownloadURL"]], NULL);
248 NSLog([[[NSBundle bundleWithIdentifier:GROWL_PREFPANE_BUNDLE_IDENTIFIER] infoDictionary] objectForKey:(NSString *)kCFBundleExecutableKey] );
249 NSLog(currVersionNumber);
250 NSLog(latestVersionNumber);
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, @""),
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], @""));
267 CFRelease(downloadURL);
269 [productVersionDict release];
271 [growlVersionProgress stopAnimation:self];
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);
283 * @brief Returns if GrowlMenu is currently running.
285 + (BOOL) isGrowlMenuRunning {
286 return [[GrowlPreferencesController sharedController] isRunning:@"com.Growl.MenuExtra"];
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];
298 [preferencesController setObject:currentVersion forKey:LastKnownVersionKey];
301 [self checkGrowlRunning];
305 [self reloadPreferences:nil];
309 * @brief copy images to avoid resizing the original images stored in the tickets.
311 - (void) cacheImages {
313 CFArrayRemoveAllValues(images);
315 images = CFArrayCreateMutable(kCFAllocatorDefault, 0, &kCFTypeArrayCallBacks);
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);
328 - (NSMutableArray *) tickets {
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) {
337 [tickets setArray:theTickets];
339 tickets = [theTickets mutableCopy];
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];
348 - (void) removeFromTicketsAtIndex:(int)indexToRemove {
349 NSIndexSet *indices = [NSIndexSet indexSetWithIndex:indexToRemove];
350 [self willChange:NSKeyValueChangeInsertion valuesAtIndexes:indices forKey:@"tickets"];
352 [tickets removeObjectAtIndex:indexToRemove];
354 [self didChange:NSKeyValueChangeInsertion valuesAtIndexes:indices forKey:@"tickets"];
357 - (void) insertInTickets:(GrowlApplicationTicket *)newTicket {
358 NSIndexSet *indices = [NSIndexSet indexSetWithIndex:[tickets count]];
359 [self willChange:NSKeyValueChangeInsertion valuesAtIndexes:indices forKey:@"tickets"];
361 [tickets addObject:newTicket];
363 [self didChange:NSKeyValueChangeInsertion valuesAtIndexes:indices forKey:@"tickets"];
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];
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]];
383 * @brief Called when a distributed GrowlPreferencesChanged notification is received.
385 - (void) reloadPrefs:(NSNotification *)notification {
386 // ignore notifications which are sent by ourselves
387 NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
389 NSNumber *pidValue = [[notification userInfo] objectForKey:@"pid"];
390 if (!pidValue || [pidValue intValue] != pid)
391 [self reloadPreferences:[notification object]];
396 - (void) updatePosition:(NSNotification *)notification {
397 if([notification object] == globalPositionPicker) {
398 [preferencesController setInteger:[globalPositionPicker selectedPosition] forKey:GROWL_POSITION_PREFERENCE_KEY];
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]];
409 * @brief Reloads the preferences and updates the GUI accordingly.
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]];
419 [self setDisplayPlugins:[[GrowlPluginController sharedController] registeredPluginNamesArrayForType:GROWL_VIEW_EXTENSION]];
421 #ifdef THIS_CODE_WAS_REMOVED_AND_I_DONT_KNOW_WHY
422 if (!object || [object isEqualToString:@"GrowlTicketChanged"])
423 [self setTickets:[[ticketController allSavedTickets] allValues]];
425 [preferencesController setSquelchMode:[preferencesController squelchMode]];
426 [preferencesController setGrowlMenuEnabled:[preferencesController isGrowlMenuEnabled]];
431 // If Growl is enabled, ensure the helper app is launched
432 if ([preferencesController boolForKey:GrowlEnabledKey])
433 [preferencesController launchGrowl:NO];
435 if ([plugins count] > 0U)
436 [self reloadDisplayPluginView];
438 [self loadViewForDisplay:nil];
441 - (BOOL) growlIsRunning {
442 return growlIsRunning;
445 - (void) setGrowlIsRunning:(BOOL)flag {
446 growlIsRunning = flag;
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];
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];
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];
483 #pragma mark Bindings accessors (not for programmatic use)
485 - (GrowlPluginController *) pluginController {
486 if (!pluginController)
487 pluginController = [GrowlPluginController sharedController];
489 return pluginController;
491 - (GrowlPreferencesController *) preferencesController {
492 if (!preferencesController)
493 preferencesController = [GrowlPreferencesController sharedController];
495 return preferencesController;
498 - (NSArray *) sounds {
499 NSMutableArray *soundNames = [[NSMutableArray alloc] init];
501 NSArray *paths = [NSArray arrayWithObjects:@"/System/Library/Sounds",
503 [NSString stringWithFormat:@"%@/Library/Sounds", NSHomeDirectory()],
507 NSEnumerator *dirEnumerator = [paths objectEnumerator];
508 while ((directory = [dirEnumerator nextObject])) {
509 BOOL isDirectory = NO;
511 if ([[NSFileManager defaultManager] fileExistsAtPath:directory isDirectory:&isDirectory]) {
513 [soundNames addObject:@"-"];
515 NSArray *files = [[NSFileManager defaultManager] directoryContentsAtPath:directory];
517 NSString *filename = nil;
518 NSEnumerator *fileEnumerator = [files objectEnumerator];
519 while ((filename = [fileEnumerator nextObject])) {
520 NSString *file = [filename stringByDeletingPathExtension];
522 if (![file isEqualToString:@".DS_Store"])
523 [soundNames addObject:file];
529 return [soundNames autorelease];
532 - (void)translateSeparatorsInMenu:(NSNotification *)notification
534 NSPopUpButton * button = [notification object];
536 NSMenu *menu = [button menu];
540 while ((itemIndex = [menu indexOfItemWithTitle:@"-"]) != -1) {
541 [menu removeItemAtIndex:itemIndex];
542 [menu insertItem:[NSMenuItem separatorItem] atIndex:itemIndex];
546 #pragma mark Growl running state
549 * @brief Launches GrowlHelperApp.
551 - (void) launchGrowl {
552 // Don't allow the button to be clicked while we update
553 [startStopGrowl setEnabled:NO];
554 [growlRunningProgress startAnimation:self];
556 // Update our status visible to the user
557 [growlRunningStatus setStringValue:NSLocalizedStringFromTableInBundle(@"Launching Growl...",nil,[self bundle],@"")];
559 [preferencesController setGrowlRunning:YES noMatterWhat:NO];
561 // After 4 seconds force a status update, in case Growl didn't start/stop
562 [self performSelector:@selector(checkGrowlRunning)
568 * @brief Terminates running GrowlHelperApp instances.
570 - (void) terminateGrowl {
571 // Don't allow the button to be clicked while we update
572 [startStopGrowl setEnabled:NO];
573 [growlRunningProgress startAnimation:self];
575 // Update our status visible to the user
576 [growlRunningStatus setStringValue:NSLocalizedStringFromTableInBundle(@"Terminating Growl...",nil,[self bundle],@"")];
578 // Ask the Growl Helper App to shutdown
579 [preferencesController setGrowlRunning:NO noMatterWhat:NO];
581 // After 4 seconds force a status update, in case growl didn't start/stop
582 [self performSelector:@selector(checkGrowlRunning)
587 #pragma mark "General" tab pane
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];
599 // Our desired state is a toggle of the current state;
601 [self terminateGrowl];
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];
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);
622 if ([(id)CFArrayGetValueAtIndex(customHistArray, i) isEqual:saveFilename]) {
623 saveFilenameIndex = i;
628 if (saveFilenameIndex == NSNotFound) {
629 if (CFArrayGetCount(customHistArray) == 3U)
630 CFArrayRemoveValueAtIndex(customHistArray, 2);
632 CFArrayRemoveValueAtIndex(customHistArray, saveFilenameIndex);
633 CFArrayInsertValueAtIndex(customHistArray, 0, saveFilename);
636 CFStringRef temp = CFRetain(CFArrayGetValueAtIndex(customHistArray, selected));
637 CFArrayRemoveValueAtIndex(customHistArray, selected);
638 CFArrayInsertValueAtIndex(customHistArray, 0, temp);
642 unsigned numHistItems = CFArrayGetCount(customHistArray);
644 id s = (id)CFArrayGetValueAtIndex(customHistArray, 0);
645 [preferencesController setObject:s forKey:GrowlCustomHistKey1];
647 if ((numHistItems > 1U) && (s = (id)CFArrayGetValueAtIndex(customHistArray, 1)))
648 [preferencesController setObject:s forKey:GrowlCustomHistKey2];
650 if ((numHistItems > 2U) && (s = (id)CFArrayGetValueAtIndex(customHistArray, 2)))
651 [preferencesController setObject:s forKey:GrowlCustomHistKey3];
653 //[[logFileType cellAtRow:1 column:0] setEnabled:YES];
654 [logFileType selectCellAtRow:1 column:0];
657 [self updateLogPopupMenu];
660 - (void) updateLogPopupMenu {
661 [customMenuButton removeAllItems];
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];
677 [customMenuButton insertItemWithTitle:[(NSString *)CFArrayGetValueAtIndex(customHistArray, i) stringByAbbreviatingWithTildeInPath] atIndex:i];
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];
687 #pragma mark "Applications" tab pane
689 - (BOOL) canRemoveTicket {
690 return canRemoveTicket;
693 - (void) setCanRemoveTicket:(BOOL)flag {
694 canRemoveTicket = flag;
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")
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];
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];
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);
724 CFNotificationCenterPostNotification(CFNotificationCenterGetDistributedCenter(),
725 (CFStringRef)GrowlPreferencesChanged,
726 CFSTR("GrowlTicketDeleted"),
729 unsigned idx = [tickets indexOfObject:ticket];
730 CFArrayRemoveValueAtIndex(images, idx);
732 unsigned oldSelectionIndex = [ticketsArrayController selectionIndex];
734 /// Hmm... This doesn't work for some reason....
735 // Even though the same method definitely (?) probably works in the appRegistered: method...
737 // [self removeFromTicketsAtIndex: [ticketsArrayController selectionIndex]];
739 NSMutableArray *newTickets = [tickets mutableCopy];
740 [newTickets removeObject:ticket];
741 [self setTickets:newTickets];
742 [newTickets release];
744 if (oldSelectionIndex >= [tickets count])
745 oldSelectionIndex = [tickets count] - 1;
747 [ticketsArrayController setSelectionIndex:oldSelectionIndex];
752 -(IBAction)playSound:(id)sender
754 if([sender indexOfSelectedItem] > 0) // The 0 item is "None"
755 [[NSSound soundNamed:[[sender selectedItem] title]] play];
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];
763 [applicationsTab selectLastTabViewItem:sender];
764 [configurationTab selectFirstTabViewItem:sender];
768 - (IBAction) changeNameOfDisplayForApplication:(id)sender {
769 NSString *newDisplayPluginName = [[sender selectedItem] representedObject];
770 [[ticketsArrayController selectedObjects] setValue:newDisplayPluginName forKey:@"displayPluginName"];
771 [self showPreview:sender];
773 - (IBAction) changeNameOfDisplayForNotification:(id)sender {
774 NSString *newDisplayPluginName = [[sender selectedItem] representedObject];
775 [[notificationsArrayController selectedObjects] setValue:newDisplayPluginName forKey:@"displayPluginName"];
776 [self showPreview:sender];
779 #pragma mark "Display" tab pane
781 - (IBAction) showDisabledDisplays:(id)sender {
782 #pragma unused(sender)
783 [disabledDisplaysList setString:[[pluginController disabledPlugins] componentsJoinedByString:@"\n"]];
785 [NSApp beginSheet:disabledDisplaysSheet
786 modalForWindow:[[self mainView] window]
792 - (IBAction) endDisabledDisplays:(id)sender {
793 #pragma unused(sender)
794 [NSApp endSheet:disabledDisplaysSheet];
795 [disabledDisplaysSheet orderOut:disabledDisplaysSheet];
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];
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))
808 NSDictionary *pluginToUse = currentPlugin;
809 NSString *pluginName = nil;
811 if ([sender isKindOfClass:[NSPopUpButton class]]) {
812 NSPopUpButton *popUp = (NSPopUpButton *)sender;
813 if (sender == displayMenuButton || sender == notificationDisplayMenuButton)
814 pluginName = [[popUp selectedItem] representedObject];
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]];
821 pluginName = [pluginToUse objectForKey:GrowlPluginInfoKeyName];
823 [[NSDistributedNotificationCenter defaultCenter] postNotificationName:GrowlPreview
827 - (void) loadViewForDisplay:(NSString *)displayName {
828 NSView *newView = nil;
829 NSPreferencePane *prefPane = nil, *oldPrefPane = nil;
832 oldPrefPane = pluginPrefPane;
835 // Old plugins won't support the new protocol. Check first
836 if ([currentPluginController respondsToSelector:@selector(preferencePane)])
837 prefPane = [currentPluginController preferencePane];
839 if (prefPane == pluginPrefPane) {
840 // Don't bother swapping anything
843 [pluginPrefPane release];
844 pluginPrefPane = [prefPane retain];
845 [oldPrefPane willUnselect];
847 if (pluginPrefPane) {
848 if ([loadedPrefPanes containsObject:pluginPrefPane]) {
849 newView = [pluginPrefPane mainView];
851 newView = [pluginPrefPane loadMainView];
852 [loadedPrefPanes addObject:pluginPrefPane];
854 [pluginPrefPane willSelect];
857 [pluginPrefPane release];
858 pluginPrefPane = nil;
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;
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]];
875 [displayPluginsTable setNextKeyView:previewButton];
879 [oldPrefPane didUnselect];
883 #pragma mark About Tab
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"]];
892 - (IBAction) openGrowlWebSite:(id)sender {
893 #pragma unused(sender)
894 [[NSWorkspace sharedWorkspace] openURL:growlWebSiteURL];
897 - (IBAction) openGrowlForum:(id)sender {
898 #pragma unused(sender)
899 [[NSWorkspace sharedWorkspace] openURL:growlForumURL];
902 - (IBAction) openGrowlBugSubmissionPage:(id)sender {
903 #pragma unused(sender)
904 [[NSWorkspace sharedWorkspace] openURL:growlBugSubmissionURL];
907 - (IBAction) openGrowlDonate:(id)sender {
908 #pragma unused(sender)
909 [[NSWorkspace sharedWorkspace] openURL:growlDonateURL];
911 #pragma mark TableView data source methods
913 - (int) numberOfRowsInTableView:(NSTableView*)tableView {
914 if(tableView == networkTableView) {
915 return [[self services] count];
919 - (void) tableViewDidClickInBody:(NSTableView *)tableView {
920 activeTableView = tableView;
921 [self setCanRemoveTicket:(activeTableView == growlApplications) && [ticketsArrayController canRemove]];
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];
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];
946 - (IBAction) tableViewDoubleClick:(id)sender {
947 [self showApplicationConfigurationTab:sender];
950 #pragma mark NSNetServiceBrowser Delegate Methods
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];
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)
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"];
981 [self writeForwardDestinations];
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];
990 while ((currentEntry = [serviceEnum nextObject])) {
991 if ([[currentEntry computerName] isEqualToString:name]) {
992 [currentEntry setActive:NO];
998 [self writeForwardDestinations];
1001 #pragma mark Bonjour
1003 - (void) resolveService:(id)sender {
1004 NSLog(@"What calls resolveService:?");
1007 - (NSMutableArray *) services {
1011 - (void) setServices:(NSMutableArray *)theServices {
1012 if (theServices != services) {
1015 [services setArray:theServices];
1017 services = [theServices retain];
1025 - (unsigned) countOfServices {
1026 return [services count];
1029 - (id) objectInServicesAtIndex:(unsigned)idx {
1030 return [services objectAtIndex:idx];
1033 - (void) insertObject:(id)anObject inServicesAtIndex:(unsigned)idx {
1034 [services insertObject:anObject atIndex:idx];
1037 - (void) replaceObjectInServicesAtIndex:(unsigned)idx withObject:(id)anObject {
1038 [services replaceObjectAtIndex:idx withObject:anObject];
1041 #pragma mark Detecting Growl
1043 - (void) checkGrowlRunning {
1044 [self setGrowlIsRunning:[preferencesController isGrowlRunning]];
1045 [self updateRunningStatus];
1048 #pragma mark "Display Options" tab pane
1050 - (NSArray *) displayPlugins {
1054 - (void) setDisplayPlugins:(NSArray *)thePlugins {
1055 if (thePlugins != plugins) {
1057 plugins = [thePlugins retain];
1061 #pragma mark Display pop-up menus
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;
1068 NSMenuItem *selectedItem = nil;
1070 [popUp removeAllItems];
1072 if (includeDefault) {
1073 NSMenuItem *item = [menu addItemWithTitle:NSLocalizedStringFromTableInBundle(@"Default", nil, [NSBundle bundleForClass:[self class]], /*comment*/ @"Title of menu item for default display")
1076 [item setRepresentedObject:nil];
1078 if (!nameOfSelectedDisplay)
1079 selectedItem = item;
1081 [menu addItem:[NSMenuItem separatorItem]];
1084 NSEnumerator *displaysEnum = [[plugins sortedArrayUsingSelector:@selector(localizedCaseInsensitiveCompare:)] objectEnumerator];
1085 while ((nameOfDisplay = [displaysEnum nextObject])) {
1086 NSMenuItem *item = [menu addItemWithTitle:nameOfDisplay
1089 [item setRepresentedObject:nameOfDisplay];
1091 if (nameOfSelectedDisplay && [nameOfSelectedDisplay respondsToSelector:@selector(isEqualToString:)] && [nameOfSelectedDisplay isEqualToString:nameOfDisplay])
1092 selectedItem = item;
1095 [popUp selectItem:selectedItem];
1101 * @brief Refresh preferences when a new application registers with Growl
1103 - (void) appRegistered: (NSNotification *) note {
1104 NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
1106 NSString *app = [note object];
1107 GrowlApplicationTicket *newTicket = [[GrowlApplicationTicket alloc] initTicketForApplication:app];
1110 * Because the tickets array is under KVObservation by the TicketsArrayController
1111 * We need to remove the ticket using the correct KVC method:
1114 NSEnumerator *ticketEnumerator = [tickets objectEnumerator];
1115 GrowlApplicationTicket *ticket;
1116 int removalIndex = -1;
1119 while ((ticket = [ticketEnumerator nextObject])) {
1120 if ([[ticket applicationName] isEqualToString:app]) {
1127 if (removalIndex != -1)
1128 [self removeFromTicketsAtIndex:removalIndex];
1129 [self insertInTickets:newTicket];
1130 [newTicket release];
1137 - (void) growlLaunched:(NSNotification *)note {
1138 #pragma unused(note)
1139 NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
1141 [self setGrowlIsRunning:YES];
1142 [self updateRunningStatus];
1147 - (void) growlTerminated:(NSNotification *)note {
1148 #pragma unused(note)
1149 NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
1151 [self setGrowlIsRunning:NO];
1152 [self updateRunningStatus];