Core/Source/GrowlPluginController.m
author Peter Hosey <hg@boredzo.org>
Sat Sep 26 17:36:21 2009 -0700 (2009-09-26)
changeset 4442 e6763de09830
parent 4441 e4bc764cf865
child 4488 8b26f543636c
permissions -rw-r--r--
When an error occurs while loading a plug-in, log it. Bug found by the analyzer.
boredzo@1385
     1
//
boredzo@1385
     2
//  GrowlPluginController.m
boredzo@1385
     3
//  Growl
boredzo@1385
     4
//
boredzo@1385
     5
//  Created by Nelson Elhage on 8/25/04.
ingmarstein@3040
     6
//  Copyright 2004-2006 The Growl Project. All rights reserved.
boredzo@1385
     7
//
boredzo@1385
     8
// This file is under the BSD License, refer to License.txt for details
boredzo@1385
     9
boredzo@1385
    10
#import "GrowlPluginController.h"
ingmarstein@2470
    11
#import "GrowlPreferencesController.h"
ingmarstein@2695
    12
#import "GrowlDisplayPlugin.h"
boredzo@2479
    13
#import "GrowlDefinesInternal.h"
ingmarstein@2641
    14
#include "CFDictionaryAdditions.h"
ingmarstein@2641
    15
#include "CFMutableDictionaryAdditions.h"
boredzo@1385
    16
eridius@2773
    17
#import "GrowlPathUtilities.h"
rudy@2947
    18
eridius@2773
    19
#import "NSSetAdditions.h"
eridius@2773
    20
#import "NSWorkspaceAdditions.h"
eridius@2773
    21
#import "GrowlWebKitPluginHandler.h"
eridius@2773
    22
ingmarstein@1790
    23
@interface GrowlPluginController (PRIVATE)
eridius@2773
    24
- (void) registerDefaultPluginHandlers;
ingmarstein@1947
    25
- (void) findPluginsInDirectory:(NSString *)dir;
ingmarstein@1790
    26
- (void) pluginInstalledSelector:(NSWindow *)sheet returnCode:(int)returnCode contextInfo:(void *)contextInfo;
ingmarstein@1790
    27
- (void) pluginExistsSelector:(NSWindow *)sheet returnCode:(int)returnCode contextInfo:(void *)contextInfo;
hg@4436
    28
- (BOOL) hasNativeArchitecture:(NSString *)filename;
boredzo@1385
    29
@end
boredzo@1385
    30
ingmarstein@1986
    31
@interface WebCoreCache
ingmarstein@1986
    32
+ (void) empty;
ingmarstein@1986
    33
@end
ingmarstein@1986
    34
boredzo@2469
    35
//for use as CFSetCallBacks.equal
boredzo@2469
    36
static Boolean caseInsensitiveStringComparator(const void *value1, const void *value2);
eridius@2773
    37
//for use as CFSetCallBacks.hash
eridius@2773
    38
static CFHashCode passthroughStringHash(const void *value);
eridius@2773
    39
//for use on array of matching plug-in handlers in -openPluginAtPath:
Rudy@4246
    40
NSInteger comparePluginHandlerRegistrationOrder(id a, id b, void *context);
eridius@2773
    41
eridius@2773
    42
#pragma mark -
eridius@2773
    43
eridius@2773
    44
NSString *GrowlPluginControllerWillAddPluginHandlerNotification = @"GrowlPluginControllerWillAddPluginHandlerNotification";
eridius@2773
    45
NSString *GrowlPluginControllerDidAddPluginHandlerNotification = @"GrowlPluginControllerDidAddPluginHandlerNotification";
eridius@2773
    46
NSString *GrowlPluginControllerWillRemovePluginHandlerNotification = @"GrowlPluginControllerWillRemovePluginHandlerNotification";
eridius@2773
    47
NSString *GrowlPluginControllerDidRemovePluginHandlerNotification = @"GrowlPluginControllerDidRemovePluginHandlerNotification";
eridius@2773
    48
eridius@2773
    49
//Info.plist keys for plug-in bundles.
eridius@2773
    50
NSString *GrowlPluginInfoKeyName              = @"CFBundleName";
eridius@2773
    51
NSString *GrowlPluginInfoKeyAuthor            = @"GrowlPluginAuthor";
eridius@2773
    52
NSString *GrowlPluginInfoKeyVersion           = @"CFBundleVersion";
eridius@2773
    53
NSString *GrowlPluginInfoKeyDescription       = @"GrowlPluginDescription";
eridius@2773
    54
//keys in plug-in description dictionaries (also includes the above).
eridius@2773
    55
NSString *GrowlPluginInfoKeyBundle            = @"GrowlPluginBundle";
eridius@2773
    56
NSString *GrowlPluginInfoKeyTypes             = @"GrowlPluginType";
eridius@2773
    57
NSString *GrowlPluginInfoKeyPath              = @"GrowlPluginPath";
eridius@2773
    58
NSString *GrowlPluginInfoKeyHumanReadableName = @"GrowlPluginHumanReadableName";
eridius@2773
    59
NSString *GrowlPluginInfoKeyIdentifier        = @"GrowlPluginIdentifier";
eridius@2773
    60
NSString *GrowlPluginInfoKeyInstance          = @"GrowlPluginInstance";
eridius@2773
    61
eridius@2773
    62
/*******************************************************************************
eridius@2773
    63
 *  _____ ___  ____   ___
eridius@2773
    64
 * |_   _/ _ \|  _ \ / _ \
eridius@2773
    65
 *   | || | | | | | | | | |
eridius@2773
    66
 *   | || |_| | |_| | |_| |
eridius@2773
    67
 *   |_| \___/|____/ \___/
eridius@2773
    68
 *
eridius@2773
    69
 *******************************************************************************
eridius@2773
    70
 *
eridius@2773
    71
 * 	-	Use identifier strings for all loaded plug-ins (simpler than using
eridius@2773
    72
 *		human-readable names) (DONE though this will probably only be used for
eridius@2773
    73
 *		storage of plug-in prefs)
eridius@2773
    74
 *	-	Use plug-in dictionaries (DONE)
eridius@2773
    75
 *	-	Write the built-in plug-in handler (DONE)
eridius@2773
    76
 *	-	Add a WebKit plug-in handler (jkp)
eridius@2773
    77
 *	-	Better localize human-readable names
eridius@2773
    78
 */
boredzo@1385
    79
boredzo@1385
    80
@implementation GrowlPluginController
boredzo@1385
    81
boredzo@2465
    82
+ (GrowlPluginController *) sharedController {
ofri@2581
    83
	return [self sharedInstance];
ofri@2581
    84
}
ofri@2581
    85
ofri@2581
    86
