Rewrote the error message text for the this-plug-in-won't-work error to be more understandable for less-experienced users.
2 // GrowlPluginController.m
5 // Created by Nelson Elhage on 8/25/04.
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 "GrowlPluginController.h"
11 #import "GrowlPreferencesController.h"
12 #import "GrowlDisplayPlugin.h"
13 #import "GrowlDefinesInternal.h"
14 #include "CFDictionaryAdditions.h"
15 #include "CFMutableDictionaryAdditions.h"
17 #import "GrowlPathUtilities.h"
18 #import "GrowlNonCopyingMutableDictionary.h"
20 #import "NSSetAdditions.h"
21 #import "NSWorkspaceAdditions.h"
22 #import "GrowlWebKitPluginHandler.h"
24 @interface GrowlPluginController (PRIVATE)
25 - (void) registerDefaultPluginHandlers;
26 - (void) findPluginsInDirectory:(NSString *)dir;
27 - (void) pluginInstalledSelector:(NSWindow *)sheet returnCode:(int)returnCode contextInfo:(void *)contextInfo;
28 - (void) pluginExistsSelector:(NSWindow *)sheet returnCode:(int)returnCode contextInfo:(void *)contextInfo;
31 @interface WebCoreCache
35 //for use as CFSetCallBacks.equal
36 static Boolean caseInsensitiveStringComparator(const void *value1, const void *value2);
37 //for use as CFSetCallBacks.hash
38 static CFHashCode passthroughStringHash(const void *value);
39 //for use on array of matching plug-in handlers in -openPluginAtPath:
40 NSInteger comparePluginHandlerRegistrationOrder(id a, id b, void *context);
44 NSString *GrowlPluginControllerWillAddPluginHandlerNotification = @"GrowlPluginControllerWillAddPluginHandlerNotification";
45 NSString *GrowlPluginControllerDidAddPluginHandlerNotification = @"GrowlPluginControllerDidAddPluginHandlerNotification";
46 NSString *GrowlPluginControllerWillRemovePluginHandlerNotification = @"GrowlPluginControllerWillRemovePluginHandlerNotification";
47 NSString *GrowlPluginControllerDidRemovePluginHandlerNotification = @"GrowlPluginControllerDidRemovePluginHandlerNotification";
49 //Info.plist keys for plug-in bundles.
50 NSString *GrowlPluginInfoKeyName = @"CFBundleName";
51 NSString *GrowlPluginInfoKeyAuthor = @"GrowlPluginAuthor";
52 NSString *GrowlPluginInfoKeyVersion = @"CFBundleVersion";
53 NSString *GrowlPluginInfoKeyDescription = @"GrowlPluginDescription";
54 //keys in plug-in description dictionaries (also includes the above).
55 NSString *GrowlPluginInfoKeyBundle = @"GrowlPluginBundle";
56 NSString *GrowlPluginInfoKeyTypes = @"GrowlPluginType";
57 NSString *GrowlPluginInfoKeyPath = @"GrowlPluginPath";
58 NSString *GrowlPluginInfoKeyHumanReadableName = @"GrowlPluginHumanReadableName";
59 NSString *GrowlPluginInfoKeyIdentifier = @"GrowlPluginIdentifier";
60 NSString *GrowlPluginInfoKeyInstance = @"GrowlPluginInstance";
62 /*******************************************************************************
64 * |_ _/ _ \| _ \ / _ \
65 * | || | | | | | | | | |
66 * | || |_| | |_| | |_| |
67 * |_| \___/|____/ \___/
69 *******************************************************************************
71 * - Use identifier strings for all loaded plug-ins (simpler than using
72 * human-readable names) (DONE though this will probably only be used for
73 * storage of plug-in prefs)
74 * - Use plug-in dictionaries (DONE)
75 * - Use GrowlNonCopyingMutableDictionary instead of NSMapTable (DONE)
76 * - Write the built-in plug-in handler (DONE)
77 * - Add a WebKit plug-in handler (jkp)
78 * - Better localize human-readable names
81 @implementation GrowlPluginController
83 + (GrowlPluginController *) sharedController {
84 return [self sharedInstance];
87 - (id) initSingleton {
88 if ((self = [super initSingleton])) {
89 bundlesToLazilyInstantiateAnInstanceFrom = [[NSMutableSet alloc] init];
91 pluginsByIdentifier = [[NSMutableDictionary alloc] init];
92 pluginIdentifiersByPath = [[NSMutableDictionary alloc] init];
93 pluginIdentifiersByBundle = [[GrowlNonCopyingMutableDictionary alloc] init];
94 pluginIdentifiersByInstance = [[GrowlNonCopyingMutableDictionary alloc] init];
96 pluginsByName = [[NSMutableDictionary alloc] init];
97 pluginsByAuthor = [[NSMutableDictionary alloc] init];
98 pluginsByVersion = [[NSMutableDictionary alloc] init];
99 pluginsByFilename = [[NSMutableDictionary alloc] init];
100 pluginsByType = [[NSMutableDictionary alloc] init];
101 pluginHumanReadableNames = [[NSCountedSet alloc] init];
103 loadedBundleIdentifiers = [[NSMutableSet alloc] init];
105 allPluginHandlers = [[NSMutableArray alloc] init];
106 pluginHandlers = [[NSMutableDictionary alloc] init];
107 handlersForPlugins = [[GrowlNonCopyingMutableDictionary alloc] init];
109 displayPlugins = [[NSMutableArray alloc] init];
110 disabledPlugins = [[NSMutableArray alloc] init];
112 [self registerDefaultPluginHandlers];
114 enum { builtInTypesCount = 4U };
115 NSString *builtInTypesArray[builtInTypesCount] = {
116 GROWL_STYLE_EXTENSION,
117 GROWL_VIEW_EXTENSION,
118 GROWL_PATHWAY_EXTENSION,
119 GROWL_PLUGIN_EXTENSION,
121 CFSetCallBacks callbacks = kCFCopyStringSetCallBacks;
122 callbacks.equal = caseInsensitiveStringComparator;
123 callbacks.hash = passthroughStringHash;
124 builtInTypes = (NSSet *)CFSetCreate(kCFAllocatorDefault,
125 (const void **)builtInTypesArray,
129 //Find plugins inside GHA itself first
130 [self findPluginsInDirectory:[[GrowlPathUtilities helperAppBundle] builtInPlugInsPath]];
132 /* Then find plug-ins in Library/Application Support/Growl/Plugins directories. This allows GHA to override externally installed plugins,
133 * which are fairly common as some 3rd party plugins have been rolled into the Growl distribution.
135 NSArray *libraries = NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSAllDomainsMask, YES);
136 NSEnumerator *enumerator = [libraries objectEnumerator];
138 while ((dir = [enumerator nextObject])) {
139 dir = [dir stringByAppendingPathComponent:@"Application Support/Growl/Plugins"];
140 [self findPluginsInDirectory:dir];
148 [pluginsByIdentifier release];
149 [pluginIdentifiersByPath release];
150 [pluginIdentifiersByBundle release];
151 [pluginIdentifiersByInstance release];
153 [pluginsByName release];
154 [pluginsByAuthor release];
155 [pluginsByVersion release];
156 [pluginsByFilename release];
157 [pluginsByType release];
158 [pluginHumanReadableNames release];
160 [loadedBundleIdentifiers release];
162 [bundlesToLazilyInstantiateAnInstanceFrom release];
163 [displayPlugins release];
164 [disabledPlugins release];
166 [allPluginHandlers release];
167 [pluginHandlers release];
168 [handlersForPlugins release];
170 [builtInTypes release];
172 [cache_allPlugins release];
173 [cache_allPluginsArray release];
174 [cache_registeredPluginTypes release];
175 [cache_registeredPluginNames release];
176 [cache_registeredPluginNamesArray release];
177 [cache_allPluginInstances release];
178 [cache_displayPlugins release];
185 - (void) registerDefaultPluginHandlers {
186 //register ourselves for display plug-ins (non-WebKit), pathway plug-ins, and functional plug-ins.
187 NSSet *types = [[NSSet alloc] initWithObjects:
189 GROWL_VIEW_EXTENSION,
190 NSFileTypeForHFSTypeCode(FOUR_CHAR_CODE('DISP')),
192 GROWL_PATHWAY_EXTENSION,
193 NSFileTypeForHFSTypeCode(FOUR_CHAR_CODE('PWAY')),
194 //generic functional plug-ins
195 GROWL_PLUGIN_EXTENSION,
196 NSFileTypeForHFSTypeCode(FOUR_CHAR_CODE('GEXT')),
199 [self addPluginHandler:self forPluginTypes:types];
203 [GrowlWebKitPluginHandler sharedInstance]; // Calling this here will cause the handler to register
207 #pragma mark GrowlPluginHandler protocol conformance
209 //the method that dispatches incoming plug-ins to plug-in handlers is -dispatchPluginAtPath:.
210 //this is for handling plug-ins of the built-in types.
211 - (BOOL)loadPluginWithBundle:(NSBundle *)bundle {
212 [self addPluginInstance:nil fromBundle:bundle];
218 #pragma mark Plugin-handler handling
220 - (void) addPluginHandler:(id <GrowlPluginHandler>)handler forPluginTypes:(NSSet *)types {
221 NSParameterAssert(handler != nil);
222 NSParameterAssert(types != nil);
224 if (![types count]) {
225 NSLog(@"Warning: -[%@ addPluginHandler:forPluginTypes:] called with an empty set of file types. This may be indicative of a bug in Growl or a plug-in. The handler was %@.", [self class], handler);
227 //make sure nobody tries to register a plug-in handler for a built-in type.
229 NSMutableSet *builtInMutable = [builtInTypes mutableCopy];
230 [builtInMutable intersectSet:types];
232 //the intersection must be empty; if it isn't, at least one of the types for which this handler is attempting to register is a built-in type.
233 NSAssert2([builtInMutable count] == 0U, @"Something attempted to register a plug-in handler for one or more reserved file types (%@). The handler was %@.", builtInMutable, handler);
235 [builtInMutable release];
238 NSEnumerator *typeEnum = [types objectEnumerator];
240 while ((type = [typeEnum nextObject])) {
241 //normalise: strip leading ., if any.
242 NSUInteger i = 0U, len = [type length];
243 for (; i < len; ++i) {
244 if ([type characterAtIndex:i] != '.')
247 NSAssert2(i < len, @"Something tried to register a plug-in handler for a file type consisting entirely of %u full stops ('.'). The handler was %@.", len, handler);
249 type = [type substringFromIndex:i];
251 NSMutableArray *handlers = [pluginHandlers objectForKey:type];
253 handlers = [[NSMutableArray alloc] initWithCapacity:1U];
254 [pluginHandlers setObject:handlers forKey:type];
257 [handlers addObject:handler];
260 [allPluginHandlers addObject:handler];
262 NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
263 NSDictionary *notificationUserInfo = [[[NSDictionary alloc] initWithObjectsAndKeys:
264 handler, @"GrowlPluginHandler",
267 [nc postNotificationName:GrowlPluginControllerWillAddPluginHandlerNotification
269 userInfo:notificationUserInfo];
271 [allPluginHandlers addObject:handler];
273 //add the handler as an observer for various notifications.
274 if ([handler respondsToSelector:@selector(growlPluginControllerWillAddPluginHandler:)]) {
275 [nc addObserver:handler
276 selector:@selector(growlPluginControllerWillAddPluginHandler:)
277 name:GrowlPluginControllerWillAddPluginHandlerNotification
280 if ([handler respondsToSelector:@selector(growlPluginControllerDidAddPluginHandler:)]) {
281 [nc addObserver:handler
282 selector:@selector(growlPluginControllerDidAddPluginHandler:)
283 name:GrowlPluginControllerDidAddPluginHandlerNotification
286 if ([handler respondsToSelector:@selector(growlPluginControllerWillRemovePluginHandler:)]) {
287 [nc addObserver:handler
288 selector:@selector(growlPluginControllerWillRemovePluginHandler:)
289 name:GrowlPluginControllerWillRemovePluginHandlerNotification
292 if ([handler respondsToSelector:@selector(growlPluginControllerDidRemovePluginHandler:)]) {
293 [nc addObserver:handler
294 selector:@selector(growlPluginControllerDidRemovePluginHandler:)
295 name:GrowlPluginControllerDidRemovePluginHandlerNotification
299 [nc postNotificationName:GrowlPluginControllerDidAddPluginHandlerNotification
301 userInfo:notificationUserInfo];
305 - (void) removePluginHandler:(id <GrowlPluginHandler>)handler forPluginTypes:(NSSet *)extensions {
306 NSParameterAssert(handler != nil);
308 [allPluginHandlers removeObjectIdenticalTo:handler];
311 extensions = (NSSet *)[pluginHandlers allKeysForObject:handler];
313 NSEnumerator *extEnum = [extensions objectEnumerator];
315 while ((ext = [extEnum nextObject])) {
316 NSMutableArray *handlers = [pluginHandlers objectForKey:ext];
318 NSUInteger idx = [handlers indexOfObjectIdenticalTo:handler];
319 if (idx != NSNotFound)
320 [handlers removeObjectAtIndex:idx];
325 - (NSArray *) allPluginHandlers {
326 return [[allPluginHandlers copy] autorelease];
332 - (NSDictionary *) addPluginInstance:(GrowlPlugin *)plugin fromPath:(NSString *)path bundle:(NSBundle *)bundle {
333 //If we're passed a bundle, refuse to load it if we've already loaded a bundle with the same identifier, instead returning early.
334 NSString *bundleIdentifier = (bundle ? [bundle objectForInfoDictionaryKey:(NSString *)kCFBundleIdentifierKey] : nil);
335 if (bundleIdentifier && [loadedBundleIdentifiers containsObject:[bundle objectForInfoDictionaryKey:(NSString *)kCFBundleIdentifierKey]]) {
339 //Look up the identifier for the plugin. We try to look up the identifier by the instance, by the bundle; and by the pathname, in that order.
340 NSString *identifier = nil;
342 identifier = [pluginIdentifiersByInstance objectForKey:plugin];
344 identifier = [pluginIdentifiersByBundle objectForKey:bundle];
346 identifier = [pluginIdentifiersByPath objectForKey:path];
348 /* If we have an identifier, look up the plug-in dictionary.
349 * If we have a plug-in dictionary but no instance (the identifier was retrieved by bundle or by path), attempt to retrieve the instance from the dictionary.
351 NSMutableDictionary *pluginDict = identifier ? [pluginsByIdentifier objectForKey:identifier] : nil;
352 if (pluginDict && !plugin)
353 plugin = [pluginDict pluginInstance];
355 //Assert that we have an instance OR a bundle. We need at least one to proceed.
356 NSAssert1(plugin || bundle, @"Cannot load plug-ins lazily without a bundle (path: %@)", path);
358 //Get the plug-in's name, author, and version. All three come from the plug-in instance if it exists and responds to -name/-author/-version; if both requirements are not satisfied, the information is retrieved from the bundle's Info.plist.
359 NSString *name = plugin ? ([plugin respondsToSelector:@selector(name)] ? [plugin name] : [bundle objectForInfoDictionaryKey:(NSString *)kCFBundleNameKey])
360 : [bundle objectForInfoDictionaryKey:(NSString *)kCFBundleNameKey];
361 NSString *author = plugin ? ([plugin respondsToSelector:@selector(author)] ? [plugin author] : [bundle objectForInfoDictionaryKey:GrowlPluginInfoKeyAuthor])
362 : [bundle objectForInfoDictionaryKey:GrowlPluginInfoKeyAuthor];
363 NSString *version = plugin ? ([plugin respondsToSelector:@selector(version)] ? [plugin version] : [bundle objectForInfoDictionaryKey:(NSString *)kCFBundleVersionKey])
364 : [bundle objectForInfoDictionaryKey:(NSString *)kCFBundleVersionKey];
366 //If we don't have a pathname, get it as the bundle's pathname.
368 path = [bundle bundlePath];
369 NSString *extension = [path pathExtension];
370 NSString *fileType = nil;
372 //Assert that we have a name, author, and version. (We got the path first so we can use it in the assertion message.)
373 NSAssert5((name != nil) && (author != nil) && (version != nil),
374 @"Cannot load plug-in at path %@ (plug-in instance's class: %@). One of these is (null), but they must all not be:\n"
376 @"\t"@" author: %@\n"
377 @"\t"@"version: %@\n",
378 path, [plugin class], name, author, version);
380 //In case we got the names from the plug-in instance and it gave us a mutable string for some reason, make copies for ourselves.
381 //Note: This isn't a performance hit when the strings are immutable. -copy = -retain in that situation. Thanks, Apple!
383 author = [author copy];
384 version = [version copy];
386 //If we don't have an identifier yet, forge it.
388 identifier = [NSString stringWithFormat:@"Name: %@ Author: %@ Path: %@", name, author, path];
390 //If we don't have an instance but we do have a bundle, see if we've previously queued the bundle for lazy instantiation.
391 if (!plugin && bundle) {
392 if (![bundlesToLazilyInstantiateAnInstanceFrom containsObject:bundle]) {
393 //We haven't previously queued it: Queue it.
394 [bundlesToLazilyInstantiateAnInstanceFrom addObject:bundle];
396 //We have: This is our cue to instantiate it.
397 plugin = [[[bundle principalClass] alloc] init];
398 //Dequeue it, because we don't want to hit this branch again for this plug-in.
399 [bundlesToLazilyInstantiateAnInstanceFrom removeObject:bundle];
400 //Stash the plug-in instance in the plug-in dictionary. This retains the instance and means that we'll never hit the lazy-instantiation machinery again (because plugin will be non-nil).
401 [pluginDict setObject:plugin forKey:GrowlPluginInfoKeyInstance];
405 if (!plugin && bundle) {
406 //*Still* no plug-in! Again we check whether it's queued for instantiation (bug?).
407 if (![bundlesToLazilyInstantiateAnInstanceFrom containsObject:bundle])
408 [bundlesToLazilyInstantiateAnInstanceFrom addObject:bundle];
410 //Apparently it is. Instantiate it, but don't stash the plug-in instance in the plug-in dictionary (why not?).
411 plugin = [[[bundle principalClass] alloc] init];
412 [bundlesToLazilyInstantiateAnInstanceFrom removeObject:bundle];
416 /*If we don't actually have a plug-in dictionary, create it.
417 *Elements of a plug-in dictionary:
423 * Instance (later; see above lazy-instantiation code)
424 * Plug-in's HFS type and filename extension (combined in a set)
426 BOOL pluginDictIsNew = !pluginDict;
428 pluginDict = [NSMutableDictionary dictionaryWithObjectsAndKeys:
429 name, GrowlPluginInfoKeyName,
430 author, GrowlPluginInfoKeyAuthor,
431 version, GrowlPluginInfoKeyVersion,
432 path, GrowlPluginInfoKeyPath,
433 identifier, GrowlPluginInfoKeyIdentifier,
435 NSString *description = ([plugin respondsToSelector:@selector(pluginDescription)] ? [plugin pluginDescription] : nil);
438 [pluginDict setObject:description forKey:GrowlPluginInfoKeyDescription];
440 [[NSWorkspace sharedWorkspace] getFileType:&fileType creatorCode:NULL forFile:path];
442 //Record the file types (HFS and filename extension) that the plug-in possessed at this time. These help determine what kind of plug-in it is (e.g. .growlView = custom view; .growlStyle = WebKit display).
445 #warning problem here...
446 ///XXX when there is no file type it is coming back as \'\'...im guessing this means no type, but it still tests as true so each plugin is registered against that type...wrong???
448 types = [NSSet setWithObjects:extension, fileType, nil];
450 types = [NSSet setWithObject:extension];
452 types = [NSSet setWithObject:fileType];
455 [pluginDict setObject:types forKey:GrowlPluginInfoKeyTypes];
458 //We have a bundle. If no previous bundle was stored in the plug-in dictionary (why wouldn't there be?), store this bundle there. Also register the identifier as being the one for this bundle.
460 if (![pluginDict objectForKey:GrowlPluginInfoKeyBundle])
461 [pluginDict setObject:bundle forKey:GrowlPluginInfoKeyBundle];
462 [pluginIdentifiersByBundle setObject:identifier forKey:bundle];
464 //We have an instance. If no previous instance was stored in the plug-in dictionary (why wouldn't there be?), store this instance there. Also register the identifier as being the one for this instance.
466 if (![pluginDict objectForKey:GrowlPluginInfoKeyInstance])
467 [pluginDict setObject:plugin forKey:GrowlPluginInfoKeyInstance];
468 [pluginIdentifiersByInstance setObject:identifier forKey:plugin];
471 //If we just created the dictionary (and got done filling it out), start storing it in places.
472 if (pluginDictIsNew) {
473 [pluginsByIdentifier setObject:pluginDict forKey:identifier];
474 [pluginIdentifiersByPath setObject:identifier forKey:path];
476 #define ADD_TO_DICT(dictName, key, value) \
478 NSMutableSet *plugins = [dictName objectForKey:key]; \
480 [plugins addObject:value]; \
482 [dictName setObject:[NSMutableSet setWithObject:value] forKey:key]; \
484 ADD_TO_DICT(pluginsByName, name, pluginDict);
485 ADD_TO_DICT(pluginsByAuthor, author, pluginDict);
486 ADD_TO_DICT(pluginsByVersion, version, pluginDict);
487 ADD_TO_DICT(pluginsByFilename, [path lastPathComponent], pluginDict);
489 ADD_TO_DICT(pluginsByType, extension, pluginDict);
490 ADD_TO_DICT(pluginsByType, fileType, pluginDict);
494 //Release our copies.
499 //Invalidate non-display plug-in caches.
500 [cache_allPlugins release];
501 cache_allPlugins = nil;
502 [cache_allPluginsArray release];
503 cache_allPluginsArray = nil;
504 [cache_registeredPluginTypes release];
505 cache_registeredPluginTypes = nil;
506 [cache_registeredPluginNames release];
507 cache_registeredPluginNames = nil;
508 [cache_registeredPluginNamesArray release];
509 cache_registeredPluginNamesArray = nil;
511 //Special handling if this plug-in is a display.
512 if ([self pluginWithDictionaryIsDisplayPlugin:pluginDict]) {
513 //If it doesn't respond to -requiresPositioning, it's old. Add it as a disabled plug-in.
514 if(![[pluginDict valueForKey:GrowlPluginInfoKeyInstance] respondsToSelector:@selector(requiresPositioning)]) {
515 [disabledPlugins addObject:[pluginDict valueForKey:GrowlPluginInfoKeyName]];
518 //It responds to -requiresPositioning, so add it as a(n enabled) display plug-in.
519 [displayPlugins addObject:pluginDict];
522 //Invalidate display plug-in cache.
523 [cache_displayPlugins release];
524 cache_displayPlugins = nil;
527 //Store the bundle identifier so we know we've loaded it.
528 if (bundleIdentifier) {
529 [loadedBundleIdentifiers addObject:bundleIdentifier];
535 - (NSArray *) disabledPlugins {
536 return disabledPlugins;
539 - (BOOL) disabledPluginsPresent {
540 return ([disabledPlugins count] > 0);
543 - (NSDictionary *) addPluginInstance:(GrowlPlugin *)plugin fromBundle:(NSBundle *)bundle {
544 return [self addPluginInstance:plugin fromPath:nil bundle:bundle];
546 - (NSDictionary *) addPluginInstance:(GrowlPlugin *)plugin fromPath:(NSString *)path {
547 return [self addPluginInstance:plugin fromPath:path bundle:nil];
552 - (NSSet *) registeredPluginTypes {
553 if (!cache_registeredPluginTypes)
554 cache_registeredPluginTypes = [[NSSet alloc] initWithArray:[pluginHandlers allKeys]];
556 return cache_registeredPluginTypes;
559 - (NSSet *) registeredPluginNames {
560 if (!cache_registeredPluginNames)
561 cache_registeredPluginNames = [[NSSet alloc] initWithArray:[self registeredPluginNamesArray]];
562 return cache_registeredPluginNames;
565 - (NSArray *) registeredPluginNamesArray {
566 if (!cache_registeredPluginNamesArray) {
567 cache_registeredPluginNamesArray = [[[pluginsByIdentifier allValues] valueForKey:GrowlPluginInfoKeyName] retain];
569 return cache_registeredPluginNamesArray;
572 - (NSArray *) registeredPluginNamesArrayForType:(NSString *)type {
573 #warning this should be cached per type
574 return [[[pluginsByType valueForKey:type] allObjects] valueForKey:GrowlPluginInfoKeyName];
580 - (NSArray *) allPluginDictionariesArray {
581 if (!cache_allPluginsArray)
582 cache_allPluginsArray = [[pluginsByIdentifier allValues] copy];
583 return cache_allPluginsArray;
585 - (NSSet *) allPluginDictionaries {
586 if (!cache_allPlugins)
587 cache_allPlugins = [[NSMutableSet alloc] initWithArray:[self allPluginDictionariesArray]];
588 return cache_allPlugins;
590 - (NSArray *) allPluginInstances {
591 if (!cache_allPluginInstances)
592 cache_allPluginInstances = [[[self allPluginDictionaries] valueForKey:GrowlPluginInfoKeyInstance] retain];
593 return cache_allPluginInstances;
596 - (NSSet *) pluginDictionariesWithName:(NSString *)name author:(NSString *)author version:(NSString *)version type:(NSString *)type {
597 NSMutableSet *matches = [[[self allPluginDictionaries] mutableCopy] autorelease];
599 if ([matches count]) {
601 [matches intersectSet:[pluginsByName objectForKey:name]];
603 [matches intersectSet:[pluginsByAuthor objectForKey:author]];
605 [matches intersectSet:[pluginsByVersion objectForKey:version]];
607 [matches intersectSet:[pluginsByType objectForKey:type]];
612 - (NSDictionary *) pluginDictionaryWithName:(NSString *)name author:(NSString *)author version:(NSString *)version type:(NSString *)type {
613 NSSet *matches = [self pluginDictionariesWithName:name author:author version:version type:type];
614 if ([matches count] == 1U)
615 return [matches anyObject];
619 - (NSDictionary *) pluginDictionaryWithName:(NSString *)name {
620 return [self pluginDictionaryWithName:name author:nil version:nil type:nil];
622 - (GrowlPlugin *) pluginInstanceWithName:(NSString *)name author:(NSString *)author version:(NSString *)version type:(NSString *)type {
623 NSDictionary *pluginDict = [self pluginDictionaryWithName:name author:author version:version type:type];
624 GrowlPlugin *instance = [pluginDict pluginInstance];
626 NSBundle *bundle = [pluginDict pluginBundle];
628 [self addPluginInstance:nil fromBundle:bundle]; //causes instantiation
629 instance = [pluginDict pluginInstance];
634 - (GrowlPlugin *) pluginInstanceWithName:(NSString *)name {
635 return [self pluginInstanceWithName:name author:nil version:nil type:nil];
640 - (NSString *) humanReadableNameForPluginWithDictionary:(NSDictionary *)pluginDict {
641 NSString *humanReadableName = [pluginDict pluginHumanReadableName];
643 if (!humanReadableName) {
644 NSString *name = [pluginDict pluginName];
645 if ([[pluginsByName objectForKey:name] count] == 1U)
646 humanReadableName = name;
648 NSString *author = [pluginDict pluginAuthor];
649 if ([[pluginsByAuthor objectForKey:author] count] == 1U)
650 humanReadableName = [NSString stringWithFormat:@"%@ (by %@)", name, author]; //XXX LOCALIZEME
652 NSString *filename = [[pluginDict pluginPath] lastPathComponent];
653 if ([[pluginsByFilename objectForKey:filename] count] == 1U)
654 humanReadableName = [NSString stringWithFormat:@"%@ (filename %@)", name, filename]; //XXX LOCALIZEME
656 humanReadableName = [NSString stringWithFormat:@"%@ (by %@, filename %@)", name, author, filename]; //XXX LOCALIZEME
657 NSUInteger count = [pluginHumanReadableNames countForObject:humanReadableName];
658 [pluginHumanReadableNames addObject:humanReadableName];
660 humanReadableName = [NSString stringWithFormat:@"%@ %u", humanReadableName, count];
665 if ([pluginDict isKindOfClass:[NSMutableDictionary class]]) {
666 //save the name for later retrieval.
667 [(NSMutableDictionary *)pluginDict setObject:humanReadableName forKey:GrowlPluginInfoKeyHumanReadableName];
671 return humanReadableName;
674 - (BOOL) pluginWithDictionaryIsDisplayPlugin:(NSDictionary *)pluginDict {
675 GrowlPlugin *instance = [pluginDict pluginInstance];
677 return [instance isKindOfClass:[GrowlDisplayPlugin class]];
679 NSBundle *bundle = [pluginDict pluginBundle];
680 NSAssert1(bundle, @"no instance or bundle in plug-in dictionary! description of dictionary follows\n%@", pluginDict);
681 Class principalClass = [bundle principalClass];
682 NSAssert1(bundle, @"bundle in plug-in dictionary has no principal class! description of dictionary follows\n%@", pluginDict);
683 return [principalClass isSubclassOfClass:[GrowlDisplayPlugin class]];
688 #warning XXX all of this could potentially go bye-bye if it is not needed
690 - (NSArray *) displayPlugins {
691 if (!cache_displayPlugins)
692 cache_displayPlugins = [[NSArray alloc] initWithArray:displayPlugins];
693 return cache_displayPlugins;
698 - (NSSet *) displayPluginDictionariesWithName:(NSString *)name author:(NSString *)author version:(NSString *)version type:(NSString *)type {
699 NSMutableSet *matches = (NSMutableSet *)[self pluginDictionariesWithName:name
704 NSSet *copyForIteration = [matches copy];
705 NSEnumerator *matchesEnum = [copyForIteration objectEnumerator];
706 NSDictionary *pluginDict;
707 while ((pluginDict = [matchesEnum nextObject])) {
708 if (![self pluginWithDictionaryIsDisplayPlugin:pluginDict])
709 [matches removeObject:pluginDict];
711 [copyForIteration release];
716 - (NSDictionary *) displayPluginDictionaryWithName:(NSString *)name author:(NSString *)author version:(NSString *)version type:(NSString *)type {
717 NSSet *matches = [self displayPluginDictionariesWithName:name
721 if ([matches count] == 1U)
722 return [matches anyObject];
727 - (GrowlDisplayPlugin *) displayPluginInstanceWithName:(NSString *)name author:(NSString *)author version:(NSString *)version type:(NSString *)type {
728 GrowlPlugin *plugin = [self pluginInstanceWithName:name author:author version:version type:type];
729 if (plugin && [plugin isKindOfClass:[GrowlDisplayPlugin class]])
730 return (GrowlDisplayPlugin *)plugin;
736 #pragma mark Finding and using installed plug-ins
738 - (void) findPluginsInDirectory:(NSString *)dir {
739 NSDirectoryEnumerator *enumerator = [[NSFileManager defaultManager] enumeratorAtPath:dir];
741 while ((file = [enumerator nextObject])) {
742 NSString *fullPath = [dir stringByAppendingPathComponent:file];
744 NSString *pathExtension = [file pathExtension];
746 [[NSWorkspace sharedWorkspace] getFileType:&fileType creatorCode:NULL forFile:fullPath];
747 if ([pluginHandlers objectForKey:pathExtension] || (fileType && [pluginHandlers objectForKey:fileType])) {
748 [self dispatchPluginAtPath:fullPath];
749 [enumerator skipDescendents];
754 - (void) dispatchPluginAtPath:(NSString *)path {
755 //get all the relevant handlers, by extension and by type.
756 NSArray *handlersByExtension = [pluginHandlers objectForKey:[path pathExtension]];
758 [[NSWorkspace sharedWorkspace] getFileType:&fileType creatorCode:NULL forFile:path];
759 NSArray *handlersByType = [pluginHandlers objectForKey:fileType];
761 //strip duplicates by making a set from both arrays.
762 NSMutableSet *allMatchingHandlersSet = handlersByExtension ? [NSMutableSet setWithArray:handlersByExtension] : [NSMutableSet set];
764 [allMatchingHandlersSet unionSet:[NSSet setWithArray:handlersByType]];
766 NSMutableArray *allMatchingHandlers = [[[allMatchingHandlersSet allObjects] mutableCopy] autorelease];
767 [allMatchingHandlers sortUsingFunction:comparePluginHandlerRegistrationOrder context:self];
769 NSBundle *pluginBundle = [[NSBundle alloc] initWithPath:path];
771 NSEnumerator *handlersEnum = [allMatchingHandlers objectEnumerator];
772 id <GrowlPluginHandler> handler;
773 while ((handler = [handlersEnum nextObject])) {
775 if (pluginBundle && [handler respondsToSelector:@selector(loadPluginWithBundle:)])
776 success = (NSUInteger)[handler performSelector:@selector(loadPluginWithBundle:) withObject:pluginBundle];
777 else if ([handler respondsToSelector:@selector(loadPluginAtPath:)])
778 success = (NSUInteger)[handler performSelector:@selector(loadPluginAtPath:) withObject:path];
779 else if ([handler respondsToSelector:@selector(loadPluginAtURL:)])
780 success = (NSUInteger)[handler performSelector:@selector(loadPluginAtURL:) withObject:[NSURL fileURLWithPath:path]];
782 NSLog(@"warning: while loading plug-in at %@, tried to use plug-in handler %@, but it appears incapable of handling a plug-in", path, handler); //XXX should do this diagnostic when adding the handler
785 [pluginBundle release];
789 #pragma mark Installing plug-ins
791 - (void) pluginInstalledSelector:(NSWindow *)sheet returnCode:(int)returnCode contextInfo:(void *)contextInfo {
792 #pragma unused(sheet, contextInfo)
793 if (returnCode == NSAlertAlternateReturn) {
794 NSBundle *prefPane = [GrowlPathUtilities growlPrefPaneBundle];
796 if (prefPane && ![[NSWorkspace sharedWorkspace] openFile:[prefPane bundlePath]])
797 NSLog(@"Could not open Growl PrefPane");
801 - (void) pluginExistsSelector:(NSWindow *)sheet returnCode:(int)returnCode contextInfo:(void *)contextInfo {
802 #pragma unused(sheet)
803 NSString *filename = (NSString *)contextInfo;
805 if (returnCode == NSAlertAlternateReturn) {
806 //'Yes' to 'Do you want to overwrite [the installed plug-in with the version you double-clicked]?'
807 NSString *pluginFile = [filename lastPathComponent];
808 NSString *destination = [[NSHomeDirectory()
809 stringByAppendingPathComponent:@"Library/Application Support/Growl/Plugins"]
810 stringByAppendingPathComponent:pluginFile];
811 NSFileManager *fileManager = [NSFileManager defaultManager];
813 // first remove old copy if present
814 [fileManager removeFileAtPath:destination handler:nil];
816 // copy new version to destination
817 if ([fileManager copyPath:filename toPath:destination handler:nil]) {
818 [self dispatchPluginAtPath:destination];
819 [[NSApplication sharedApplication] activateIgnoringOtherApps:YES];
820 if([self _hasNativeArchitecture:destination])
821 NSBeginInformationalAlertSheet( NSLocalizedString( @"Plugin installed", @"" ),
822 NSLocalizedString( @"No", @"" ),
823 NSLocalizedString( @"Yes", @"" ),
825 @selector(pluginInstalledSelector:returnCode:contextInfo:),
827 NSLocalizedString( @"Plugin '%@' has been installed successfully. Do you want to configure it now?", @"" ),
828 [pluginFile stringByDeletingPathExtension] );
830 [[NSApplication sharedApplication] activateIgnoringOtherApps:YES];
831 NSBeginCriticalAlertSheet( NSLocalizedString( @"Plugin not installed", @"" ),
832 NSLocalizedString( @"OK", @"" ),
833 nil, nil, nil, self, NULL, NULL, NULL,
834 NSLocalizedString( @"There was an error while installing the plugin '%@'.", @"" ),
835 [pluginFile stringByDeletingPathExtension] );
842 - (void) installPluginFromPath:(NSString *)filename {
843 NSString *pluginFile = [filename lastPathComponent];
844 NSString *destination = [[NSHomeDirectory()
845 stringByAppendingPathComponent:@"Library/Application Support/Growl/Plugins"]
846 stringByAppendingPathComponent:pluginFile];
847 // retain a copy of the filename because it is passed as context to the sheetDidEnd selectors
848 NSString *filenameCopy = [[NSString alloc] initWithString:filename];
850 //Check to see if we've got valid architectures in this plugin for our use, if not, bail.
851 if(![self _hasNativeArchitecture:filenameCopy]) {
852 NSBeginAlertSheet( NSLocalizedString( @"Plugin missing native architecture", @"" ),
853 NSLocalizedString( @"No", @"" ),
854 NSLocalizedString( @"Yes", @"" ), nil, nil, self,
855 NULL, @selector(pluginExistsSelector:returnCode:contextInfo:),
857 NSLocalizedString( @"Plugin '%@' will not work on this Mac running this version of Mac OS X. Install it anyway?", @"" ),
858 [pluginFile stringByDeletingPathExtension] );
861 if ([[NSFileManager defaultManager] fileExistsAtPath:destination]) {
862 // plugin already exists at destination
863 [[NSApplication sharedApplication] activateIgnoringOtherApps:YES];
864 NSBeginAlertSheet( NSLocalizedString( @"Plugin already exists", @"" ),
865 NSLocalizedString( @"No", @"" ),
866 NSLocalizedString( @"Yes", @"" ), nil, nil, self,
867 NULL, @selector(pluginExistsSelector:returnCode:contextInfo:),
869 NSLocalizedString( @"Plugin '%@' is already installed, do you want to overwrite it?", @"" ),
870 [pluginFile stringByDeletingPathExtension] );
872 [self pluginExistsSelector:nil returnCode:NSAlertAlternateReturn contextInfo:filenameCopy];
877 - (BOOL)_hasNativeArchitecture:(NSString*)filename {
879 NSInteger currentArchitecture = 0;
880 #if defined(__ppc__) && __ppc__
881 currentArchitecture = NSBundleExecutableArchitecturePPC;
882 #elif defined(__i386__) && __i386__
883 currentArchitecture = NSBundleExecutableArchitectureI386;
884 #elif defined(__x86_64__) && __x86_64__
885 currentArchitecture = NSBundleExecutableArchitectureX86_64;
887 #error unsupported architecture
889 NSBundle *pluginBundle = [NSBundle bundleWithPath:filename];
890 NSString *executablePath = [pluginBundle executablePath];
891 //we check to see if there is actually an executable in this plugin, it could be a growlStyle, under which we accept it as valid.
892 if(executablePath && [[NSFileManager defaultManager] fileExistsAtPath:executablePath]) {
893 NSArray *pluginArchitectures = [pluginBundle executableArchitectures];
894 if([pluginArchitectures containsObject:[NSNumber numberWithInteger:currentArchitecture]])
906 static Boolean caseInsensitiveStringComparator(const void *value1, const void *value2) {
907 Class NSStringClass = [NSString class];
908 return [(id)value1 isKindOfClass:NSStringClass] \
909 && [(id)value2 isKindOfClass:NSStringClass] \
910 && ([(NSString *)value1 caseInsensitiveCompare:(NSString *)value2] == NSOrderedSame);
913 static CFHashCode passthroughStringHash(const void *value) {
914 return [[(NSString *)value lowercaseString] hash];
919 @implementation NSDictionary (GrowlPluginKeys)
921 - (NSString *) pluginName {
922 return [self objectForKey:GrowlPluginInfoKeyName];
924 - (NSString *) pluginAuthor {
925 return [self objectForKey:GrowlPluginInfoKeyAuthor];
927 - (NSString *) pluginDescription {
928 return [self objectForKey:GrowlPluginInfoKeyDescription];
930 - (NSString *) pluginVersion {
931 return [self objectForKey:GrowlPluginInfoKeyVersion];
933 - (NSBundle *) pluginBundle {
934 return [self objectForKey:GrowlPluginInfoKeyBundle];
936 - (NSString *) pluginPath {
937 return [self objectForKey:GrowlPluginInfoKeyPath];
939 - (NSSet *) pluginTypes {
940 return [self objectForKey:GrowlPluginInfoKeyTypes];
942 - (NSString *) pluginHumanReadableName {
943 return [self objectForKey:GrowlPluginInfoKeyHumanReadableName];
945 - (NSString *) pluginIdentifier {
946 return [self objectForKey:GrowlPluginInfoKeyIdentifier];
948 - (GrowlPlugin *) pluginInstance {
949 return [self objectForKey:GrowlPluginInfoKeyInstance];
954 #define ASSERT_IN_FUNCTION(condition, desc, ...) \
955 [[NSAssertionHandler currentHandler] handleFailureInFunction:[NSString stringWithCString:__func__] \
956 file:[NSString stringWithCString:__FILE__] \
957 lineNumber:__LINE__ \
958 description:desc, __VA_ARGS__];
960 NSInteger comparePluginHandlerRegistrationOrder(id a, id b, void *context) {
961 GrowlPluginController *self = (GrowlPluginController *)context;
962 NSArray *allPluginHandlers = [self allPluginHandlers];
964 NSUInteger aIndex = [allPluginHandlers indexOfObjectIdenticalTo:a];
965 NSUInteger bIndex = [allPluginHandlers indexOfObjectIdenticalTo:b];
967 ASSERT_IN_FUNCTION(aIndex != NSNotFound, @"Attempted to compare two plug-in handlers, but the first object was not a (registered) plug-in handler! Description of object: %@", a);
968 ASSERT_IN_FUNCTION(bIndex != NSNotFound, @"Attempted to compare two plug-in handlers, but the second object was not a (registered) plug-in handler! Description of object: %@", b);
971 return NSOrderedAscending;
972 else if (aIndex > bIndex)
973 return NSOrderedDescending;
975 return NSOrderedSame;