Updated the Growl Bug Submission button to go to our new bug-reporting page (http://growl.info/reportabug.php) instead of our long-deceased Trac. Fixes #301955.
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 @implementation GrowlPreferencePane
43 - (id) initWithBundle:(NSBundle *)bundle {
44 // Check that we're running Panther
45 // if a user with a previous OS version tries to launch us - switch out the pane.
47 NSApp = [NSApplication sharedApplication];
48 if (![NSApp respondsToSelector:@selector(replyToOpenOrPrint:)]) {
49 if (NSRunInformationalAlertPanel(NSLocalizedStringFromTableInBundle(@"System requirements not met", nil, bundle, "Title for the dialogue shown if attempting to run Growl on 10.2 or earlier"),
50 NSLocalizedStringFromTableInBundle(@"Mac OS X 10.3 \"Panther\" or greater is required.", nil, bundle, nil),
51 NSLocalizedStringFromTableInBundle(@"Quit", nil, bundle, "Quit button title"),
52 NSLocalizedStringFromTableInBundle(@"Upgrade Mac OS X...", nil, bundle, "Button title"),
53 nil) == NSAlertAlternateReturn) {
54 [[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:@"http://www.apple.com/macosx/"]];
56 [NSApp terminate:nil];
59 if ((self = [super initWithBundle:bundle])) {
61 loadedPrefPanes = [[NSMutableArray alloc] init];
62 preferencesController = [GrowlPreferencesController sharedController];
64 NSNotificationCenter *nc = [NSDistributedNotificationCenter defaultCenter];
65 [nc addObserver:self selector:@selector(growlLaunched:) name:GROWL_IS_READY object:nil];
66 [nc addObserver:self selector:@selector(growlTerminated:) name:GROWL_SHUTDOWN object:nil];
67 [nc addObserver:self selector:@selector(reloadPrefs:) name:GrowlPreferencesChanged object:nil];
69 CFStringRef file = (CFStringRef)[bundle pathForResource:@"GrowlDefaults" ofType:@"plist"];
70 CFURLRef fileURL = CFURLCreateWithFileSystemPath(kCFAllocatorDefault, file, kCFURLPOSIXPathStyle, /*isDirectory*/ false);
71 NSDictionary *defaultDefaults = (NSDictionary *)createPropertyListFromURL((NSURL *)fileURL, kCFPropertyListImmutable, NULL, NULL);
73 if (defaultDefaults) {
74 [preferencesController registerDefaults:defaultDefaults];
75 [defaultDefaults release];
83 [[NSDistributedNotificationCenter defaultCenter] removeObserver:self];
86 [pluginPrefPane release];
87 [loadedPrefPanes release];
90 [currentPlugin release];
91 CFRelease(customHistArray);
92 [versionCheckURL release];
93 [growlWebSiteURL release];
94 [growlForumURL release];
95 [growlBugSubmissionURL release];
96 [growlDonateURL release];
101 - (void) awakeFromNib {
102 ACImageAndTextCell *imageTextCell = [[[ACImageAndTextCell alloc] init] autorelease];
104 [ticketsArrayController addObserver:self forKeyPath:@"selection" options:0 context:nil];
105 [displayPluginsArrayController addObserver:self forKeyPath:@"selection" options:0 context:nil];
107 [self setCanRemoveTicket:NO];
109 browser = [[NSNetServiceBrowser alloc] init];
111 // create a deep mutable copy of the forward destinations
112 NSArray *destinations = [preferencesController objectForKey:GrowlForwardDestinationsKey];
113 NSEnumerator *destEnum = [destinations objectEnumerator];
114 NSMutableArray *theServices = [[NSMutableArray alloc] initWithCapacity:[destinations count]];
115 NSDictionary *destination;
116 while ((destination = [destEnum nextObject])) {
117 GrowlBrowserEntry *entry = [[GrowlBrowserEntry alloc] initWithDictionary:destination];
118 [entry setOwner:self];
119 [theServices addObject:entry];
122 [self setServices:theServices];
123 [theServices release];
125 [browser setDelegate:self];
126 [browser searchForServicesOfType:@"_growl._tcp." inDomain:@""];
128 [self setupAboutTab];
130 if ([preferencesController isGrowlMenuEnabled] && ![GrowlPreferencePane isGrowlMenuRunning])
131 [preferencesController enableGrowlMenu];
133 growlWebSiteURL = [[NSURL alloc] initWithString:@"http://growl.info"];
134 growlForumURL = [[NSURL alloc] initWithString:@"http://forums.cocoaforge.com/viewforum.php?f=6"];
135 growlBugSubmissionURL = [[NSURL alloc] initWithString:@"http://growl.info/reportabug.php"];
136 growlDonateURL = [[NSURL alloc] initWithString:@"http://growl.info/donate.php"];
138 customHistArray = CFArrayCreateMutable(kCFAllocatorDefault, 3, &kCFTypeArrayCallBacks);
139 id value = [preferencesController objectForKey:GrowlCustomHistKey1];
141 CFArrayAppendValue(customHistArray, value);
142 value = [preferencesController objectForKey:GrowlCustomHistKey2];
144 CFArrayAppendValue(customHistArray, value);
145 value = [preferencesController objectForKey:GrowlCustomHistKey3];
147 CFArrayAppendValue(customHistArray, value);
150 [self updateLogPopupMenu];
151 int typePref = [preferencesController integerForKey:GrowlLogTypeKey];
152 [logFileType selectCellAtRow:typePref column:0];
154 [growlApplications setDoubleAction:@selector(tableViewDoubleClick:)];
155 [growlApplications setTarget:self];
157 // bind the global position picker programmatically since its a custom view, register for notification so we can handle updating manually
158 [globalPositionPicker bind:@"selectedPosition" toObject:preferencesController withKeyPath:@"selectedPosition" options:nil];
159 [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(updatePosition:) name:GrowlPositionPickerChangedSelectionNotification object:globalPositionPicker];
161 // bind the app level position picker programmatically since its a custom view, register for notification so we can handle updating manually
162 [appPositionPicker bind:@"selectedPosition" toObject:ticketsArrayController withKeyPath:@"selection.selectedPosition" options:nil];
163 [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(updatePosition:) name:GrowlPositionPickerChangedSelectionNotification object:appPositionPicker];
165 [applicationNameAndIconColumn setDataCell:imageTextCell];
166 [networkTableView reloadData];
168 // Select the default style if possible.
170 id arrangedObjects = [displayPluginsArrayController arrangedObjects];
171 int count = [arrangedObjects count];
172 NSString *defaultDisplayPluginName = [[self preferencesController] defaultDisplayPluginName];
173 int defaultStyleRow = NSNotFound;
174 for (int i = 0; i < count; i++) {
175 if ([[[arrangedObjects objectAtIndex:i] valueForKey:@"CFBundleName"] isEqualToString:defaultDisplayPluginName]) {
181 if (defaultStyleRow != NSNotFound) {
182 /* Wait until the next run loop; otherwise everything isn't finished loading and we throw an exception.
183 * This is setting the view for the Displays tab, which isn't initially visible, so the user won't see
184 * the flicker. I'm don't know why this is necessary. -evands
186 [self performSelector:@selector(selectRow:)
187 withObject:[NSIndexSet indexSetWithIndex:defaultStyleRow]
191 [[NSNotificationCenter defaultCenter] addObserver:self
192 selector:@selector(translateSeparatorsInMenu:)
193 name:NSPopUpButtonWillPopUpNotification
194 object:soundMenuButton];
196 [[NSNotificationCenter defaultCenter] addObserver:self
197 selector:@selector(translateSeparatorsInMenu:)
198 name:NSPopUpButtonWillPopUpNotification
199 object:displayMenuButton];
201 [[NSNotificationCenter defaultCenter] addObserver:self
202 selector:@selector(translateSeparatorsInMenu:)
203 name:NSPopUpButtonWillPopUpNotification
204 object:notificationDisplayMenuButton];
208 - (void)selectRow:(NSIndexSet *)indexSet
210 [displayPluginsTable selectRowIndexes:indexSet byExtendingSelection:NO];
213 - (void) mainViewDidLoad {
214 [[NSDistributedNotificationCenter defaultCenter] addObserver:self
215 selector:@selector(appRegistered:)
216 name:GROWL_APP_REGISTRATION_CONF
223 * @brief Returns the bundle version of the Growl.prefPane bundle.
225 - (NSString *) bundleVersion {
226 return [[self bundle] objectForInfoDictionaryKey:(NSString *)kCFBundleVersionKey];
230 * @brief Checks if a newer version of Growl is available at the Growl download site.
232 * The version.xml file is a property list which contains version numbers and
233 * download URLs for several components.
235 - (IBAction) checkVersion:(id)sender {
236 #pragma unused(sender)
237 [growlVersionProgress startAnimation:self];
239 if (!versionCheckURL)
240 versionCheckURL = [[NSURL alloc] initWithString:@"http://growl.info/version.xml"];
242 NSBundle *bundle = [self bundle];
243 NSDictionary *infoDict = [bundle infoDictionary];
244 NSString *currVersionNumber = [infoDict objectForKey:(NSString *)kCFBundleVersionKey];
245 NSDictionary *productVersionDict = [[NSDictionary alloc] initWithContentsOfURL:versionCheckURL];
246 NSString *executableName = [infoDict objectForKey:(NSString *)kCFBundleExecutableKey];
247 NSString *latestVersionNumber = [productVersionDict objectForKey:executableName];
249 CFURLRef downloadURL = CFURLCreateWithString(kCFAllocatorDefault,
250 (CFStringRef)[productVersionDict objectForKey:[executableName stringByAppendingString:@"DownloadURL"]], NULL);
252 NSLog([[[NSBundle bundleWithIdentifier:GROWL_PREFPANE_BUNDLE_IDENTIFIER] infoDictionary] objectForKey:(NSString *)kCFBundleExecutableKey] );
253 NSLog(currVersionNumber);
254 NSLog(latestVersionNumber);
257 // do nothing--be quiet if there is no active connection or if the
258 // version number could not be downloaded
259 if (latestVersionNumber && (compareVersionStringsTranslating1_0To0_5(latestVersionNumber, currVersionNumber) > 0))
260 NSBeginAlertSheet(/*title*/ NSLocalizedStringFromTableInBundle(@"Update Available", nil, bundle, @""),
261 /*defaultButton*/ nil, // use default localized button title ("OK" in English)
262 /*alternateButton*/ NSLocalizedStringFromTableInBundle(@"Cancel", nil, bundle, @""),
265 /*modalDelegate*/ self,
266 /*didEndSelector*/ NULL,
267 /*didDismissSelector*/ @selector(downloadSelector:returnCode:contextInfo:),
268 /*contextInfo*/ (void *)downloadURL,
269 /*msg*/ NSLocalizedStringFromTableInBundle(@"A newer version of Growl is available online. Would you like to download it now?", nil, [self bundle], @""));
271 CFRelease(downloadURL);
273 [productVersionDict release];
275 [growlVersionProgress stopAnimation:self];
278 - (void) downloadSelector:(NSWindow *)sheet returnCode:(int)returnCode contextInfo:(void *)contextInfo {
279 #pragma unused(sheet)
280 CFURLRef downloadURL = (CFURLRef)contextInfo;
281 if (returnCode == NSAlertDefaultReturn)
282 [[NSWorkspace sharedWorkspace] openURL:(NSURL *)downloadURL];
283 CFRelease(downloadURL);
287 * @brief Returns if GrowlMenu is currently running.
289 + (BOOL) isGrowlMenuRunning {
290 return [[GrowlPreferencesController sharedController] isRunning:@"com.Growl.MenuExtra"];
293 //subclassed from NSPreferencePane; called before the pane is displayed.
294 - (void) willSelect {
295 NSString *lastVersion = [preferencesController objectForKey:LastKnownVersionKey];
296 NSString *currentVersion = [self bundleVersion];
297 if (!(lastVersion && [lastVersion isEqualToString:currentVersion])) {
298 if ([preferencesController isGrowlRunning]) {
299 [preferencesController setGrowlRunning:NO noMatterWhat:NO];
300 [preferencesController setGrowlRunning:YES noMatterWhat:YES];
302 [preferencesController setObject:currentVersion forKey:LastKnownVersionKey];
305 [self checkGrowlRunning];
309 [self reloadPreferences:nil];
313 * @brief copy images to avoid resizing the original images stored in the tickets.
315 - (void) cacheImages {
317 CFArrayRemoveAllValues(images);
319 images = CFArrayCreateMutable(kCFAllocatorDefault, 0, &kCFTypeArrayCallBacks);
321 NSEnumerator *enumerator = [[ticketsArrayController content] objectEnumerator];
322 GrowlApplicationTicket *ticket;
323 while ((ticket = [enumerator nextObject])) {
324 NSImage *icon = [[ticket icon] copy];
325 [icon setScalesWhenResized:YES];
326 [icon setSize:NSMakeSize(32.0f, 32.0f)];
327 CFArrayAppendValue(images, icon);
332 - (NSMutableArray *) tickets {
336 //using setTickets: will tip off the controller (KVO).
337 //use this to set the tickets secretly.
338 - (void) setTicketsWithoutTellingAnybody:(NSArray *)theTickets {
339 if (theTickets != tickets) {
341 [tickets setArray:theTickets];
343 tickets = [theTickets mutableCopy];
347 //we don't need to do any special extra magic here - just being setTickets: is enough to tip off the controller.
348 - (void) setTickets:(NSArray *)theTickets {
349 [self setTicketsWithoutTellingAnybody:theTickets];
352 - (void) removeFromTicketsAtIndex:(int)indexToRemove {
353 NSIndexSet *indices = [NSIndexSet indexSetWithIndex:indexToRemove];
354 [self willChange:NSKeyValueChangeInsertion valuesAtIndexes:indices forKey:@"tickets"];
356 [tickets removeObjectAtIndex:indexToRemove];
358 [self didChange:NSKeyValueChangeInsertion valuesAtIndexes:indices forKey:@"tickets"];
361 - (void) insertInTickets:(GrowlApplicationTicket *)newTicket {
362 NSIndexSet *indices = [NSIndexSet indexSetWithIndex:[tickets count]];
363 [self willChange:NSKeyValueChangeInsertion valuesAtIndexes:indices forKey:@"tickets"];
365 [tickets addObject:newTicket];
367 [self didChange:NSKeyValueChangeInsertion valuesAtIndexes:indices forKey:@"tickets"];
370 - (void) reloadDisplayPluginView {
371 NSArray *selectedPlugins = [displayPluginsArrayController selectedObjects];
372 unsigned numPlugins = [plugins count];
373 [currentPlugin release];
374 if (numPlugins > 0U && selectedPlugins && [selectedPlugins count] > 0U)
375 currentPlugin = [[selectedPlugins objectAtIndex:0U] retain];
379 NSString *currentPluginName = [currentPlugin objectForKey:(NSString *)kCFBundleNameKey];
380 currentPluginController = (GrowlPlugin *)[pluginController pluginInstanceWithName:currentPluginName];
381 [self loadViewForDisplay:currentPluginName];
382 [displayAuthor setStringValue:[currentPlugin objectForKey:@"GrowlPluginAuthor"]];
383 [displayVersion setStringValue:[currentPlugin objectForKey:(NSString *)kCFBundleNameKey]];
387 * @brief Called when a distributed GrowlPreferencesChanged notification is received.
389 - (void) reloadPrefs:(NSNotification *)notification {
390 // ignore notifications which are sent by ourselves
391 NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
393 NSNumber *pidValue = [[notification userInfo] objectForKey:@"pid"];
394 if (!pidValue || [pidValue intValue] != pid)
395 [self reloadPreferences:[notification object]];
400 - (void) updatePosition:(NSNotification *)notification {
401 if([notification object] == globalPositionPicker) {
402 [preferencesController setInteger:[globalPositionPicker selectedPosition] forKey:GROWL_POSITION_PREFERENCE_KEY];
404 else if([notification object] == appPositionPicker) {
405 // a cheap hack around selection not providing a workable object
406 NSArray *selection = [ticketsArrayController selectedObjects];
407 if ([selection count] > 0)
408 [[selection objectAtIndex:0] setSelectedPosition:[appPositionPicker selectedPosition]];
413 * @brief Reloads the preferences and updates the GUI accordingly.
415 - (void) reloadPreferences:(NSString *)object {
416 if (!object || [object isEqualToString:@"GrowlTicketChanged"]) {
417 GrowlTicketController *ticketController = [GrowlTicketController sharedController];
418 [ticketController loadAllSavedTickets];
419 [self setTickets:[[ticketController allSavedTickets] allValues]];
423 [self setDisplayPlugins:[[GrowlPluginController sharedController] registeredPluginNamesArrayForType:GROWL_VIEW_EXTENSION]];
425 #ifdef THIS_CODE_WAS_REMOVED_AND_I_DONT_KNOW_WHY
426 if (!object || [object isEqualToString:@"GrowlTicketChanged"])
427 [self setTickets:[[ticketController allSavedTickets] allValues]];
429 [preferencesController setSquelchMode:[preferencesController squelchMode]];
430 [preferencesController setGrowlMenuEnabled:[preferencesController isGrowlMenuEnabled]];
435 // If Growl is enabled, ensure the helper app is launched
436 if ([preferencesController boolForKey:GrowlEnabledKey])
437 [preferencesController launchGrowl:NO];
439 if ([plugins count] > 0U)
440 [self reloadDisplayPluginView];
442 [self loadViewForDisplay:nil];
445 - (BOOL) growlIsRunning {
446 return growlIsRunning;
449 - (void) setGrowlIsRunning:(BOOL)flag {
450 growlIsRunning = flag;
453 - (void) updateRunningStatus {
454 [startStopGrowl setEnabled:YES];
455 NSBundle *bundle = [self bundle];
456 [startStopGrowl setTitle:
457 growlIsRunning ? NSLocalizedStringFromTableInBundle(@"Stop Growl",nil,bundle,@"")
458 : NSLocalizedStringFromTableInBundle(@"Start Growl",nil,bundle,@"")];
459 [growlRunningStatus setStringValue:
460 growlIsRunning ? NSLocalizedStringFromTableInBundle(@"Growl is running.",nil,bundle,@"")
461 : NSLocalizedStringFromTableInBundle(@"Growl is stopped.",nil,bundle,@"")];
462 [growlRunningProgress stopAnimation:self];
465 - (void) observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object
466 change:(NSDictionary *)change context:(void *)context {
467 #pragma unused(change, context)
468 if ([keyPath isEqualToString:@"selection"]) {
469 if ((object == ticketsArrayController))
470 [self setCanRemoveTicket:(activeTableView == growlApplications) && [ticketsArrayController canRemove]];
471 else if (object == displayPluginsArrayController)
472 [self reloadDisplayPluginView];
476 - (void) writeForwardDestinations {
477 NSMutableArray *destinations = [[NSMutableArray alloc] initWithCapacity:[services count]];
478 NSEnumerator *enumerator = [services objectEnumerator];
479 GrowlBrowserEntry *entry;
480 while ((entry = [enumerator nextObject]))
481 [destinations addObject:[entry properties]];
482 [preferencesController setObject:destinations forKey:GrowlForwardDestinationsKey];
483 [destinations release];
487 #pragma mark Bindings accessors (not for programmatic use)
489 - (GrowlPluginController *) pluginController {
490 if (!pluginController)
491 pluginController = [GrowlPluginController sharedController];
493 return pluginController;
495 - (GrowlPreferencesController *) preferencesController {
496 if (!preferencesController)
497 preferencesController = [GrowlPreferencesController sharedController];
499 return preferencesController;
502 - (NSArray *) sounds {
503 NSMutableArray *soundNames = [[NSMutableArray alloc] init];
505 NSArray *paths = [NSArray arrayWithObjects:@"/System/Library/Sounds",
507 [NSString stringWithFormat:@"%@/Library/Sounds", NSHomeDirectory()],
511 NSEnumerator *dirEnumerator = [paths objectEnumerator];
512 while ((directory = [dirEnumerator nextObject])) {
513 BOOL isDirectory = NO;
515 if ([[NSFileManager defaultManager] fileExistsAtPath:directory isDirectory:&isDirectory]) {
517 [soundNames addObject:@"-"];
519 NSArray *files = [[NSFileManager defaultManager] directoryContentsAtPath:directory];
521 NSString *filename = nil;
522 NSEnumerator *fileEnumerator = [files objectEnumerator];
523 while ((filename = [fileEnumerator nextObject])) {
524 NSString *file = [filename stringByDeletingPathExtension];
526 if (![file isEqualToString:@".DS_Store"])
527 [soundNames addObject:file];
533 return [soundNames autorelease];
536 - (void)translateSeparatorsInMenu:(NSNotification *)notification
538 NSPopUpButton * button = [notification object];
540 NSMenu *menu = [button menu];
544 while ((itemIndex = [menu indexOfItemWithTitle:@"-"]) != -1) {
545 [menu removeItemAtIndex:itemIndex];
546 [menu insertItem:[NSMenuItem separatorItem] atIndex:itemIndex];
550 #pragma mark Growl running state
553 * @brief Launches GrowlHelperApp.
555 - (void) launchGrowl {
556 // Don't allow the button to be clicked while we update
557 [startStopGrowl setEnabled:NO];
558 [growlRunningProgress startAnimation:self];
560 // Update our status visible to the user
561 [growlRunningStatus setStringValue:NSLocalizedStringFromTableInBundle(@"Launching Growl...",nil,[self bundle],@"")];
563 [preferencesController setGrowlRunning:YES noMatterWhat:NO];
565 // After 4 seconds force a status update, in case Growl didn't start/stop
566 [self performSelector:@selector(checkGrowlRunning)
572 * @brief Terminates running GrowlHelperApp instances.
574 - (void) terminateGrowl {
575 // Don't allow the button to be clicked while we update
576 [startStopGrowl setEnabled:NO];
577 [growlRunningProgress startAnimation:self];
579 // Update our status visible to the user
580 [growlRunningStatus setStringValue:NSLocalizedStringFromTableInBundle(@"Terminating Growl...",nil,[self bundle],@"")];
582 // Ask the Growl Helper App to shutdown
583 [preferencesController setGrowlRunning:NO noMatterWhat:NO];
585 // After 4 seconds force a status update, in case growl didn't start/stop
586 [self performSelector:@selector(checkGrowlRunning)
591 #pragma mark "General" tab pane
593 - (IBAction) startStopGrowl:(id) sender {
594 #pragma unused(sender)
595 // Make sure growlIsRunning is correct
596 if (growlIsRunning != [preferencesController isGrowlRunning]) {
597 // Nope - lets just flip it and update status
598 [self setGrowlIsRunning:!growlIsRunning];
599 [self updateRunningStatus];
603 // Our desired state is a toggle of the current state;
605 [self terminateGrowl];
612 - (IBAction) customFileChosen:(id)sender {
613 int selected = [sender indexOfSelectedItem];
614 if ((selected == [sender numberOfItems] - 1) || (selected == -1)) {
615 NSSavePanel *sp = [NSSavePanel savePanel];
616 [sp setRequiredFileType:@"log"];
617 [sp setCanSelectHiddenExtension:YES];
619 int runResult = [sp runModalForDirectory:nil file:@""];
620 NSString *saveFilename = [sp filename];
621 if (runResult == NSFileHandlingPanelOKButton) {
622 unsigned saveFilenameIndex = NSNotFound;
623 unsigned i = CFArrayGetCount(customHistArray);
626 if ([(id)CFArrayGetValueAtIndex(customHistArray, i) isEqual:saveFilename]) {
627 saveFilenameIndex = i;
632 if (saveFilenameIndex == NSNotFound) {
633 if (CFArrayGetCount(customHistArray) == 3U)
634 CFArrayRemoveValueAtIndex(customHistArray, 2);
636 CFArrayRemoveValueAtIndex(customHistArray, saveFilenameIndex);
637 CFArrayInsertValueAtIndex(customHistArray, 0, saveFilename);
640 CFStringRef temp = CFRetain(CFArrayGetValueAtIndex(customHistArray, selected));
641 CFArrayRemoveValueAtIndex(customHistArray, selected);
642 CFArrayInsertValueAtIndex(customHistArray, 0, temp);
646 unsigned numHistItems = CFArrayGetCount(customHistArray);
648 id s = (id)CFArrayGetValueAtIndex(customHistArray, 0);
649 [preferencesController setObject:s forKey:GrowlCustomHistKey1];
651 if ((numHistItems > 1U) && (s = (id)CFArrayGetValueAtIndex(customHistArray, 1)))
652 [preferencesController setObject:s forKey:GrowlCustomHistKey2];
654 if ((numHistItems > 2U) && (s = (id)CFArrayGetValueAtIndex(customHistArray, 2)))
655 [preferencesController setObject:s forKey:GrowlCustomHistKey3];
657 //[[logFileType cellAtRow:1 column:0] setEnabled:YES];
658 [logFileType selectCellAtRow:1 column:0];
661 [self updateLogPopupMenu];
664 - (void) updateLogPopupMenu {
665 [customMenuButton removeAllItems];
667 int numHistItems = CFArrayGetCount(customHistArray);
668 for (int i = 0U; i < numHistItems; i++) {
669 NSArray *pathComponentry = [[(NSString *)CFArrayGetValueAtIndex(customHistArray, i) stringByAbbreviatingWithTildeInPath] pathComponents];
670 unsigned numPathComponents = [pathComponentry count];
671 if (numPathComponents > 2U) {
672 unichar ellipsis = 0x2026;
673 NSMutableString *arg = [[NSMutableString alloc] initWithCharacters:&ellipsis length:1U];
674 [arg appendString:@"/"];
675 [arg appendString:[pathComponentry objectAtIndex:(numPathComponents - 2U)]];
676 [arg appendString:@"/"];
677 [arg appendString:[pathComponentry objectAtIndex:(numPathComponents - 1U)]];
678 [customMenuButton insertItemWithTitle:arg atIndex:i];
681 [customMenuButton insertItemWithTitle:[(NSString *)CFArrayGetValueAtIndex(customHistArray, i) stringByAbbreviatingWithTildeInPath] atIndex:i];
683 // No separator if there's no file list yet
684 if (numHistItems > 0)
685 [[customMenuButton menu] addItem:[NSMenuItem separatorItem]];
686 [customMenuButton addItemWithTitle:NSLocalizedStringFromTableInBundle(@"Browse menu item title", nil, [self bundle], nil)];
687 //select first item, if any
688 [customMenuButton selectItemAtIndex:numHistItems ? 0 : -1];
691 #pragma mark "Applications" tab pane
693 - (BOOL) canRemoveTicket {
694 return canRemoveTicket;
697 - (void) setCanRemoveTicket:(BOOL)flag {
698 canRemoveTicket = flag;
701 - (void) deleteTicket:(id)sender {
702 #pragma unused(sender)
703 NSString *appName = [[[ticketsArrayController selectedObjects] objectAtIndex:0U] applicationName];
704 NSAlert *alert = [NSAlert alertWithMessageText:[NSString stringWithFormat:NSLocalizedStringFromTableInBundle(@"Are you sure you want to remove %@?", nil, [self bundle], nil), appName]
705 defaultButton:NSLocalizedStringFromTableInBundle(@"Remove", nil, [self bundle], "Button title for removing something")
706 alternateButton:NSLocalizedStringFromTableInBundle(@"Cancel", nil, [self bundle], "Button title for canceling")
708 informativeTextWithFormat:[NSString stringWithFormat:
709 NSLocalizedStringFromTableInBundle(@"This will remove all Growl settings for %@.", nil, [self bundle], ""), appName]];
710 [alert setIcon:[[[NSImage alloc] initWithContentsOfFile:[[self bundle] pathForImageResource:@"growl-icon"]] autorelease]];
711 [alert beginSheetModalForWindow:[[NSApplication sharedApplication] keyWindow] modalDelegate:self didEndSelector:@selector(deleteCallbackDidEnd:returnCode:contextInfo:) contextInfo:nil];
714 // this method is used as our callback to determine whether or not to delete the ticket
715 -(void) deleteCallbackDidEnd:(NSAlert *)alert returnCode:(int)returnCode contextInfo:(void *)eventID {
716 #pragma unused(alert)
717 #pragma unused(eventID)
718 if (returnCode == NSAlertDefaultReturn) {
719 GrowlApplicationTicket *ticket = [[ticketsArrayController selectedObjects] objectAtIndex:0U];
720 NSString *path = [ticket path];
722 if ([[NSFileManager defaultManager] removeFileAtPath:path handler:nil]) {
723 CFNumberRef pidValue = CFNumberCreate(kCFAllocatorDefault, kCFNumberIntType, &pid);
724 CFStringRef keys[2] = { CFSTR("TicketName"), CFSTR("pid") };
725 CFTypeRef values[2] = { [ticket applicationName], pidValue };
726 CFDictionaryRef userInfo = CFDictionaryCreate(kCFAllocatorDefault, (const void **)keys, (const void **)values, 2, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
728 CFNotificationCenterPostNotification(CFNotificationCenterGetDistributedCenter(),
729 (CFStringRef)GrowlPreferencesChanged,
730 CFSTR("GrowlTicketDeleted"),
733 unsigned idx = [tickets indexOfObject:ticket];
734 CFArrayRemoveValueAtIndex(images, idx);
736 unsigned oldSelectionIndex = [ticketsArrayController selectionIndex];
738 /// Hmm... This doesn't work for some reason....
739 // Even though the same method definitely (?) probably works in the appRegistered: method...
741 // [self removeFromTicketsAtIndex: [ticketsArrayController selectionIndex]];
743 NSMutableArray *newTickets = [tickets mutableCopy];
744 [newTickets removeObject:ticket];
745 [self setTickets:newTickets];
746 [newTickets release];
748 if (oldSelectionIndex >= [tickets count])
749 oldSelectionIndex = [tickets count] - 1;
751 [ticketsArrayController setSelectionIndex:oldSelectionIndex];
756 -(IBAction)playSound:(id)sender
758 if([sender indexOfSelectedItem] > 0) // The 0 item is "None"
759 [[NSSound soundNamed:[[sender selectedItem] title]] play];
762 #pragma mark "Display" tab pane
764 - (IBAction) showDisabledDisplays:(id)sender {
765 #pragma unused(sender)
766 [disabledDisplaysList setString:[[pluginController disabledPlugins] componentsJoinedByString:@"\n"]];
768 [NSApp beginSheet:disabledDisplaysSheet
769 modalForWindow:[[self mainView] window]
775 - (IBAction) endDisabledDisplays:(id)sender {
776 #pragma unused(sender)
777 [NSApp endSheet:disabledDisplaysSheet];
778 [disabledDisplaysSheet orderOut:disabledDisplaysSheet];
781 // Returns a boolean based on whether any disabled displays are present, used for the 'hidden' binding of the button on the tab
782 - (BOOL)hasDisabledDisplays {
783 return [pluginController disabledPluginsPresent];
786 // Popup buttons that post preview notifications support suppressing the preview with the Option key
787 - (IBAction) showPreview:(id)sender {
788 if(([sender isKindOfClass:[NSPopUpButton class]]) && (GetCurrentKeyModifiers() & optionKey))
791 NSDictionary *pluginToUse = currentPlugin;
793 #warning This does not work if the popup button is not using the exact same order as displayPluginsArrayController - a default or separator item breaks it
794 if([sender isKindOfClass:[NSPopUpButton class]])
795 pluginToUse = [[displayPluginsArrayController content] objectAtIndex:[(NSPopUpButton *)sender indexOfSelectedItem]];
797 [[NSDistributedNotificationCenter defaultCenter] postNotificationName:GrowlPreview
798 object:[pluginToUse objectForKey:GrowlPluginInfoKeyName]];
801 - (void) loadViewForDisplay:(NSString *)displayName {
802 NSView *newView = nil;
803 NSPreferencePane *prefPane = nil, *oldPrefPane = nil;
806 oldPrefPane = pluginPrefPane;
809 // Old plugins won't support the new protocol. Check first
810 if ([currentPluginController respondsToSelector:@selector(preferencePane)])
811 prefPane = [currentPluginController preferencePane];
813 if (prefPane == pluginPrefPane) {
814 // Don't bother swapping anything
817 [pluginPrefPane release];
818 pluginPrefPane = [prefPane retain];
819 [oldPrefPane willUnselect];
821 if (pluginPrefPane) {
822 if ([loadedPrefPanes containsObject:pluginPrefPane]) {
823 newView = [pluginPrefPane mainView];
825 newView = [pluginPrefPane loadMainView];
826 [loadedPrefPanes addObject:pluginPrefPane];
828 [pluginPrefPane willSelect];
831 [pluginPrefPane release];
832 pluginPrefPane = nil;
835 newView = displayDefaultPrefView;
836 if (displayPrefView != newView) {
837 // Make sure the new view is framed correctly
838 [newView setFrame:DISPLAY_PREF_FRAME];
839 [[displayPrefView superview] replaceSubview:displayPrefView with:newView];
840 displayPrefView = newView;
842 if (pluginPrefPane) {
843 [pluginPrefPane didSelect];
844 // Hook up key view chain
845 [displayPluginsTable setNextKeyView:[pluginPrefPane firstKeyView]];
846 [[pluginPrefPane lastKeyView] setNextKeyView:previewButton];
847 //[[displayPluginsTable window] makeFirstResponder:[pluginPrefPane initialKeyView]];
849 [displayPluginsTable setNextKeyView:previewButton];
853 [oldPrefPane didUnselect];
857 #pragma mark About Tab
859 - (void) setupAboutTab {
860 [aboutVersionString setStringValue:[NSString stringWithFormat:@"%@ %@",
861 [[self bundle] objectForInfoDictionaryKey:@"CFBundleName"],
862 [[self bundle] objectForInfoDictionaryKey:@"CFBundleShortVersionString"]]];
863 [aboutBoxTextView readRTFDFromFile:[[self bundle] pathForResource:@"About" ofType:@"rtf"]];
866 - (IBAction) openGrowlWebSite:(id)sender {
867 #pragma unused(sender)
868 [[NSWorkspace sharedWorkspace] openURL:growlWebSiteURL];
871 - (IBAction) openGrowlForum:(id)sender {
872 #pragma unused(sender)
873 [[NSWorkspace sharedWorkspace] openURL:growlForumURL];
876 - (IBAction) openGrowlBugSubmissionPage:(id)sender {
877 #pragma unused(sender)
878 [[NSWorkspace sharedWorkspace] openURL:growlBugSubmissionURL];
881 - (IBAction) openGrowlDonate:(id)sender {
882 #pragma unused(sender)
883 [[NSWorkspace sharedWorkspace] openURL:growlDonateURL];
885 #pragma mark TableView data source methods
887 - (int) numberOfRowsInTableView:(NSTableView*)tableView {
888 if(tableView == networkTableView) {
889 return [[self services] count];
893 - (void) tableViewDidClickInBody:(NSTableView *)tableView {
894 activeTableView = tableView;
895 [self setCanRemoveTicket:(activeTableView == growlApplications) && [ticketsArrayController canRemove]];
898 - (void)tableView:(NSTableView *)aTableView setObjectValue:(id)anObject forTableColumn:(NSTableColumn *)aTableColumn row:(int)rowIndex {
899 #pragma unused(aTableView)
900 if(aTableColumn == servicePasswordColumn) {
901 [[services objectAtIndex:rowIndex] setPassword:anObject];
906 - (id) tableView:(NSTableView *)aTableView objectValueForTableColumn:(NSTableColumn *)aTableColumn row:(int)rowIndex {
907 #pragma unused(aTableView)
908 // we check to make sure we have the image + text column and then set its image manually
909 if (aTableColumn == applicationNameAndIconColumn) {
910 NSArray *arrangedTickets = [ticketsArrayController arrangedObjects];
911 unsigned idx = [tickets indexOfObject:[arrangedTickets objectAtIndex:rowIndex]];
912 [[aTableColumn dataCellForRow:rowIndex] setImage:(NSImage *)CFArrayGetValueAtIndex(images,idx)];
913 } else if (aTableColumn == servicePasswordColumn) {
914 return [[services objectAtIndex:rowIndex] password];
920 - (IBAction) tableViewDoubleClick:(id)sender {
921 if ([ticketsArrayController selectionIndex] != NSNotFound) {
922 [applicationsTab selectLastTabViewItem:sender];
923 [configurationTab selectFirstTabViewItem:sender];
927 #pragma mark NSNetServiceBrowser Delegate Methods
929 - (void) netServiceBrowser:(NSNetServiceBrowser *)aNetServiceBrowser didFindService:(NSNetService *)aNetService moreComing:(BOOL)moreComing {
930 #pragma unused(aNetServiceBrowser)
931 // check if a computer with this name has already been added
932 NSString *name = [aNetService name];
933 NSEnumerator *enumerator = [services objectEnumerator];
934 GrowlBrowserEntry *entry;
935 while ((entry = [enumerator nextObject])) {
936 if ([[entry computerName] isEqualToString:name]) {
937 [entry setActive:YES];
942 // don't add the local machine
943 CFStringRef localHostName = SCDynamicStoreCopyComputerName(/*store*/ NULL,
944 /*nameEncoding*/ NULL);
945 CFComparisonResult isLocalHost = CFStringCompare(localHostName, (CFStringRef)name, 0);
946 CFRelease(localHostName);
947 if (isLocalHost == kCFCompareEqualTo)
950 // add a new entry at the end
951 entry = [[GrowlBrowserEntry alloc] initWithComputerName:name];
952 [self willChangeValueForKey:@"services"];
953 [services addObject:entry];
954 [self didChangeValueForKey:@"services"];
958 [self writeForwardDestinations];
961 - (void) netServiceBrowser:(NSNetServiceBrowser *)aNetServiceBrowser didRemoveService:(NSNetService *)aNetService moreComing:(BOOL)moreComing {
962 #pragma unused(aNetServiceBrowser)
963 NSEnumerator *serviceEnum = [services objectEnumerator];
964 GrowlBrowserEntry *currentEntry;
965 NSString *name = [aNetService name];
967 while ((currentEntry = [serviceEnum nextObject])) {
968 if ([[currentEntry computerName] isEqualToString:name]) {
969 [currentEntry setActive:NO];
975 [self writeForwardDestinations];
980 - (void) resolveService:(id)sender {
981 NSLog(@"What calls resolveService:?");
984 - (NSMutableArray *) services {
988 - (void) setServices:(NSMutableArray *)theServices {
989 if (theServices != services) {
992 [services setArray:theServices];
994 services = [theServices retain];
1002 - (unsigned) countOfServices {
1003 return [services count];
1006 - (id) objectInServicesAtIndex:(unsigned)idx {
1007 return [services objectAtIndex:idx];
1010 - (void) insertObject:(id)anObject inServicesAtIndex:(unsigned)idx {
1011 [services insertObject:anObject atIndex:idx];
1014 - (void) replaceObjectInServicesAtIndex:(unsigned)idx withObject:(id)anObject {
1015 [services replaceObjectAtIndex:idx withObject:anObject];
1018 #pragma mark Detecting Growl
1020 - (void) checkGrowlRunning {
1021 [self setGrowlIsRunning:[preferencesController isGrowlRunning]];
1022 [self updateRunningStatus];
1025 #pragma mark "Display Options" tab pane
1027 - (NSArray *) displayPlugins {
1031 - (void) setDisplayPlugins:(NSArray *)thePlugins {
1032 if (thePlugins != plugins) {
1034 plugins = [thePlugins retain];
1041 * @brief Refresh preferences when a new application registers with Growl
1043 - (void) appRegistered: (NSNotification *) note {
1044 NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
1046 NSString *app = [note object];
1047 GrowlApplicationTicket *newTicket = [[GrowlApplicationTicket alloc] initTicketForApplication:app];
1050 * Because the tickets array is under KVObservation by the TicketsArrayController
1051 * We need to remove the ticket using the correct KVC method:
1054 NSEnumerator *ticketEnumerator = [tickets objectEnumerator];
1055 GrowlApplicationTicket *ticket;
1056 int removalIndex = -1;
1059 while ((ticket = [ticketEnumerator nextObject])) {
1060 if ([[ticket applicationName] isEqualToString:app]) {
1067 if (removalIndex != -1)
1068 [self removeFromTicketsAtIndex:removalIndex];
1069 [self insertInTickets:newTicket];
1070 [newTicket release];
1077 - (void) growlLaunched:(NSNotification *)note {
1078 #pragma unused(note)
1079 NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
1081 [self setGrowlIsRunning:YES];
1082 [self updateRunningStatus];
1087 - (void) growlTerminated:(NSNotification *)note {
1088 #pragma unused(note)
1089 NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
1091 [self setGrowlIsRunning:NO];
1092 [self updateRunningStatus];