- (id) initSingleton {
ofri@2581
    87
	if ((self = [super initSingleton])) {
eridius@2773
    88
		bundlesToLazilyInstantiateAnInstanceFrom = [[NSMutableSet alloc] init];
ingmarstein@2943
    89
eridius@2773
    90
		pluginsByIdentifier         = [[NSMutableDictionary alloc] init];
eridius@2773
    91
		pluginIdentifiersByPath     = [[NSMutableDictionary alloc] init];
hg@4440
    92
		pluginIdentifiersByBundle   = [[NSMapTable mapTableWithStrongToStrongObjects] retain];
hg@4440
    93
		pluginIdentifiersByInstance = [[NSMapTable mapTableWithStrongToStrongObjects] retain];
eridius@2773
    94
eridius@2773
    95
		pluginsByName     = [[NSMutableDictionary alloc] init];
eridius@2773
    96
		pluginsByAuthor   = [[NSMutableDictionary alloc] init];
eridius@2773
    97
		pluginsByVersion  = [[NSMutableDictionary alloc] init];
eridius@2773
    98
		pluginsByFilename = [[NSMutableDictionary alloc] init];
eridius@2773
    99
		pluginsByType     = [[NSMutableDictionary alloc] init];
eridius@2773
   100
		pluginHumanReadableNames = [[NSCountedSet alloc] init];
eridius@2773
   101
evands@3839
   102
		loadedBundleIdentifiers = [[NSMutableSet alloc] init];
evands@3839
   103
		
eridius@2773
   104
		allPluginHandlers = [[NSMutableArray alloc] init];
eridius@2773
   105
		pluginHandlers  = [[NSMutableDictionary alloc] init];
hg@4440
   106
		handlersForPlugins = [[NSMapTable mapTableWithStrongToStrongObjects] retain];
eridius@2773
   107
eridius@2773
   108
		displayPlugins = [[NSMutableArray alloc] init];
bgannin@3280
   109
		disabledPlugins = [[NSMutableArray alloc] init];
eridius@2773
   110
eridius@2773
   111
		[self registerDefaultPluginHandlers];
eridius@2773
   112
eridius@2773
   113
		enum { builtInTypesCount = 4U };
eridius@2773
   114
		NSString *builtInTypesArray[builtInTypesCount] = {
eridius@2773
   115
			GROWL_STYLE_EXTENSION,
eridius@2773
   116
			GROWL_VIEW_EXTENSION,
eridius@2773
   117
			GROWL_PATHWAY_EXTENSION,
eridius@2773
   118
			GROWL_PLUGIN_EXTENSION,
eridius@2773
   119
		};
eridius@2773
   120
		CFSetCallBacks callbacks = kCFCopyStringSetCallBacks;
ingmarstein@2540
   121
		callbacks.equal = caseInsensitiveStringComparator;
eridius@2773
   122
		callbacks.hash = passthroughStringHash;
eridius@2773
   123
		builtInTypes = (NSSet *)CFSetCreate(kCFAllocatorDefault,
eridius@2773
   124
		                                    (const void **)builtInTypesArray,
eridius@2773
   125
		                                    builtInTypesCount,
eridius@2773
   126
		                                    &callbacks);
eridius@2773
   127
evands@3839
   128
		//Find plugins inside GHA itself first
evands@3839
   129
		[self findPluginsInDirectory:[[GrowlPathUtilities helperAppBundle] builtInPlugInsPath]];
evands@3839
   130
		
evands@3839
   131
		/* Then find plug-ins in Library/Application Support/Growl/Plugins directories. This allows GHA to override externally installed plugins,
evands@3839
   132
		 * which are fairly common as some 3rd party plugins have been rolled into the Growl distribution.
evands@3839
   133
		 */
boredzo@2465
   134
		NSArray *libraries = NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSAllDomainsMask, YES);
boredzo@2465
   135
		NSEnumerator *enumerator = [libraries objectEnumerator];
boredzo@2465
   136
		NSString *dir;
ingmarstein@1579
   137
		while ((dir = [enumerator nextObject])) {
ingmarstein@1947
   138
			dir = [dir stringByAppendingPathComponent:@"Application Support/Growl/Plugins"];
ingmarstein@1947
   139
			[self findPluginsInDirectory:dir];
evands@3839
   140
		}		
boredzo@1385
   141
	}
ingmarstein@1639
   142
boredzo@1385
   143
	return self;
boredzo@1385
   144
}
boredzo@1385
   145
ofri@2581
   146
- (void) destroy {
ingmarstein@2943
   147
	[pluginsByIdentifier         release];
ingmarstein@2943
   148
	[pluginIdentifiersByPath     release];
ingmarstein@2943
   149
	[pluginIdentifiersByBundle   release];
eridius@2773
   150
	[pluginIdentifiersByInstance release];
eridius@2773
   151
eridius@2773
   152
	[pluginsByName     release];
eridius@2773
   153
	[pluginsByAuthor   release];
eridius@2773
   154
	[pluginsByVersion  release];
eridius@2773
   155
	[pluginsByFilename release];
eridius@2773
   156
	[pluginsByType     release];
eridius@2773
   157
	[pluginHumanReadableNames release];
eridius@2773
   158
evands@3839
   159
	[loadedBundleIdentifiers release];
evands@3839
   160
eridius@2773
   161
	[bundlesToLazilyInstantiateAnInstanceFrom release];
eridius@2773
   162
	[displayPlugins release];
bgannin@3280
   163
	[disabledPlugins release];
eridius@2773
   164
eridius@2773
   165
	[allPluginHandlers release];
eridius@2773
   166
	[pluginHandlers  release];
eridius@2773
   167
	[handlersForPlugins release];
eridius@2773
   168
eridius@2773
   169
	[builtInTypes release];
eridius@2773
   170
eridius@2773
   171
	[cache_allPlugins release];
eridius@2773
   172
	[cache_allPluginsArray release];
eridius@2773
   173
	[cache_registeredPluginTypes release];
eridius@2773
   174
	[cache_registeredPluginNames release];
eridius@2773
   175
	[cache_registeredPluginNamesArray release];
eridius@2773
   176
	[cache_allPluginInstances release];
eridius@2773
   177
	[cache_displayPlugins release];
boredzo@2465
   178
ofri@2581
   179
	[super destroy];
boredzo@2465
   180
}
boredzo@2465
   181
boredzo@1385
   182
#pragma mark -
boredzo@1385
   183
eridius@2773
   184
- (void) registerDefaultPluginHandlers {
eridius@2773
   185
	//register ourselves for display plug-ins (non-WebKit), pathway plug-ins, and functional plug-ins.
eridius@2773
   186
	NSSet *types = [[NSSet alloc] initWithObjects:
eridius@2773
   187
		//display plug-ins
eridius@2773
   188
		GROWL_VIEW_EXTENSION,
eridius@2773
   189
		NSFileTypeForHFSTypeCode(FOUR_CHAR_CODE('DISP')),
eridius@2773
   190
		//pathway plug-ins
eridius@2773
   191
		GROWL_PATHWAY_EXTENSION,
eridius@2773
   192
		NSFileTypeForHFSTypeCode(FOUR_CHAR_CODE('PWAY')),
eridius@2773
   193
		//generic functional plug-ins
eridius@2773
   194
		GROWL_PLUGIN_EXTENSION,
eridius@2773
   195
		NSFileTypeForHFSTypeCode(FOUR_CHAR_CODE('GEXT')),
eridius@2773
   196
		nil];
eridius@2773
   197
eridius@2773
   198
	[self addPluginHandler:self forPluginTypes:types];
eridius@2773
   199
eridius@2773
   200
	[types release];
eridius@2773
   201
eridius@2773
   202
	[GrowlWebKitPluginHandler sharedInstance];		// Calling this here will cause the handler to register
eridius@2773
   203
}
eridius@2773
   204
eridius@2773
   205
#pragma mark -
eridius@2773
   206
#pragma mark GrowlPluginHandler protocol conformance
eridius@2773
   207
eridius@2773
   208
//the method that dispatches incoming plug-ins to plug-in handlers is -dispatchPluginAtPath:.
eridius@2773
   209
//this is for handling plug-ins of the built-in types.
eridius@2773
   210
- (BOOL)loadPluginWithBundle:(NSBundle *)bundle {
eridius@2773
   211
	[self addPluginInstance:nil fromBundle:bundle];
eridius@2773
   212
eridius@2773
   213
	return YES;
eridius@2773
   214
}
eridius@2773
   215
eridius@2773
   216
#pragma mark -
eridius@2773
   217
#pragma mark Plugin-handler handling
eridius@2773
   218
eridius@2773
   219
- (void) addPluginHandler:(id <GrowlPluginHandler>)handler forPluginTypes:(NSSet *)types {
eridius@2773
   220
	NSParameterAssert(handler != nil);
eridius@2773
   221
	NSParameterAssert(types != nil);
eridius@2773
   222
eridius@2773
   223
	if (![types count]) {
eridius@2773
   224
		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);
eridius@2773
   225
	} else {
eridius@2773
   226
		//make sure nobody tries to register a plug-in handler for a built-in type.
eridius@2773
   227
		if (builtInTypes) {
eridius@2773
   228
			NSMutableSet *builtInMutable = [builtInTypes mutableCopy];
eridius@2773
   229
			[builtInMutable intersectSet:types];
eridius@2773
   230
eridius@2773
   231
			//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.
eridius@2773
   232
			NSAssert2([builtInMutable count] == 0U, @"Something attempted to register a plug-in handler for one or more reserved file types (%@). The handler was %@.", builtInMutable, handler);
eridius@2773
   233
eridius@2773
   234
			[builtInMutable release];
eridius@2773
   235
		}
eridius@2773
   236
eridius@2773
   237
		NSEnumerator *typeEnum = [types objectEnumerator];
eridius@2773
   238
		NSString *type;
eridius@2773
   239
		while ((type = [typeEnum nextObject])) {
eridius@2773
   240
			//normalise: strip leading ., if any.
Rudy@4246
   241
			NSUInteger i = 0U, len = [type length];
eridius@2773
   242
			for (; i < len; ++i) {
eridius@2773
   243
				if ([type characterAtIndex:i] != '.')
eridius@2773
   244
					break;
eridius@2773
   245
			}
eridius@2773
   246
			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);
eridius@2773
   247
			if (i)
eridius@2773
   248
				type = [type substringFromIndex:i];
eridius@2773
   249
eridius@2773
   250
			NSMutableArray *handlers = [pluginHandlers objectForKey:type];
eridius@2773
   251
			if (!handlers) {
eridius@2773
   252
				handlers = [[NSMutableArray alloc] initWithCapacity:1U];
eridius@2773
   253
				[pluginHandlers setObject:handlers forKey:type];
eridius@2773
   254
				[handlers release];
eridius@2773
   255
			}
eridius@2773
   256
			[handlers addObject:handler];
eridius@2773
   257
		}
