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