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.0, 58.0, 354.0, 289.0)
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 NSInteger 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 NSUInteger count = [arrangedObjects count];
178 NSString *defaultDisplayPluginName = [[self preferencesController] defaultDisplayPluginName];
179 NSUInteger defaultStyleRow = NSNotFound;
180 for (NSUInteger i = 0; i < count; i++) {
181 if ([[[arrangedObjects objectAtIndex:i] valueForKey:@"CFBundleName"] isEqualToString:defaultDisplayPluginName]) {
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)
234 [growlVersionProgress startAnimation:self];
236 [growlVersionProgress stopAnimation:self];
240 - (void) downloadSelector:(NSWindow *)sheet returnCode:(int)returnCode contextInfo:(void *)contextInfo {
241 #pragma unused(sheet)
242 CFURLRef downloadURL = (CFURLRef)contextInfo;
243 if (returnCode == NSAlertDefaultReturn)
244 [[NSWorkspace sharedWorkspace] openURL:(NSURL *)downloadURL];
245 CFRelease(downloadURL);
249 * @brief Returns if GrowlMenu is currently running.
251 + (BOOL) isGrowlMenuRunning {
252 return [[GrowlPreferencesController sharedController] isRunning:@"com.Growl.MenuExtra"];
255 //subclassed from NSPreferencePane; called before the pane is displayed.
256 - (void) willSelect {
257 NSString *lastVersion = [preferencesController objectForKey:LastKnownVersionKey];
258 NSString *currentVersion = [self bundleVersion];
259 if (!(lastVersion && [lastVersion isEqualToString:currentVersion])) {
260 if ([preferencesController isGrowlRunning]) {
261 [preferencesController setGrowlRunning:NO noMatterWhat:NO];
262 [preferencesController setGrowlRunning:YES noMatterWhat:YES];
264 [preferencesController setObject:currentVersion forKey:LastKnownVersionKey];
267 [self checkGrowlRunning];
271 [self reloadPreferences:nil];
275 * @brief copy images to avoid resizing the original images stored in the tickets.
277 - (void) cacheImages {
279 CFArrayRemoveAllValues(images);
281 images = CFArrayCreateMutable(kCFAllocatorDefault, 0, &kCFTypeArrayCallBacks);
283 NSEnumerator *enumerator = [[ticketsArrayController content] objectEnumerator];
284 GrowlApplicationTicket *ticket;
285 while ((ticket = [enumerator nextObject])) {
286 NSImage *icon = [[ticket icon] copy];
287 [icon setScalesWhenResized:YES];
288 [icon setSize:NSMakeSize(32.0, 32.0)];
289 CFArrayAppendValue(images, icon);
294 - (NSMutableArray *) tickets {
298 //using setTickets: will tip off the controller (KVO).
299 //use this to set the tickets secretly.
300 - (void) setTicketsWithoutTellingAnybody:(NSArray *)theTickets {
301 if (theTickets != tickets) {
303 [tickets setArray:theTickets];
305 tickets = [theTickets mutableCopy];
309 //we don't need to do any special extra magic here - just being setTickets: is enough to tip off the controller.
310 - (void) setTickets:(NSArray *)theTickets {
311 [self setTicketsWithoutTellingAnybody:theTickets];
314 - (void) removeFromTicketsAtIndex:(int)indexToRemove {
315 NSIndexSet *indices = [NSIndexSet indexSetWithIndex:indexToRemove];
316 [self willChange:NSKeyValueChangeInsertion valuesAtIndexes:indices forKey:@"tickets"];
318 [tickets removeObjectAtIndex:indexToRemove];
320 [self didChange:NSKeyValueChangeInsertion valuesAtIndexes:indices forKey:@"tickets"];
323 - (void) insertInTickets:(GrowlApplicationTicket *)newTicket {
324 NSIndexSet *indices = [NSIndexSet indexSetWithIndex:[tickets count]];
325 [self willChange:NSKeyValueChangeInsertion valuesAtIndexes:indices forKey:@"tickets"];
327 [tickets addObject:newTicket];
329 [self didChange:NSKeyValueChangeInsertion valuesAtIndexes:indices forKey:@"tickets"];
332 - (void) reloadDisplayPluginView {
333 NSArray *selectedPlugins = [displayPluginsArrayController selectedObjects];
334 NSUInteger numPlugins = [plugins count];
335 [currentPlugin release];
336 if (numPlugins > 0U && selectedPlugins && [selectedPlugins count] > 0U)
337 currentPlugin = [[selectedPlugins objectAtIndex:0U] retain];
341 NSString *currentPluginName = [currentPlugin objectForKey:(NSString *)kCFBundleNameKey];
342 currentPluginController = (GrowlPlugin *)[pluginController pluginInstanceWithName:currentPluginName];
343 [self loadViewForDisplay:currentPluginName];
344 [displayAuthor setStringValue:[currentPlugin objectForKey:@"GrowlPluginAuthor"]];
345 [displayVersion setStringValue:[currentPlugin objectForKey:(NSString *)kCFBundleNameKey]];
349 * @brief Called when a distributed GrowlPreferencesChanged notification is received.
351 - (void) reloadPrefs:(NSNotification *)notification {
352 // ignore notifications which are sent by ourselves
353 NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
355 NSNumber *pidValue = [[notification userInfo] objectForKey:@"pid"];
356 if (!pidValue || [pidValue intValue] != pid)
357 [self reloadPreferences:[notification object]];
362 - (void) updatePosition:(NSNotification *)notification {
363 if([notification object] == globalPositionPicker) {
364 [preferencesController setInteger:[globalPositionPicker selectedPosition] forKey:GROWL_POSITION_PREFERENCE_KEY];
366 else if([notification object] == appPositionPicker) {
367 // a cheap hack around selection not providing a workable object
368 NSArray *selection = [ticketsArrayController selectedObjects];
369 if ([selection count] > 0)
370 [[selection objectAtIndex:0] setSelectedPosition:[appPositionPicker selectedPosition]];
375 * @brief Reloads the preferences and updates the GUI accordingly.
377 - (void) reloadPreferences:(NSString *)object {
378 if (!object || [object isEqualToString:@"GrowlTicketChanged"]) {
379 GrowlTicketController *ticketController = [GrowlTicketController sharedController];
380 [ticketController loadAllSavedTickets];
381 [self setTickets:[[ticketController allSavedTickets] allValues]];
385 [self setDisplayPlugins:[[[GrowlPluginController sharedController] displayPlugins] valueForKey:GrowlPluginInfoKeyName]];
387 #ifdef THIS_CODE_WAS_REMOVED_AND_I_DONT_KNOW_WHY
388 if (!object || [object isEqualToString:@"GrowlTicketChanged"])
389 [self setTickets:[[ticketController allSavedTickets] allValues]];
391 [preferencesController setSquelchMode:[preferencesController squelchMode]];
392 [preferencesController setGrowlMenuEnabled:[preferencesController isGrowlMenuEnabled]];
397 // If Growl is enabled, ensure the helper app is launched
398 if ([preferencesController boolForKey:GrowlEnabledKey])
399 [preferencesController launchGrowl:NO];
401 if ([plugins count] > 0U)
402 [self reloadDisplayPluginView];
404 [self loadViewForDisplay:nil];
407 - (BOOL) growlIsRunning {
408 return growlIsRunning;
411 - (void) setGrowlIsRunning:(BOOL)flag {
412 growlIsRunning = flag;
415 - (void) updateRunningStatus {
416 [startStopGrowl setEnabled:YES];
417 NSBundle *bundle = [self bundle];
418 [startStopGrowl setTitle:
419 growlIsRunning ? NSLocalizedStringFromTableInBundle(@"Stop Growl",nil,bundle,@"")
420 : NSLocalizedStringFromTableInBundle(@"Start Growl",nil,bundle,@"")];
421 [growlRunningStatus setStringValue:
422 growlIsRunning ? NSLocalizedStringFromTableInBundle(@"Growl is running.",nil,bundle,@"")
423 : NSLocalizedStringFromTableInBundle(@"Growl is stopped.",nil,bundle,@"")];
424 [growlRunningProgress stopAnimation:self];
427 - (void) observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object
428 change:(NSDictionary *)change context:(void *)context {
429 #pragma unused(change, context)
430 if ([keyPath isEqualToString:@"selection"]) {
431 if ((object == ticketsArrayController))
432 [self setCanRemoveTicket:(activeTableView == growlApplications) && [ticketsArrayController canRemove]];
433 else if (object == displayPluginsArrayController)
434 [self reloadDisplayPluginView];
438 - (void) writeForwardDestinations {
439 NSMutableArray *destinations = [[NSMutableArray alloc] initWithCapacity:[services count]];
440 NSEnumerator *enumerator = [services objectEnumerator];
441 GrowlBrowserEntry *entry;
442 while ((entry = [enumerator nextObject]))
443 [destinations addObject:[entry properties]];
444 [preferencesController setObject:destinations forKey:GrowlForwardDestinationsKey];
445 [destinations release];
449 #pragma mark Bindings accessors (not for programmatic use)
451 - (GrowlPluginController *) pluginController {
452 if (!pluginController)
453 pluginController = [GrowlPluginController sharedController];
455 return pluginController;
457 - (GrowlPreferencesController *) preferencesController {
458 if (!preferencesController)
459 preferencesController = [GrowlPreferencesController sharedController];
461 return preferencesController;
464 - (NSArray *) sounds {
465 NSMutableArray *soundNames = [[NSMutableArray alloc] init];
467 NSArray *paths = [NSArray arrayWithObjects:@"/System/Library/Sounds",
469 [NSString stringWithFormat:@"%@/Library/Sounds", NSHomeDirectory()],
473 NSEnumerator *dirEnumerator = [paths objectEnumerator];
474 while ((directory = [dirEnumerator nextObject])) {
475 BOOL isDirectory = NO;
477 if ([[NSFileManager defaultManager] fileExistsAtPath:directory isDirectory:&isDirectory]) {
479 [soundNames addObject:@"-"];
481 NSArray *files = [[NSFileManager defaultManager] directoryContentsAtPath:directory];
483 NSString *filename = nil;
484 NSEnumerator *fileEnumerator = [files objectEnumerator];
485 while ((filename = [fileEnumerator nextObject])) {
486 NSString *file = [filename stringByDeletingPathExtension];
488 if (![file isEqualToString:@".DS_Store"])
489 [soundNames addObject:file];
495 return [soundNames autorelease];
498 - (void)translateSeparatorsInMenu:(NSNotification *)notification
500 NSPopUpButton * button = [notification object];
502 NSMenu *menu = [button menu];
504 NSInteger itemIndex = 0;
506 while ((itemIndex = [menu indexOfItemWithTitle:@"-"]) != -1) {
507 [menu removeItemAtIndex:itemIndex];
508 [menu insertItem:[NSMenuItem separatorItem] atIndex:itemIndex];
512 #pragma mark Growl running state
515 * @brief Launches GrowlHelperApp.
517 - (void) launchGrowl {
518 // Don't allow the button to be clicked while we update
519 [startStopGrowl setEnabled:NO];
520 [growlRunningProgress startAnimation:self];
522 // Update our status visible to the user
523 [growlRunningStatus setStringValue:NSLocalizedStringFromTableInBundle(@"Launching Growl...",nil,[self bundle],@"")];
525 [preferencesController setGrowlRunning:YES noMatterWhat:NO];
527 // After 4 seconds force a status update, in case Growl didn't start/stop
528 [self performSelector:@selector(checkGrowlRunning)
534 * @brief Terminates running GrowlHelperApp instances.
536 - (void) terminateGrowl {
537 // Don't allow the button to be clicked while we update
538 [startStopGrowl setEnabled:NO];
539 [growlRunningProgress startAnimation:self];
541 // Update our status visible to the user
542 [growlRunningStatus setStringValue:NSLocalizedStringFromTableInBundle(@"Terminating Growl...",nil,[self bundle],@"")];
544 // Ask the Growl Helper App to shutdown
545 [preferencesController setGrowlRunning:NO noMatterWhat:NO];
547 // After 4 seconds force a status update, in case growl didn't start/stop
548 [self performSelector:@selector(checkGrowlRunning)
553 #pragma mark "General" tab pane
555 - (IBAction) startStopGrowl:(id) sender {
556 #pragma unused(sender)
557 // Make sure growlIsRunning is correct
558 if (growlIsRunning != [preferencesController isGrowlRunning]) {
559 // Nope - lets just flip it and update status
560 [self setGrowlIsRunning:!growlIsRunning];
561 [self updateRunningStatus];
565 // Our desired state is a toggle of the current state;
567 [self terminateGrowl];
574 - (IBAction) customFileChosen:(id)sender {
575 int selected = [sender indexOfSelectedItem];
576 if ((selected == [sender numberOfItems] - 1) || (selected == -1)) {
577 NSSavePanel *sp = [NSSavePanel savePanel];
578 [sp setRequiredFileType:@"log"];
579 [sp setCanSelectHiddenExtension:YES];
581 int runResult = [sp runModalForDirectory:nil file:@""];
582 NSString *saveFilename = [sp filename];
583 if (runResult == NSFileHandlingPanelOKButton) {
584 unsigned saveFilenameIndex = NSNotFound;
585 unsigned i = CFArrayGetCount(customHistArray);
588 if ([(id)CFArrayGetValueAtIndex(customHistArray, i) isEqual:saveFilename]) {
589 saveFilenameIndex = i;
594 if (saveFilenameIndex == NSNotFound) {
595 if (CFArrayGetCount(customHistArray) == 3U)
596 CFArrayRemoveValueAtIndex(customHistArray, 2);
598 CFArrayRemoveValueAtIndex(customHistArray, saveFilenameIndex);
599 CFArrayInsertValueAtIndex(customHistArray, 0, saveFilename);
602 CFStringRef temp = CFRetain(CFArrayGetValueAtIndex(customHistArray, selected));
603 CFArrayRemoveValueAtIndex(customHistArray, selected);
604 CFArrayInsertValueAtIndex(customHistArray, 0, temp);
608 unsigned numHistItems = CFArrayGetCount(customHistArray);
610 id s = (id)CFArrayGetValueAtIndex(customHistArray, 0);
611 [preferencesController setObject:s forKey:GrowlCustomHistKey1];
613 if ((numHistItems > 1U) && (s = (id)CFArrayGetValueAtIndex(customHistArray, 1)))
614 [preferencesController setObject:s forKey:GrowlCustomHistKey2];
616 if ((numHistItems > 2U) && (s = (id)CFArrayGetValueAtIndex(customHistArray, 2)))
617 [preferencesController setObject:s forKey:GrowlCustomHistKey3];
619 //[[logFileType cellAtRow:1 column:0] setEnabled:YES];
620 [logFileType selectCellAtRow:1 column:0];
623 [self updateLogPopupMenu];
626 - (void) updateLogPopupMenu {
627 [customMenuButton removeAllItems];
629 CFIndex numHistItems = CFArrayGetCount(customHistArray);
630 for (int i = 0U; i < numHistItems; i++) {
631 NSArray *pathComponentry = [[(NSString *)CFArrayGetValueAtIndex(customHistArray, i) stringByAbbreviatingWithTildeInPath] pathComponents];
632 NSUInteger numPathComponents = [pathComponentry count];
633 if (numPathComponents > 2U) {
634 unichar ellipsis = 0x2026;
635 NSMutableString *arg = [[NSMutableString alloc] initWithCharacters:&ellipsis length:1U];
636 [arg appendString:@"/"];
637 [arg appendString:[pathComponentry objectAtIndex:(numPathComponents - 2U)]];
638 [arg appendString:@"/"];
639 [arg appendString:[pathComponentry objectAtIndex:(numPathComponents - 1U)]];
640 [customMenuButton insertItemWithTitle:arg atIndex:i];
643 [customMenuButton insertItemWithTitle:[(NSString *)CFArrayGetValueAtIndex(customHistArray, i) stringByAbbreviatingWithTildeInPath] atIndex:i];
645 // No separator if there's no file list yet
646 if (numHistItems > 0)
647 [[customMenuButton menu] addItem:[NSMenuItem separatorItem]];
648 [customMenuButton addItemWithTitle:NSLocalizedStringFromTableInBundle(@"Browse menu item title", nil, [self bundle], nil)];
649 //select first item, if any
650 [customMenuButton selectItemAtIndex:numHistItems ? 0 : -1];
653 #pragma mark "Applications" tab pane
655 - (BOOL) canRemoveTicket {
656 return canRemoveTicket;
659 - (void) setCanRemoveTicket:(BOOL)flag {
660 canRemoveTicket = flag;
663 - (void) deleteTicket:(id)sender {
664 #pragma unused(sender)
665 NSString *appName = [[[ticketsArrayController selectedObjects] objectAtIndex:0U] applicationName];
666 NSAlert *alert = [NSAlert alertWithMessageText:[NSString stringWithFormat:NSLocalizedStringFromTableInBundle(@"Are you sure you want to remove %@?", nil, [self bundle], nil), appName]
667 defaultButton:NSLocalizedStringFromTableInBundle(@"Remove", nil, [self bundle], "Button title for removing something")
668 alternateButton:NSLocalizedStringFromTableInBundle(@"Cancel", nil, [self bundle], "Button title for canceling")
670 informativeTextWithFormat:[NSString stringWithFormat:
671 NSLocalizedStringFromTableInBundle(@"This will remove all Growl settings for %@.", nil, [self bundle], ""), appName]];
672 [alert setIcon:[[[NSImage alloc] initWithContentsOfFile:[[self bundle] pathForImageResource:@"growl-icon"]] autorelease]];
673 [alert beginSheetModalForWindow:[[NSApplication sharedApplication] keyWindow] modalDelegate:self didEndSelector:@selector(deleteCallbackDidEnd:returnCode:contextInfo:) contextInfo:nil];
676 // this method is used as our callback to determine whether or not to delete the ticket
677 -(void) deleteCallbackDidEnd:(NSAlert *)alert returnCode:(int)returnCode contextInfo:(void *)eventID {
678 #pragma unused(alert)
679 #pragma unused(eventID)
680 if (returnCode == NSAlertDefaultReturn) {
681 GrowlApplicationTicket *ticket = [[ticketsArrayController selectedObjects] objectAtIndex:0U];
682 NSString *path = [ticket path];
684 if ([[NSFileManager defaultManager] removeFileAtPath:path handler:nil]) {
685 CFNumberRef pidValue = CFNumberCreate(kCFAllocatorDefault, kCFNumberIntType, &pid);
686 CFStringRef keys[2] = { CFSTR("TicketName"), CFSTR("pid") };
687 CFTypeRef values[2] = { [ticket applicationName], pidValue };
688 CFDictionaryRef userInfo = CFDictionaryCreate(kCFAllocatorDefault, (const void **)keys, (const void **)values, 2, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
690 CFNotificationCenterPostNotification(CFNotificationCenterGetDistributedCenter(),
691 (CFStringRef)GrowlPreferencesChanged,
692 CFSTR("GrowlTicketDeleted"),
695 NSUInteger idx = [tickets indexOfObject:ticket];
696 CFArrayRemoveValueAtIndex(images, idx);
698 NSUInteger oldSelectionIndex = [ticketsArrayController selectionIndex];
700 /// Hmm... This doesn't work for some reason....
701 // Even though the same method definitely (?) probably works in the appRegistered: method...
703 // [self removeFromTicketsAtIndex: [ticketsArrayController selectionIndex]];
705 NSMutableArray *newTickets = [tickets mutableCopy];
706 [newTickets removeObject:ticket];
707 [self setTickets:newTickets];
708 [newTickets release];
710 if (oldSelectionIndex >= [tickets count])
711 oldSelectionIndex = [tickets count] - 1;
713 [ticketsArrayController setSelectionIndex:oldSelectionIndex];
718 -(IBAction)playSound:(id)sender
720 if([sender indexOfSelectedItem] > 0) // The 0 item is "None"
721 [[NSSound soundNamed:[[sender selectedItem] title]] play];
724 - (IBAction) showApplicationConfigurationTab:(id)sender {
725 if ([ticketsArrayController selectionIndex] != NSNotFound) {
726 [self populateDisplaysPopUpButton:displayMenuButton nameOfSelectedDisplay:[[ticketsArrayController selection] valueForKey:@"displayPluginName"] includeDefaultMenuItem:YES];
727 [self populateDisplaysPopUpButton:notificationDisplayMenuButton nameOfSelectedDisplay:[[notificationsArrayController selection] valueForKey:@"displayPluginName"] includeDefaultMenuItem:YES];
729 [applicationsTab selectLastTabViewItem:sender];
730 [configurationTab selectFirstTabViewItem:sender];
734 - (IBAction) changeNameOfDisplayForApplication:(id)sender {
735 NSString *newDisplayPluginName = [[sender selectedItem] representedObject];
736 [[ticketsArrayController selectedObjects] setValue:newDisplayPluginName forKey:@"displayPluginName"];
737 [self showPreview:sender];
739 - (IBAction) changeNameOfDisplayForNotification:(id)sender {
740 NSString *newDisplayPluginName = [[sender selectedItem] representedObject];
741 [[notificationsArrayController selectedObjects] setValue:newDisplayPluginName forKey:@"displayPluginName"];
742 [self showPreview:sender];
745 - (NSIndexSet *) selectedNotificationIndexes {
746 return selectedNotificationIndexes;
748 - (void) setSelectedNotificationIndexes:(NSIndexSet *)newSelectedNotificationIndexes {
749 if(selectedNotificationIndexes != newSelectedNotificationIndexes) {
750 [selectedNotificationIndexes release];
751 selectedNotificationIndexes = [newSelectedNotificationIndexes copy];
753 int indexOfMenuItem = [[notificationDisplayMenuButton menu] indexOfItemWithRepresentedObject:[[notificationsArrayController selection] valueForKey:@"displayPluginName"]];
754 if (indexOfMenuItem < 0)
756 [notificationDisplayMenuButton selectItemAtIndex:indexOfMenuItem];
760 #pragma mark "Display" tab pane
762 - (IBAction) showDisabledDisplays:(id)sender {
763 #pragma unused(sender)
764 [disabledDisplaysList setString:[[pluginController disabledPlugins] componentsJoinedByString:@"\n"]];
766 [NSApp beginSheet:disabledDisplaysSheet
767 modalForWindow:[[self mainView] window]
773 - (IBAction) endDisabledDisplays:(id)sender {
774 #pragma unused(sender)
775 [NSApp endSheet:disabledDisplaysSheet];
776 [disabledDisplaysSheet orderOut:disabledDisplaysSheet];
779 // Returns a boolean based on whether any disabled displays are present, used for the 'hidden' binding of the button on the tab
780 - (BOOL)hasDisabledDisplays {
781 return [pluginController disabledPluginsPresent];
784 // Popup buttons that post preview notifications support suppressing the preview with the Option key
785 - (IBAction) showPreview:(id)sender {
786 if(([sender isKindOfClass:[NSPopUpButton class]]) && (GetCurrentKeyModifiers() & optionKey))
789 NSDictionary *pluginToUse = currentPlugin;
790 NSString *pluginName = nil;
792 if ([sender isKindOfClass:[NSPopUpButton class]]) {
793 NSPopUpButton *popUp = (NSPopUpButton *)sender;
794 if (sender == displayMenuButton || sender == notificationDisplayMenuButton)
795 pluginName = [[popUp selectedItem] representedObject];
797 #warning This does not work if the popup button is not using the exact same order as displayPluginsArrayController - a default or separator item breaks it
798 pluginToUse = [[displayPluginsArrayController content] objectAtIndex:[popUp indexOfSelectedItem]];
802 pluginName = [pluginToUse objectForKey:GrowlPluginInfoKeyName];
804 [[NSDistributedNotificationCenter defaultCenter] postNotificationName:GrowlPreview
808 - (void) loadViewForDisplay:(NSString *)displayName {
809 NSView *newView = nil;
810 NSPreferencePane *prefPane = nil, *oldPrefPane = nil;
813 oldPrefPane = pluginPrefPane;
816 // Old plugins won't support the new protocol. Check first
817 if ([currentPluginController respondsToSelector:@selector(preferencePane)])
818 prefPane = [currentPluginController preferencePane];
820 if (prefPane == pluginPrefPane) {
821 // Don't bother swapping anything
824 [pluginPrefPane release];
825 pluginPrefPane = [prefPane retain];
826 [oldPrefPane willUnselect];
828 if (pluginPrefPane) {
829 if ([loadedPrefPanes containsObject:pluginPrefPane]) {
830 newView = [pluginPrefPane mainView];
832 newView = [pluginPrefPane loadMainView];
833 [loadedPrefPanes addObject:pluginPrefPane];
835 [pluginPrefPane willSelect];
838 [pluginPrefPane release];
839 pluginPrefPane = nil;
842 newView = displayDefaultPrefView;
843 if (displayPrefView != newView) {
844 // Make sure the new view is framed correctly
845 [newView setFrame:DISPLAY_PREF_FRAME];
846 [[displayPrefView superview] replaceSubview:displayPrefView with:newView];
847 displayPrefView = newView;
849 if (pluginPrefPane) {
850 [pluginPrefPane didSelect];
851 // Hook up key view chain
852 [displayPluginsTable setNextKeyView:[pluginPrefPane firstKeyView]];
853 [[pluginPrefPane lastKeyView] setNextKeyView:previewButton];
854 //[[displayPluginsTable window] makeFirstResponder:[pluginPrefPane initialKeyView]];
856 [displayPluginsTable setNextKeyView:previewButton];
860 [oldPrefPane didUnselect];
864 #pragma mark About Tab
866 - (void) setupAboutTab {
867 [aboutVersionString setStringValue:[NSString stringWithFormat:@"%@ %@",
868 [[self bundle] objectForInfoDictionaryKey:@"CFBundleName"],
869 [[self bundle] objectForInfoDictionaryKey:@"CFBundleShortVersionString"]]];
870 [aboutBoxTextView readRTFDFromFile:[[self bundle] pathForResource:@"About" ofType:@"rtf"]];
873 - (IBAction) openGrowlWebSite:(id)sender {
874 #pragma unused(sender)
875 [[NSWorkspace sharedWorkspace] openURL:growlWebSiteURL];
878 - (IBAction) openGrowlForum:(id)sender {
879 #pragma unused(sender)
880 [[NSWorkspace sharedWorkspace] openURL:growlForumURL];
883 - (IBAction) openGrowlBugSubmissionPage:(id)sender {
884 #pragma unused(sender)
885 [[NSWorkspace sharedWorkspace] openURL:growlBugSubmissionURL];
888 - (IBAction) openGrowlDonate:(id)sender {
889 #pragma unused(sender)
890 [[NSWorkspace sharedWorkspace] openURL:growlDonateURL];
892 #pragma mark TableView data source methods
894 - (NSInteger) numberOfRowsInTableView:(NSTableView*)tableView {
895 if(tableView == networkTableView) {
896 return [[self services] count];
900 - (void) tableViewDidClickInBody:(NSTableView *)tableView {
901 activeTableView = tableView;
902 [self setCanRemoveTicket:(activeTableView == growlApplications) && [ticketsArrayController canRemove]];
905 - (void)tableView:(NSTableView *)aTableView setObjectValue:(id)anObject forTableColumn:(NSTableColumn *)aTableColumn row:(NSInteger)rowIndex {
906 #pragma unused(aTableView)
907 if(aTableColumn == servicePasswordColumn) {
908 [[services objectAtIndex:rowIndex] setPassword:anObject];
913 - (id) tableView:(NSTableView *)aTableView objectValueForTableColumn:(NSTableColumn *)aTableColumn row:(NSInteger)rowIndex {
914 #pragma unused(aTableView)
915 // we check to make sure we have the image + text column and then set its image manually
916 if (aTableColumn == applicationNameAndIconColumn) {
917 NSArray *arrangedTickets = [ticketsArrayController arrangedObjects];
918 NSUInteger idx = [tickets indexOfObject:[arrangedTickets objectAtIndex:rowIndex]];
919 [[aTableColumn dataCellForRow:rowIndex] setImage:(NSImage *)CFArrayGetValueAtIndex(images,idx)];
920 } else if (aTableColumn == servicePasswordColumn) {
921 return [[services objectAtIndex:rowIndex] password];
927 - (IBAction) tableViewDoubleClick:(id)sender {
928 [self showApplicationConfigurationTab:sender];
931 #pragma mark NSNetServiceBrowser Delegate Methods
933 - (void) netServiceBrowser:(NSNetServiceBrowser *)aNetServiceBrowser didFindService:(NSNetService *)aNetService moreComing:(BOOL)moreComing {
934 #pragma unused(aNetServiceBrowser)
935 // check if a computer with this name has already been added
936 NSString *name = [aNetService name];
937 NSEnumerator *enumerator = [services objectEnumerator];
938 GrowlBrowserEntry *entry;
939 while ((entry = [enumerator nextObject])) {
940 if ([[entry computerName] isEqualToString:name]) {
941 [entry setActive:YES];
946 // don't add the local machine
947 CFStringRef localHostName = SCDynamicStoreCopyComputerName(/*store*/ NULL,
948 /*nameEncoding*/ NULL);
949 CFComparisonResult isLocalHost = CFStringCompare(localHostName, (CFStringRef)name, 0);
950 CFRelease(localHostName);
951 if (isLocalHost == kCFCompareEqualTo)
954 // add a new entry at the end
955 entry = [[GrowlBrowserEntry alloc] initWithComputerName:name];
956 [self willChangeValueForKey:@"services"];
957 [services addObject:entry];
958 [self didChangeValueForKey:@"services"];
962 [self writeForwardDestinations];
965 - (void) netServiceBrowser:(NSNetServiceBrowser *)aNetServiceBrowser didRemoveService:(NSNetService *)aNetService moreComing:(BOOL)moreComing {
966 #pragma unused(aNetServiceBrowser)
967 NSEnumerator *serviceEnum = [services objectEnumerator];
968 GrowlBrowserEntry *currentEntry;
969 NSString *name = [aNetService name];
971 while ((currentEntry = [serviceEnum nextObject])) {
972 if ([[currentEntry computerName] isEqualToString:name]) {
973 [currentEntry setActive:NO];
979 [self writeForwardDestinations];
984 - (void) resolveService:(id)sender {
985 NSLog(@"What calls resolveService:?");
988 - (NSMutableArray *) services {
992 - (void) setServices:(NSMutableArray *)theServices {
993 if (theServices != services) {
996 [services setArray:theServices];
998 services = [theServices retain];
1006 - (NSUInteger) countOfServices {
1007 return [services count];
1010 - (id) objectInServicesAtIndex:(unsigned)idx {
1011 return [services objectAtIndex:idx];
1014 - (void) insertObject:(id)anObject inServicesAtIndex:(unsigned)idx {
1015 [services insertObject:anObject atIndex:idx];
1018 - (void) replaceObjectInServicesAtIndex:(unsigned)idx withObject:(id)anObject {
1019 [services replaceObjectAtIndex:idx withObject:anObject];
1022 #pragma mark Detecting Growl
1024 - (void) checkGrowlRunning {
1025 [self setGrowlIsRunning:[preferencesController isGrowlRunning]];
1026 [self updateRunningStatus];
1029 #pragma mark "Display Options" tab pane
1031 - (NSArray *) displayPlugins {
1035 - (void) setDisplayPlugins:(NSArray *)thePlugins {
1036 if (thePlugins != plugins) {
1038 plugins = [thePlugins retain];
1042 #pragma mark Display pop-up menus
1044 //Empties the pop-up menu and fills it out with a menu item for each display, optionally including a special menu item for the default display, selecting the menu item whose name is nameOfSelectedDisplay.
1045 - (void) populateDisplaysPopUpButton:(NSPopUpButton *)popUp nameOfSelectedDisplay:(NSString *)nameOfSelectedDisplay includeDefaultMenuItem:(BOOL)includeDefault {
1046 NSMenu *menu = [popUp menu];
1047 NSString *nameOfDisplay = nil, *displayNameOfDisplay;
1049 NSMenuItem *selectedItem = nil;
1051 [popUp removeAllItems];
1053 if (includeDefault) {
1054 displayNameOfDisplay = NSLocalizedStringFromTableInBundle(@"Default", nil, [NSBundle bundleForClass:[self class]], /*comment*/ @"Title of menu item for default display");
1055 NSMenuItem *item = [menu addItemWithTitle:displayNameOfDisplay
1058 [item setRepresentedObject:nil];
1060 if (!nameOfSelectedDisplay)
1061 selectedItem = item;
1063 [menu addItem:[NSMenuItem separatorItem]];
1066 NSEnumerator *displaysEnum = [[plugins sortedArrayUsingSelector:@selector(localizedCaseInsensitiveCompare:)] objectEnumerator];
1067 while ((nameOfDisplay = [displaysEnum nextObject])) {
1068 displayNameOfDisplay = [[pluginController pluginDictionaryWithName:nameOfDisplay] pluginHumanReadableName];
1069 if (!displayNameOfDisplay)
1070 displayNameOfDisplay = nameOfDisplay;
1072 NSMenuItem *item = [menu addItemWithTitle:displayNameOfDisplay
1075 [item setRepresentedObject:nameOfDisplay];
1077 if (nameOfSelectedDisplay && [nameOfSelectedDisplay respondsToSelector:@selector(isEqualToString:)] && [nameOfSelectedDisplay isEqualToString:nameOfDisplay])
1078 selectedItem = item;
1081 [popUp selectItem:selectedItem];
1087 * @brief Refresh preferences when a new application registers with Growl
1089 - (void) appRegistered: (NSNotification *) note {
1090 NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
1092 NSString *app = [note object];
1093 GrowlApplicationTicket *newTicket = [[GrowlApplicationTicket alloc] initTicketForApplication:app];
1096 * Because the tickets array is under KVObservation by the TicketsArrayController
1097 * We need to remove the ticket using the correct KVC method:
1100 NSEnumerator *ticketEnumerator = [tickets objectEnumerator];
1101 GrowlApplicationTicket *ticket;
1102 int removalIndex = -1;
1105 while ((ticket = [ticketEnumerator nextObject])) {
1106 if ([[ticket applicationName] isEqualToString:app]) {
1113 if (removalIndex != -1)
1114 [self removeFromTicketsAtIndex:removalIndex];
1115 [self insertInTickets:newTicket];
1116 [newTicket release];
1123 - (void) growlLaunched:(NSNotification *)note {
1124 #pragma unused(note)
1125 NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
1127 [self setGrowlIsRunning:YES];
1128 [self updateRunningStatus];
1133 - (void) growlTerminated:(NSNotification *)note {
1134 #pragma unused(note)
1135 NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
1137 [self setGrowlIsRunning:NO];
1138 [self updateRunningStatus];