eridius@2773
   258
boredzo@2954
   259
		[allPluginHandlers addObject:handler];
boredzo@2954
   260
eridius@2773
   261
		NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
rudy@3304
   262
		NSDictionary *notificationUserInfo = [[[NSDictionary alloc] initWithObjectsAndKeys:
eridius@2773
   263
			handler, @"GrowlPluginHandler",
rudy@3304
   264
			nil] autorelease];
eridius@2773
   265
eridius@2773
   266
		[nc postNotificationName:GrowlPluginControllerWillAddPluginHandlerNotification
eridius@2773
   267
						  object:self
eridius@2773
   268
						userInfo:notificationUserInfo];
eridius@2773
   269
eridius@2773
   270
		[allPluginHandlers addObject:handler];
eridius@2773
   271
eridius@2773
   272
		//add the handler as an observer for various notifications.
eridius@2773
   273
		if ([handler respondsToSelector:@selector(growlPluginControllerWillAddPluginHandler:)]) {
eridius@2773
   274
			[nc addObserver:handler
eridius@2773
   275
				   selector:@selector(growlPluginControllerWillAddPluginHandler:)
eridius@2773
   276
					   name:GrowlPluginControllerWillAddPluginHandlerNotification
eridius@2773
   277
					 object:self];
eridius@2773
   278
		}
eridius@2773
   279
		if ([handler respondsToSelector:@selector(growlPluginControllerDidAddPluginHandler:)]) {
eridius@2773
   280
			[nc addObserver:handler
eridius@2773
   281
				   selector:@selector(growlPluginControllerDidAddPluginHandler:)
eridius@2773
   282
					   name:GrowlPluginControllerDidAddPluginHandlerNotification
eridius@2773
   283
					 object:self];
eridius@2773
   284
		}
eridius@2773
   285
		if ([handler respondsToSelector:@selector(growlPluginControllerWillRemovePluginHandler:)]) {
eridius@2773
   286
			[nc addObserver:handler
eridius@2773
   287
				   selector:@selector(growlPluginControllerWillRemovePluginHandler:)
eridius@2773
   288
					   name:GrowlPluginControllerWillRemovePluginHandlerNotification
eridius@2773
   289
					 object:self];
eridius@2773
   290
		}
eridius@2773
   291
		if ([handler respondsToSelector:@selector(growlPluginControllerDidRemovePluginHandler:)]) {
eridius@2773
   292
			[nc addObserver:handler
eridius@2773
   293
				   selector:@selector(growlPluginControllerDidRemovePluginHandler:)
eridius@2773
   294
					   name:GrowlPluginControllerDidRemovePluginHandlerNotification
eridius@2773
   295
					 object:self];
eridius@2773
   296
		}
eridius@2773
   297
eridius@2773
   298
		[nc postNotificationName:GrowlPluginControllerDidAddPluginHandlerNotification
eridius@2773
   299
						  object:self
eridius@2773
   300
						userInfo:notificationUserInfo];
eridius@2773
   301
	}
eridius@2773
   302
}
bgannin@3280
   303
eridius@2773
   304
- (void) removePluginHandler:(id <GrowlPluginHandler>)handler forPluginTypes:(NSSet *)extensions {
eridius@2773
   305
	NSParameterAssert(handler != nil);
eridius@2773
   306
eridius@2773
   307
	[allPluginHandlers removeObjectIdenticalTo:handler];
eridius@2773
   308
eridius@2773
   309
	if (!extensions)
eridius@2773
   310
		extensions = (NSSet *)[pluginHandlers allKeysForObject:handler];
eridius@2773
   311
eridius@2773
   312
	NSEnumerator *extEnum = [extensions objectEnumerator];
eridius@2773
   313
	NSString *ext;
eridius@2773
   314
	while ((ext = [extEnum nextObject])) {
eridius@2773
   315
		NSMutableArray *handlers = [pluginHandlers objectForKey:ext];
eridius@2773
   316
		if (handlers) {
Rudy@4246
   317
			NSUInteger idx = [handlers indexOfObjectIdenticalTo:handler];
eridius@2773
   318
			if (idx != NSNotFound)
eridius@2773
   319
				[handlers removeObjectAtIndex:idx];
eridius@2773
   320
		}
eridius@2773
   321
	}
eridius@2773
   322
}
eridius@2773
   323
eridius@2773
   324
- (NSArray *) allPluginHandlers {
eridius@2773
   325
	return [[allPluginHandlers copy] autorelease];
eridius@2773
   326
}
eridius@2773
   327
eridius@2773
   328
#pragma mark -
eridius@2773
   329
eridius@2773
   330
//private method.
eridius@2773
   331
- (NSDictionary *) addPluginInstance:(GrowlPlugin *)plugin fromPath:(NSString *)path bundle:(NSBundle *)bundle {
evands@3839
   332
	//If we're passed a bundle, refuse to load it if we've already loaded a bundle with the same identifier, instead returning early.
evands@3839
   333
	NSString *bundleIdentifier = (bundle ? [bundle objectForInfoDictionaryKey:(NSString *)kCFBundleIdentifierKey] : nil);
evands@3839
   334
	if (bundleIdentifier && [loadedBundleIdentifiers containsObject:[bundle objectForInfoDictionaryKey:(NSString *)kCFBundleIdentifierKey]]) {
evands@3839
   335
		return nil;
evands@3839
   336
	}
evands@3839
   337
	
evands@3839
   338
	//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.
eridius@2773
   339
	NSString *identifier = nil;
eridius@2773
   340
	if (plugin)
eridius@2773
   341
		identifier = [pluginIdentifiersByInstance objectForKey:plugin];
eridius@2773
   342
	else if (bundle)
eridius@2773
   343
		identifier = [pluginIdentifiersByBundle   objectForKey:bundle];
eridius@2773
   344
	else if (path)
eridius@2773
   345
		identifier = [pluginIdentifiersByPath     objectForKey:path];
eridius@2773
   346
evands@3839
   347
	/* If we have an identifier, look up the plug-in dictionary.
evands@3839
   348
	 * 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.
evands@3839
   349
	 */
eridius@2773
   350
	NSMutableDictionary *pluginDict = identifier ? [pluginsByIdentifier objectForKey:identifier] : nil;
eridius@2773
   351
	if (pluginDict && !plugin)
eridius@2773
   352
		plugin = [pluginDict pluginInstance];
eridius@2773
   353
boredzo@3587
   354
	//Assert that we have an instance OR a bundle. We need at least one to proceed.
eridius@2773
   355
	NSAssert1(plugin || bundle, @"Cannot load plug-ins lazily without a bundle (path: %@)", path);
eridius@2773
   356
boredzo@3587
   357
	//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.
bgannin@3280
   358
	NSString *name    = plugin ? ([plugin respondsToSelector:@selector(name)] ? [plugin name] : [bundle objectForInfoDictionaryKey:(NSString *)kCFBundleNameKey]) 
bgannin@3280
   359
								: [bundle objectForInfoDictionaryKey:(NSString *)kCFBundleNameKey];
bgannin@3280
   360
	NSString *author  = plugin ? ([plugin respondsToSelector:@selector(author)] ? [plugin author] : [bundle objectForInfoDictionaryKey:GrowlPluginInfoKeyAuthor])
bgannin@3280
   361
							    : [bundle objectForInfoDictionaryKey:GrowlPluginInfoKeyAuthor];
bgannin@3280
   362
	NSString *version = plugin ? ([plugin respondsToSelector:@selector(version)] ? [plugin version] : [bundle objectForInfoDictionaryKey:(NSString *)kCFBundleVersionKey])
bgannin@3280
   363
							    : [bundle objectForInfoDictionaryKey:(NSString *)kCFBundleVersionKey];
boredzo@3587
   364
boredzo@3587
   365
	//If we don't have a pathname, get it as the bundle's pathname.
eridius@2773
   366
	if (!path)
eridius@2773
   367
		path = [bundle bundlePath];
boredzo@2957
   368
	NSString *extension = [path pathExtension];
boredzo@2957
   369
	NSString *fileType = nil;
eridius@2773
   370
boredzo@3587
   371
	//Assert that we have a name, author, and version. (We got the path first so we can use it in the assertion message.)
eridius@2773
   372
	NSAssert5((name != nil) && (author != nil) && (version != nil),
eridius@2773
   373
			  @"Cannot load plug-in at path %@ (plug-in instance's class: %@). One of these is (null), but they must all not be:\n"
eridius@2773
   374
			  @"\t"@"   name: %@\n"
eridius@2773
   375
			  @"\t"@" author: %@\n"
eridius@2773
   376
			  @"\t"@"version: %@\n",
eridius@2773
   377
			  path, [plugin class], name, author, version);
eridius@2773
   378
boredzo@3587
   379
	//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.
boredzo@3587
   380
	//Note: This isn't a performance hit when the strings are immutable. -copy = -retain in that situation. Thanks, Apple!
eridius@2773
   381
	name    = [name    copy];
eridius@2773
   382
	author  = [author  copy];
eridius@2773
   383
	version = [version copy];
eridius@2773
   384
boredzo@3587
   385
	//If we don't have an identifier yet, forge it.
eridius@2773
   386
	if (!identifier)
eridius@2773
   387
		identifier = [NSString stringWithFormat:@"Name: %@ Author: %@ Path: %@", name, author, path];
boredzo@3587
   388
boredzo@3587
   389
	//If we don't have an instance but we do have a bundle, see if we've previously queued the bundle for lazy instantiation.
boredzo@2954
   390
	if (!plugin && bundle) {
boredzo@3587
   391
		if (![bundlesToLazilyInstantiateAnInstanceFrom containsObject:bundle]) {
boredzo@3587
   392
			//We haven't previously queued it: Queue it.
boredzo@3587
   393
			[bundlesToLazilyInstantiateAnInstanceFrom addObject:bundle];
boredzo@3587
   394
		} else {
boredzo@3587
   395
			//We have: This is our cue to instantiate it.
hg@4441
   396
			plugin = [[[[bundle principalClass] alloc] init] autorelease];
boredzo@3587
   397
			//Dequeue it, because we don't want to hit this branch again for this plug-in.
boredzo@3587
   398
			[bundlesToLazilyInstantiateAnInstanceFrom removeObject:bundle];
boredzo@3587
   399
			//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).
boredzo@3587
   400
			[pluginDict setObject:plugin forKey:GrowlPluginInfoKeyInstance];
boredzo@3587
   401
		}
boredzo@3587
   402
	}
boredzo@3587
   403
boredzo@3587
   404
	if (!plugin && bundle) {
boredzo@3587
   405
		//*Still* no plug-in! Again we check whether it's queued for instantiation (bug?).
boredzo@2954
   406
		if (![bundlesToLazilyInstantiateAnInstanceFrom containsObject:bundle])
boredzo@2954
   407
			[bundlesToLazilyInstantiateAnInstanceFrom addObject:bundle];
boredzo@2954
   408
		else {
boredzo@3587
   409
			//Apparently it is. Instantiate it, but don't stash the plug-in instance in the plug-in dictionary (why not?).
hg@4441
   410
			plugin = [[[[bundle principalClass] alloc] init] autorelease];
boredzo@2954
   411
			[bundlesToLazilyInstantiateAnInstanceFrom removeObject:bundle];
boredzo@3587
   412
		}
boredzo@3587
   413
	}
boredzo@3587
   414
boredzo@3587
   415
	/*If we don't actually have a plug-in dictionary, create it.
boredzo@3587
   416
	 *Elements of a plug-in dictionary:
boredzo@3587
   417
	 *	Plug-in name
boredzo@3587
   418
	 *	Author
boredzo@3587
   419
	 *	Version
boredzo@3587
   420
	 *	Pathname
boredzo@3587
   421
	 *	Identifier
boredzo@3587
   422
	 *	Instance (later; see above lazy-instantiation code)
boredzo@3587
   423
	 *	Plug-in's HFS type and filename extension (combined in a set)
boredzo@3587
   424
	 */
boredzo@2957
   425
	BOOL pluginDictIsNew = !pluginDict;
eridius@2773
   426
	if (!pluginDict) {
eridius@2773
   427
		pluginDict = [NSMutableDictionary dictionaryWithObjectsAndKeys:
eridius@2773
   428
			name,                 GrowlPluginInfoKeyName,
eridius@2773
   429
			author,               GrowlPluginInfoKeyAuthor,
eridius@2773
   430
			version,              GrowlPluginInfoKeyVersion,
eridius@2773
   431
			path,                 GrowlPluginInfoKeyPath,
boredzo@2957
   432
			identifier,           GrowlPluginInfoKeyIdentifier,
eridius@2773
   433
			nil];
bgannin@3280
   434
		NSString *description = ([plugin respondsToSelector:@selector(pluginDescription)] ? [plugin pluginDescription] : nil);
bgannin@3280
   435
		
eridius@2773
   436
		if (description)
eridius@2773
   437
			[pluginDict setObject:description forKey:GrowlPluginInfoKeyDescription];
eridius@2773
   438
eridius@2773
   439
		[[NSWorkspace sharedWorkspace] getFileType:&fileType creatorCode:NULL forFile:path];
eridius@2773
   440
boredzo@3587
   441
		//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).
evands@2927
   442
		NSSet *types = nil;
eridius@2773
   443
		if (extension) {
evands@2917
   444
#warning problem here...
evands@2917
   445
			///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???
eridius@2773
   446
			if (fileType)
eridius@2773
   447
				types = [NSSet setWithObjects:extension, fileType, nil];
eridius@2773
   448
			else
eridius@2773
   449
				types = [NSSet setWithObject:extension];
eridius@2773
   450
		} else if (fileType)
eridius@2773
   451
			types = [NSSet setWithObject:fileType];
ingmarstein@2943
   452
evands@2927
   453
		if (types)
evands@2927
   454
			[pluginDict setObject:types forKey:GrowlPluginInfoKeyTypes];
boredzo@2957
   455
	}
boredzo@2957
   456
boredzo@3587
   457
	//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.
boredzo@2957
   458
	if (bundle) {
boredzo@2957
   459
		if (![pluginDict objectForKey:GrowlPluginInfoKeyBundle])
boredzo@2957
   460
			[pluginDict setObject:bundle forKey:GrowlPluginInfoKeyBundle];
boredzo@2957
   461
		[pluginIdentifiersByBundle setObject:identifier forKey:bundle];
boredzo@2957
   462
	}
boredzo@3587
   463
	//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.
boredzo@2957
   464
	if (plugin) {
boredzo@2957
   465
		if (![pluginDict objectForKey:GrowlPluginInfoKeyInstance])
boredzo@2957
   466
			[pluginDict setObject:plugin forKey:GrowlPluginInfoKeyInstance];
boredzo@2957
   467
		[pluginIdentifiersByInstance setObject:identifier forKey:plugin];
boredzo@2957
   468
	}
boredzo@2957
   469
boredzo@3587
   470
	//If we just created the dictionary (and got done filling it out), start storing it in places.
boredzo@2957
   471
	if (pluginDictIsNew) {
eridius@2773
   472
		[pluginsByIdentifier setObject:pluginDict forKey:identifier];
eridius@2773
   473
		[pluginIdentifiersByPath setObject:identifier forKey:path];
ingmarstein@2943
   474
eridius@2773
   475
	#define ADD_TO_DICT(dictName, key, value)                                          \
eridius@2773
   476
			do {                                                                        \
eridius@2773
   477
				NSMutableSet *plugins = [dictName objectForKey:key];                     \
eridius@2773
   478
				if (plugins)                                                              \
eridius@2773
   479
					[plugins addObject:value];                                             \
eridius@2773
   480
				else                                                                        \
eridius@2773
   481
					[dictName setObject:[NSMutableSet setWithObject:value] forKey:key];      \
eridius@2773
   482
			} while(0)
eridius@2773
   483
		ADD_TO_DICT(pluginsByName,     name,                     pluginDict);
eridius@2773
   484
		ADD_TO_DICT(pluginsByAuthor,   author,                   pluginDict);
eridius@2773
   485
		ADD_TO_DICT(pluginsByVersion,  version,                  pluginDict);
eridius@2773
   486
		ADD_TO_DICT(pluginsByFilename, [path lastPathComponent], pluginDict);
ingmarstein@2943
   487
eridius@2773
   488
		ADD_TO_DICT(pluginsByType, extension, pluginDict);
ingmarstein@2943
   489
		ADD_TO_DICT(pluginsByType, fileType,  pluginDict);
eridius@2773
   490
	#undef ADD_TO_DICT
eridius@2773
   491
	}
evands@2927
   492
boredzo@3587
   493
	//Release our copies.
eridius@2773
   494
	[name    release];
eridius@2773
   495
	[author  release];
eridius@2773
   496
	[version release];
eridius@2773
   497
boredzo@3587
   498
	//Invalidate non-display plug-in caches.
eridius@2773
   499
	[cache_allPlugins release];
eridius@2773
   500
	 cache_allPlugins = nil;
eridius@2773
   501
	[cache_allPluginsArray release];
eridius@2773
   502
	 cache_allPluginsArray = nil;
eridius@2773
   503
	[cache_registeredPluginTypes release];
eridius@2773
   504
	 cache_registeredPluginTypes = nil;
eridius@2773
   505
	[cache_registeredPluginNames release];
eridius@2773
   506
	 cache_registeredPluginNames = nil;
eridius@2773
   507
	[cache_registeredPluginNamesArray release];
eridius@2773
   508
	 cache_registeredPluginNamesArray = nil;
eridius@2773
   509
boredzo@3587
   510
	//Special handling if this plug-in is a display.
eridius@2773
   511
	if ([self pluginWithDictionaryIsDisplayPlugin:pluginDict]) {
boredzo@3587
   512
		//If it doesn't respond to -requiresPositioning, it's old. Add it as a disabled plug-in.
bgannin@3280
   513
		if(![[pluginDict valueForKey:GrowlPluginInfoKeyInstance] respondsToSelector:@selector(requiresPositioning)]) {
bgannin@3280
   514
			[disabledPlugins addObject:[pluginDict valueForKey:GrowlPluginInfoKeyName]];
bgannin@3280
   515
		} 
bgannin@3280
   516
		else {
boredzo@3587
   517
			//It responds to -requiresPositioning, so add it as a(n enabled) display plug-in.
bgannin@3280
   518
			[displayPlugins addObject:pluginDict];
bgannin@3280
   519
		}
bgannin@3280
   520
		
boredzo@3587
   521
		//Invalidate display plug-in cache.
eridius@2773
   522
		[cache_displayPlugins release];
eridius@2773
   523
		 cache_displayPlugins = nil;
eridius@2773
   524
	}
eridius@2773
   525
evands@3839
   526
	//Store the bundle identifier so we know we've loaded it.
evands@3839
   527
	if (bundleIdentifier) {
evands@3839
   528
		[loadedBundleIdentifiers addObject:bundleIdentifier];
evands@3839
   529
	}
evands@3839
   530
eridius@2773
   531
	return pluginDict;
eridius@2773
   532
}
eridius@2773
   533
bgannin@3280
   534
- (NSArray *) disabledPlugins {
bgannin@3280
   535
	return disabledPlugins;
bgannin@3280
   536
}
bgannin@3280
   537
bgannin@3280
   538
- (BOOL) disabledPluginsPresent {
bgannin@3280
   539
	return ([disabledPlugins count] > 0);
bgannin@3280
   540
}
bgannin@3280
   541
eridius@2773
   542
- (NSDictionary *) addPluginInstance:(GrowlPlugin *)plugin fromBundle:(NSBundle *)bundle {
eridius@2773
   543
	return [self addPluginInstance:plugin fromPath:nil bundle:bundle];
eridius@2773
   544
}
eridius@2773
   545
- (NSDictionary *) addPluginInstance:(GrowlPlugin *)plugin fromPath:(NSString *)path {
eridius@2773
   546
	return [self addPluginInstance:plugin fromPath:path bundle:nil];
eridius@2773
   547
}
eridius@2773
   548
eridius@2773
   549
#pragma mark -
eridius@2773
   550
eridius@2773
   551
- (NSSet *) registeredPluginTypes {
eridius@2773
   552
	if (!cache_registeredPluginTypes)
eridius@2773
   553
		cache_registeredPluginTypes = [[NSSet alloc] initWithArray:[pluginHandlers allKeys]];
eridius@2773
   554
eridius@2773
   555
	return cache_registeredPluginTypes;
eridius@2773
   556
}
eridius@2773
   557
eridius@2773
   558
- (NSSet *) registeredPluginNames {
eridius@2773
   559
	if (!cache_registeredPluginNames)
eridius@2773
   560
		cache_registeredPluginNames = [[NSSet alloc] initWithArray:[self registeredPluginNamesArray]];
eridius@2773
   561
	return cache_registeredPluginNames;
eridius@2773
   562
}
bgannin@3280
   563
eridius@2773
   564
- (NSArray *) registeredPluginNamesArray {
eridius@2773
   565
	if (!cache_registeredPluginNamesArray) {
eridius@2773
   566
		cache_registeredPluginNamesArray = [[[pluginsByIdentifier allValues] valueForKey:GrowlPluginInfoKeyName] retain];
eridius@2773
   567
	}
eridius@2773
   568
	return cache_registeredPluginNamesArray;
eridius@2773
   569
}
eridius@2773
   570
eridius@2773
   571
- (NSArray *) registeredPluginNamesArrayForType:(NSString *)type {
eridius@2773
   572
#warning this should be cached per type
eridius@2773
   573
	return [[[pluginsByType valueForKey:type] allObjects] valueForKey:GrowlPluginInfoKeyName];
eridius@2773
   574
}
eridius@2773
   575
eridius@2773
   576
eridius@2773
   577
#pragma mark -
eridius@2773
   578
eridius@2773
   579
- (NSArray *) allPluginDictionariesArray {
eridius@2773
   580
	if (!cache_allPluginsArray)
eridius@2773
   581
		cache_allPluginsArray = [[pluginsByIdentifier allValues] copy];
eridius@2773
   582
	return cache_allPluginsArray;
eridius@2773
   583
}
eridius@2773
   584
- (NSSet *) allPluginDictionaries {
eridius@2773
   585
	if (!cache_allPlugins)
eridius@2773
   586
		cache_allPlugins = [[NSMutableSet alloc] initWithArray:[self allPluginDictionariesArray]];
eridius@2773
   587
	return cache_allPlugins;
eridius@2773
   588
}
eridius@2773
   589
- (NSArray *) allPluginInstances {
eridius@2773
   590
	if (!cache_allPluginInstances)
eridius@2773
   591
		cache_allPluginInstances = [[[self allPluginDictionaries] valueForKey:GrowlPluginInfoKeyInstance] retain];
eridius@2773
   592
	return cache_allPluginInstances;
eridius@2773
   593
}
eridius@2773
   594
eridius@2773
   595
- (NSSet *) pluginDictionariesWithName:(NSString *)name author:(NSString *)author version:(NSString *)version type:(NSString *)type {
eridius@2773
   596
	NSMutableSet *matches = [[[self allPluginDictionaries] mutableCopy] autorelease];
eridius@2773
   597
eridius@2773
   598
	if ([matches count]) {
eridius@2773
   599
		if (name)
boredzo@3003
   600
			[matches intersectSet:[pluginsByName objectForKey:name]];
eridius@2773
   601
		if (author)
boredzo@3003
   602
			[matches intersectSet:[pluginsByAuthor objectForKey:author]];
eridius@2773
   603
		if (version)
boredzo@3003
   604
			[matches intersectSet:[pluginsByVersion objectForKey:version]];
eridius@2773
   605
		if (type)
boredzo@3003
   606
			[matches intersectSet:[pluginsByType objectForKey:type]];
eridius@2773
   607
	}
eridius@2773
   608
eridius@2773
   609
	return matches;
eridius@2773
   610
}
eridius@2773
   611
- (NSDictionary *) pluginDictionaryWithName:(NSString *)name author:(NSString *)author version:(NSString *)version type:(NSString *)type {
eridius@2773
   612
	NSSet *matches = [self pluginDictionariesWithName:name author:author version:version type:type];
ingmarstein@2941
   613
	if ([matches count] == 1U)
eridius@2773
   614
		return [matches anyObject];
eridius@2773
   615
	else
eridius@2773
   616
		return nil;
eridius@2773
   617
}
eridius@2773
   618
- (NSDictionary *) pluginDictionaryWithName:(NSString *)name {
eridius@2773
   619
	return [self pluginDictionaryWithName:name author:nil version:nil type:nil];
eridius@2773
   620
}
eridius@2773
   621
- (GrowlPlugin *) pluginInstanceWithName:(NSString *)name author:(NSString *)author version:(NSString *)version type:(NSString *)type {
eridius@2773
   622
	NSDictionary *pluginDict = [self pluginDictionaryWithName:name author:author version:version type:type];
eridius@2773
   623
	GrowlPlugin *instance = [pluginDict pluginInstance];
eridius@2773
   624
	if (!instance) {
eridius@2773
   625
		NSBundle *bundle = [pluginDict pluginBundle];
eridius@2773
   626
		if (bundle) {
eridius@2773
   627
			[self addPluginInstance:nil fromBundle:bundle]; //causes instantiation
eridius@2773
   628
			instance = [pluginDict pluginInstance];
eridius@2773
   629
		}
eridius@2773
   630
	}
eridius@2773
   631
	return instance;
eridius@2773
   632
}
eridius@2773
   633
- (GrowlPlugin *) pluginInstanceWithName:(NSString *)name {
eridius@2773
   634
	return [self pluginInstanceWithName:name author:nil version:nil type:nil];
eridius@2773
   635
}
eridius@2773
   636
eridius@2773
   637
#pragma mark -
eridius@2773
   638
eridius@2773
   639
- (NSString *) humanReadableNameForPluginWithDictionary:(NSDictionary *)pluginDict {
eridius@2773
   640
	NSString *humanReadableName = [pluginDict pluginHumanReadableName];
eridius@2773
   641
eridius@2773
   642
	if (!humanReadableName) {
eridius@2773
   643
		NSString *name = [pluginDict pluginName];
eridius@2773
   644
		if ([[pluginsByName objectForKey:name] count] == 1U)
eridius@2773
   645
			humanReadableName = name;
eridius@2773
   646
		else {
eridius@2773
   647
			NSString *author = [pluginDict pluginAuthor];
eridius@2773
   648
			if ([[pluginsByAuthor objectForKey:author] count] == 1U)
eridius@2773
   649
				humanReadableName = [NSString stringWithFormat:@"%@ (by %@)", name, author]; //XXX LOCALIZEME
eridius@2773
   650
			else {
eridius@2773
   651
				NSString *filename = [[pluginDict pluginPath] lastPathComponent];
eridius@2773
   652
				if ([[pluginsByFilename objectForKey:filename] count] == 1U)
eridius@2773
   653
					humanReadableName = [NSString stringWithFormat:@"%@ (filename %@)", name, filename]; //XXX LOCALIZEME
eridius@2773
   654
				else {
eridius@2773
   655
					humanReadableName = [NSString stringWithFormat:@"%@ (by %@, filename %@)", name, author, filename]; //XXX LOCALIZEME
Rudy@4246
   656
					NSUInteger count = [pluginHumanReadableNames countForObject:humanReadableName];
eridius@2773
   657
					[pluginHumanReadableNames addObject:humanReadableName];
eridius@2773
   658
					if (count > 1U)
eridius@2773
   659
						humanReadableName = [NSString stringWithFormat:@"%@ %u", humanReadableName, count];
eridius@2773
   660
				}
eridius@2773
   661
			}
eridius@2773
   662
		}
eridius@2773
   663
eridius@2773
   664
		if ([pluginDict isKindOfClass:[NSMutableDictionary class]]) {
eridius@2773
   665
			//save the name for later retrieval.
eridius@2773
   666
			[(NSMutableDictionary *)pluginDict setObject:humanReadableName forKey:GrowlPluginInfoKeyHumanReadableName];
eridius@2773
   667
		}
eridius@2773
   668
	}
eridius@2773
   669
eridius@2773
   670
	return humanReadableName;
eridius@2773
   671
}
eridius@2773
   672
eridius@2773
   673
- (BOOL) pluginWithDictionaryIsDisplayPlugin:(NSDictionary *)pluginDict {
eridius@2773
   674
	GrowlPlugin *instance = [pluginDict pluginInstance];
eridius@2773
   675
	if (instance)
eridius@2773
   676
		return [instance isKindOfClass:[GrowlDisplayPlugin class]];
eridius@2773
   677
	else {
eridius@2773
   678
		NSBundle *bundle = [pluginDict pluginBundle];
eridius@2773
   679
		NSAssert1(bundle, @"no instance or bundle in plug-in dictionary! description of dictionary follows\n%@", pluginDict);
eridius@2773
   680
		Class principalClass = [bundle principalClass];
eridius@2773
   681
		NSAssert1(bundle, @"bundle in plug-in dictionary has no principal class! description of dictionary follows\n%@", pluginDict);
eridius@2773
   682
		return [principalClass isSubclassOfClass:[GrowlDisplayPlugin class]];
eridius@2773
   683
	}
eridius@2773
   684
}
eridius@2773
   685
eridius@2773
   686
#pragma mark -
evands@2916
   687
#warning XXX all of this could potentially go bye-bye if it is not needed
boredzo@2465
   688
boredzo@2465
   689
- (NSArray *) displayPlugins {
eridius@2773
   690
	if (!cache_displayPlugins)
eridius@2773
   691
		cache_displayPlugins = [[NSArray alloc] initWithArray:displayPlugins];
eridius@2773
   692
	return cache_displayPlugins;
eridius@2773
   693
}
eridius@2773
   694
eridius@2773
   695
#pragma mark -
eridius@2773
   696
eridius@2773
   697
- (NSSet *) displayPluginDictionariesWithName:(NSString *)name author:(NSString *)author version:(NSString *)version type:(NSString *)type {
eridius@2773
   698
	NSMutableSet *matches = (NSMutableSet *)[self pluginDictionariesWithName:name
eridius@2773
   699
																	  author:author
eridius@2773
   700
																	 version:version
eridius@2773
   701
																		type:type];
eridius@2773
   702
eridius@2773
   703
	NSSet *copyForIteration = [matches copy];
eridius@2773
   704
	NSEnumerator *matchesEnum = [copyForIteration objectEnumerator];
eridius@2773
   705
	NSDictionary *pluginDict;
eridius@2773
   706
	while ((pluginDict = [matchesEnum nextObject])) {
eridius@2773
   707
		if (![self pluginWithDictionaryIsDisplayPlugin:pluginDict])
eridius@2773
   708
			[matches removeObject:pluginDict];
eridius@2773
   709
	}
evands@3999
   710
	[copyForIteration release];
eridius@2773
   711
eridius@2773
   712
	return matches;
eridius@2773
   713
}
bgannin@3280
   714
eridius@2773
   715
- (NSDictionary *) displayPluginDictionaryWithName:(NSString *)name author:(NSString *)author version:(NSString *)version type:(NSString *)type {
eridius@2773
   716
	NSSet *matches = [self displayPluginDictionariesWithName:name
eridius@2773
   717
													  author:author
eridius@2773
   718
													 version:version
eridius@2773
   719
														type:type];
eridius@2773
   720
	if ([matches count] == 1U)
eridius@2773
   721
		return [matches anyObject];
eridius@2773
   722
	else
eridius@2773
   723
		return nil;
eridius@2773
   724
}
bgannin@3280
   725
eridius@2773
   726
- (GrowlDisplayPlugin *) displayPluginInstanceWithName:(NSString *)name author:(NSString *)author version:(NSString *)version type:(NSString *)type {
eridius@2773
   727
	GrowlPlugin *plugin = [self pluginInstanceWithName:name author:author version:version type:type];
eridius@2773
   728
	if (plugin && [plugin isKindOfClass:[GrowlDisplayPlugin class]])
eridius@2773
   729
		return (GrowlDisplayPlugin *)plugin;
eridius@2773
   730
	else
eridius@2773
   731
		return nil;
eridius@2773
   732
}
eridius@2773
   733
eridius@2773
   734
#pragma mark -
eridius@2773
   735
#pragma mark Finding and using installed plug-ins
ingmarstein@2540
   736
ingmarstein@1947
   737
- (void) findPluginsInDirectory:(NSString *)dir {
ingmarstein@1624
   738
	NSDirectoryEnumerator *enumerator = [[NSFileManager defaultManager] enumeratorAtPath:dir];
ingmarstein@1624
   739
	NSString *file;
ingmarstein@1579
   740
	while ((file = [enumerator nextObject])) {
eridius@2773
   741
		NSString *fullPath = [dir stringByAppendingPathComponent:file];
eridius@2773
   742
ingmarstein@1947
   743
		NSString *pathExtension = [file pathExtension];
eridius@2773
   744
		NSString *fileType;
eridius@2773
   745
		[[NSWorkspace sharedWorkspace] getFileType:&fileType creatorCode:NULL forFile:fullPath];
eridius@2773
   746
		if ([pluginHandlers objectForKey:pathExtension] || (fileType && [pluginHandlers objectForKey:fileType])) {
eridius@2773
   747
			[self dispatchPluginAtPath:fullPath];
ingmarstein@1624
   748
			[enumerator skipDescendents];
boredzo@1385
   749
		}
boredzo@1385
   750
	}
boredzo@1385
   751
}
boredzo@1385
   752
eridius@2773
   753
- (void) dispatchPluginAtPath:(NSString *)path {
eridius@2773
   754
	//get all the relevant handlers, by extension and by type.
eridius@2773
   755
	NSArray *handlersByExtension = [pluginHandlers objectForKey:[path pathExtension]];
eridius@2773
   756
	NSString *fileType;
eridius@2773
   757
	[[NSWorkspace sharedWorkspace] getFileType:&fileType creatorCode:NULL forFile:path];
eridius@2773
   758
	NSArray *handlersByType      = [pluginHandlers objectForKey:fileType];
eridius@2773
   759
eridius@2773
   760
	//strip duplicates by making a set from both arrays.
eridius@2773
   761
	NSMutableSet *allMatchingHandlersSet = handlersByExtension ? [NSMutableSet setWithArray:handlersByExtension] : [NSMutableSet set];
eridius@2773
   762
	if (handlersByType)
eridius@2773
   763
		[allMatchingHandlersSet unionSet:[NSSet setWithArray:handlersByType]];
eridius@2773
   764
rudy@3304
   765
	NSMutableArray *allMatchingHandlers = [[[allMatchingHandlersSet allObjects] mutableCopy] autorelease];
eridius@2773
   766
	[allMatchingHandlers sortUsingFunction:comparePluginHandlerRegistrationOrder context:self];
eridius@2773
   767
ingmarstein@1925
   768
	NSBundle *pluginBundle = [[NSBundle alloc] initWithPath:path];
ingmarstein@1628
   769
eridius@2773
   770
	NSEnumerator *handlersEnum = [allMatchingHandlers objectEnumerator];
eridius@2773
   771
	id <GrowlPluginHandler> handler;
eridius@2773
   772
	while ((handler = [handlersEnum nextObject])) {
eridius@2773
   773
		BOOL success = NO;
hg@4442
   774
		if (pluginBundle && [handler respondsToSelector:@selector(loadPluginWithBundle:)]) {
Rudy@4246
   775
			success = (NSUInteger)[handler performSelector:@selector(loadPluginWithBundle:) withObject:pluginBundle];
hg@4442
   776
			if (!success)
hg@4442
   777
				NSLog(@"%@: Handler %@ could not load plug-in with bundle %@", [self class], handler, pluginBundle);
hg@4442
   778
		} else if ([handler respondsToSelector:@selector(loadPluginAtPath:)]) {
Rudy@4246
   779
			success = (NSUInteger)[handler performSelector:@selector(loadPluginAtPath:) withObject:path];
hg@4442
   780
			if (!success)
hg@4442
   781
				NSLog(@"%@: Handler %@ could not load plug-in at path %@", [self class], handler, path);
hg@4442
   782
		} else if ([handler respondsToSelector:@selector(loadPluginAtURL:)]) {
Rudy@4246
   783
			success = (NSUInteger)[handler performSelector:@selector(loadPluginAtURL:) withObject:[NSURL fileURLWithPath:path]];
hg@4442
   784
			if (!success)
hg@4442
   785
				NSLog(@"%@: Handler %@ could not load plug-in at URL for path %@", [self class], handler, path);
hg@4442
   786
		} else
eridius@2773
   787
			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
eridius@2773
   788
	}
eridius@2773
   789
eridius@2773
   790
	[pluginBundle release];
eridius@2773
   791
}
eridius@2773
   792
eridius@2773
   793
#pragma mark -
eridius@2773
   794
#pragma mark Installing plug-ins
boredzo@1385
   795
boredzo@1385
   796
- (void) pluginInstalledSelector:(NSWindow *)sheet returnCode:(int)returnCode contextInfo:(void *)contextInfo {
ingmarstein@1865
   797
#pragma unused(sheet, contextInfo)
ingmarstein@1586
   798
	if (returnCode == NSAlertAlternateReturn) {
ofri@2683
   799
		NSBundle *prefPane = [GrowlPathUtilities growlPrefPaneBundle];
ingmarstein@1586
   800
ingmarstein@2641
   801
		if (prefPane && ![[NSWorkspace sharedWorkspace] openFile:[prefPane bundlePath]])
ingmarstein@1790
   802
			NSLog(@"Could not open Growl PrefPane");
boredzo@1385
   803
	}
boredzo@1385
   804
}
boredzo@1385
   805
boredzo@1385
   806
- (void) pluginExistsSelector:(NSWindow *)sheet returnCode:(int)returnCode contextInfo:(void *)contextInfo {
ingmarstein@1865
   807
#pragma unused(sheet)
boredzo@1385
   808
	NSString *filename = (NSString *)contextInfo;
ingmarstein@1579
   809
ingmarstein@1579
   810
	if (returnCode == NSAlertAlternateReturn) {
eridius@2773
   811
		//'Yes' to 'Do you want to overwrite [the installed plug-in with the version you double-clicked]?'
boredzo@1385
   812
		NSString *pluginFile = [filename lastPathComponent];
ingmarstein@1947
   813
		NSString *destination = [[NSHomeDirectory()
ingmarstein@1947
   814
			stringByAppendingPathComponent:@"Library/Application Support/Growl/Plugins"]
ingmarstein@1790
   815
			stringByAppendingPathComponent:pluginFile];
boredzo@1385
   816
		NSFileManager *fileManager = [NSFileManager defaultManager];
boredzo@1385
   817
boredzo@1385
   818
		// first remove old copy if present
boredzo@1385
   819
		[fileManager removeFileAtPath:destination handler:nil];
boredzo@1385
   820
boredzo@1385
   821
		// copy new version to destination
ingmarstein@1579
   822
		if ([fileManager copyPath:filename toPath:destination handler:nil]) {
eridius@2773
   823
			[self dispatchPluginAtPath:destination];
eridius@2773
   824
			[[NSApplication sharedApplication] activateIgnoringOtherApps:YES];
hg@4435
   825
			if([self hasNativeArchitecture:destination])
Rudy@4413
   826
				NSBeginInformationalAlertSheet( NSLocalizedString( @"Plugin installed", @"" ),
boredzo@2465
   827
											NSLocalizedString( @"No",  @"" ),
boredzo@1385
   828
											NSLocalizedString( @"Yes", @"" ),
boredzo@1385
   829
											nil, nil, self,
boredzo@1385
   830
											@selector(pluginInstalledSelector:returnCode:contextInfo:),
boredzo@1385
   831
											NULL, NULL,
boredzo@1385
   832
											NSLocalizedString( @"Plugin '%@' has been installed successfully. Do you want to configure it now?", @"" ),
boredzo@1385
   833
											[pluginFile stringByDeletingPathExtension] );
boredzo@1385
   834
		} else {
eridius@2773
   835
			[[NSApplication sharedApplication] activateIgnoringOtherApps:YES];
boredzo@1385
   836
			NSBeginCriticalAlertSheet( NSLocalizedString( @"Plugin not installed", @"" ),
boredzo@1385
   837
									   NSLocalizedString( @"OK", @"" ),
boredzo@1385
   838
									   nil, nil, nil, self, NULL, NULL, NULL,
boredzo@1385
   839
									   NSLocalizedString( @"There was an error while installing the plugin '%@'.", @"" ),
boredzo@1385
   840
									   [pluginFile stringByDeletingPathExtension] );
boredzo@1385
   841
		}
boredzo@1385
   842
	}
ingmarstein@1625
   843
boredzo@1385
   844
	[filename release];
boredzo@1385
   845
}
boredzo@1385
   846
eridius@2773
   847
- (void) installPluginFromPath:(NSString *)filename {
boredzo@1385
   848
	NSString *pluginFile = [filename lastPathComponent];
ingmarstein@1947
   849
	NSString *destination = [[NSHomeDirectory()
ingmarstein@1947
   850
		stringByAppendingPathComponent:@"Library/Application Support/Growl/Plugins"]
ingmarstein@1790
   851
		stringByAppendingPathComponent:pluginFile];
boredzo@1385
   852
	// retain a copy of the filename because it is passed as context to the sheetDidEnd selectors
boredzo@1385
   853
	NSString *filenameCopy = [[NSString alloc] initWithString:filename];
boredzo@1385
   854
Rudy@4413
   855
	//Check to see if we've got valid architectures in this plugin for our use, if not, bail.
hg@4435
   856
	if(![self hasNativeArchitecture:filenameCopy]) {
Rudy@4413
   857
		NSBeginAlertSheet( NSLocalizedString( @"Plugin missing native architecture", @"" ),
Rudy@4413
   858
						  NSLocalizedString( @"No", @"" ),
Rudy@4413
   859
						  NSLocalizedString( @"Yes", @"" ), nil, nil, self,
Rudy@4413
   860
						  NULL, @selector(pluginExistsSelector:returnCode:contextInfo:),
Rudy@4413
   861
						  filenameCopy,
hg@4425
   862
						  NSLocalizedString( @"Plugin '%@' will not work on this Mac running this version of Mac OS X. Install it anyway?", @"" ),
Rudy@4413
   863
						  [pluginFile stringByDeletingPathExtension] );		
Rudy@4413
   864
	}
Rudy@4413
   865
	else {
Rudy@4413
   866
		if ([[NSFileManager defaultManager] fileExistsAtPath:destination]) {
Rudy@4413
   867
			// plugin already exists at destination
Rudy@4413
   868
			[[NSApplication sharedApplication] activateIgnoringOtherApps:YES];
Rudy@4413
   869
			NSBeginAlertSheet( NSLocalizedString( @"Plugin already exists", @"" ),
Rudy@4413
   870
							  NSLocalizedString( @"No", @"" ),
Rudy@4413
   871
							  NSLocalizedString( @"Yes", @"" ), nil, nil, self,
Rudy@4413
   872
							  NULL, @selector(pluginExistsSelector:returnCode:contextInfo:),
Rudy@4413
   873
							  filenameCopy,
Rudy@4413
   874
							  NSLocalizedString( @"Plugin '%@' is already installed, do you want to overwrite it?", @"" ),
Rudy@4413
   875
							  [pluginFile stringByDeletingPathExtension] );
Rudy@4413
   876
		} else {
Rudy@4413
   877
			[self pluginExistsSelector:nil returnCode:NSAlertAlternateReturn contextInfo:filenameCopy];
Rudy@4413
   878
		}
Rudy@4413
   879
	}
Rudy@4413
   880
}
Rudy@4413
   881
hg@4435
   882
- (BOOL)hasNativeArchitecture:(NSString*)filename {	
Rudy@4413
   883
	BOOL result = NO;
Rudy@4413
   884
	NSInteger currentArchitecture = 0;
Rudy@4413
   885
#if defined(__ppc__) && __ppc__
Rudy@4413
   886
	currentArchitecture = NSBundleExecutableArchitecturePPC;
Rudy@4413
   887
#elif defined(__i386__) && __i386__
Rudy@4413
   888
	currentArchitecture = NSBundleExecutableArchitectureI386;
Rudy@4413
   889
#elif defined(__x86_64__) && __x86_64__
Rudy@4413
   890
	currentArchitecture = NSBundleExecutableArchitectureX86_64;
Rudy@4413
   891
#else
Rudy@4413
   892
	#error unsupported architecture
Rudy@4413
   893
#endif
Rudy@4413
   894
	NSBundle *pluginBundle = [NSBundle bundleWithPath:filename];
Rudy@4423
   895
	NSString *executablePath = [pluginBundle executablePath];
Rudy@4423
   896
	//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.
Rudy@4423
   897
	if(executablePath && [[NSFileManager defaultManager] fileExistsAtPath:executablePath]) {
Rudy@4423
   898
		NSArray *pluginArchitectures = [pluginBundle executableArchitectures];
Rudy@4423
   899
		if([pluginArchitectures containsObject:[NSNumber numberWithInteger:currentArchitecture]])
Rudy@4423
   900
			result = YES;
Rudy@4423
   901
	}
Rudy@4423
   902
	else {
Rudy@4413
   903
		result = YES;
Rudy@4423
   904
	}
Rudy@4423
   905
Rudy@4413
   906
	return result;
boredzo@1385
   907
}
boredzo@1385
   908
boredzo@1385
   909
@end
boredzo@2469
   910
boredzo@2469
   911
static Boolean caseInsensitiveStringComparator(const void *value1, const void *value2) {
boredzo@2469
   912
	Class NSStringClass = [NSString class];
boredzo@2469
   913
	return [(id)value1 isKindOfClass:NSStringClass] \
boredzo@2469
   914
	    && [(id)value2 isKindOfClass:NSStringClass]  \
boredzo@2469
   915
	    && ([(NSString *)value1 caseInsensitiveCompare:(NSString *)value2] == NSOrderedSame);
boredzo@2469
   916
}
eridius@2773
   917
eridius@2773
   918
static CFHashCode passthroughStringHash(const void *value) {
eridius@2773
   919
	return [[(NSString *)value lowercaseString] hash];
eridius@2773
   920
}
eridius@2773
   921
eridius@2773
   922
#pragma mark -
eridius@2773
   923
eridius@2773
   924
@implementation NSDictionary (GrowlPluginKeys)
eridius@2773
   925
eridius@2773
   926
- (NSString *) pluginName {
eridius@2773
   927
	return [self objectForKey:GrowlPluginInfoKeyName];
eridius@2773
   928
}
eridius@2773
   929
- (NSString *) pluginAuthor {
eridius@2773
   930
	return [self objectForKey:GrowlPluginInfoKeyAuthor];
eridius@2773
   931
}
eridius@2773
   932
- (NSString *) pluginDescription {
eridius@2773
   933
	return [self objectForKey:GrowlPluginInfoKeyDescription];
eridius@2773
   934
}
eridius@2773
   935
- (NSString *) pluginVersion {
eridius@2773
   936
	return [self objectForKey:GrowlPluginInfoKeyVersion];
eridius@2773
   937
}
eridius@2773
   938
- (NSBundle *) pluginBundle {
eridius@2773
   939
	return [self objectForKey:GrowlPluginInfoKeyBundle];
eridius@2773
   940
}
eridius@2773
   941
- (NSString *) pluginPath {
eridius@2773
   942
	return [self objectForKey:GrowlPluginInfoKeyPath];
eridius@2773
   943
}
eridius@2773
   944
- (NSSet *) pluginTypes {
eridius@2773
   945
	return [self objectForKey:GrowlPluginInfoKeyTypes];
eridius@2773
   946
}
eridius@2773
   947
- (NSString *) pluginHumanReadableName {
eridius@2773
   948
	return [self objectForKey:GrowlPluginInfoKeyHumanReadableName];
eridius@2773
   949
}
eridius@2773
   950
- (NSString *) pluginIdentifier {
eridius@2773
   951
	return [self objectForKey:GrowlPluginInfoKeyIdentifier];
eridius@2773
   952
}
eridius@2773
   953
- (GrowlPlugin *) pluginInstance {
eridius@2773
   954
	return [self objectForKey:GrowlPluginInfoKeyInstance];
eridius@2773
   955
}
eridius@2773
   956
eridius@2773
   957
@end
eridius@2773
   958
eridius@2773
   959
#define ASSERT_IN_FUNCTION(condition, desc, ...)                                                      \
eridius@2773
   960
	[[NSAssertionHandler currentHandler] handleFailureInFunction:[NSString stringWithCString:__func__] \
eridius@2773
   961
															file:[NSString stringWithCString:__FILE__]  \
eridius@2773
   962
													  lineNumber:__LINE__                                \
eridius@2773
   963
													 description:desc, __VA_ARGS__];
eridius@2773
   964
Rudy@4246
   965
NSInteger comparePluginHandlerRegistrationOrder(id a, id b, void *context) {
eridius@2773
   966
	GrowlPluginController *self = (GrowlPluginController *)context;
eridius@2773
   967
	NSArray *allPluginHandlers = [self allPluginHandlers];
eridius@2773
   968
Rudy@4246
   969
	NSUInteger aIndex = [allPluginHandlers indexOfObjectIdenticalTo:a];
Rudy@4246
   970
	NSUInteger bIndex = [allPluginHandlers indexOfObjectIdenticalTo:b];
eridius@2773
   971
eridius@2773
   972
	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);
eridius@2773
   973
	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);
eridius@2773
   974
ingmarstein@2941
   975
	if (aIndex < bIndex)
eridius@2773
   976
		return NSOrderedAscending;
ingmarstein@2941
   977
	else if (aIndex > bIndex)
eridius@2773
   978
		return NSOrderedDescending;
eridius@2773
   979
	else
eridius@2773
   980
		return NSOrderedSame;
eridius@2773
   981
}