// // MPT_PluginController.m // AUCoderDemo // // Created by Andrew Salamon on 9/25/06. // Copyright (c) 2006 Machine Perception Laboratory, // University of California San Diego. All rights reserved. #import "MPT_PluginController.h" #import "MPT_PluginProtocolSVM.h" #import "MPT_ResolveAlias.h" #import "MPT_AutoTileView.h" #import "MPT_AUView.h" #import "MPT_Preferences.h" #import "MPT_Demo.h" #import "MPT_FaceFinderStub.h" extern "C" { #include } static MPT_PluginController *controller; @interface MPT_PluginControllerBase(PrivateMethods) - (void)setPlugins:(NSArray *)newPlugins; - (void)checkPluginDependencies; @end @interface MPT_PluginController(PrivateMethods) - (void)initPluginViews:(id)plugin; - (void)initOnePlugin:(id)plugin; - (void)buildPluginMenuItems; @end @implementation MPT_PluginController + (MPT_PluginController *)defaultController { if( !controller ) controller = [[MPT_PluginController alloc] init]; return controller; } + (NSString *)pluginStateChangedNotification { return @"pluginStateChangedNotification"; } + (MPT_PluginControllerBase *)copyForProcessing { NSMutableArray *newPlugins = [NSMutableArray array]; NSEnumerator *plugEnum = [[[self defaultController] plugins] objectEnumerator]; id plugin; while( plugin = [plugEnum nextObject] ) { id cp = [plugin copy]; if( cp ) { [newPlugins addObject:cp]; [cp release]; } } MPT_PluginControllerBase *cp = [[MPT_PluginControllerBase alloc] init]; [cp setPlugins:newPlugins]; return cp; } - (id)init { if( self = [super init] ) { plugins = [[NSMutableArray alloc] init]; reverseDependencies = [[NSMutableDictionary alloc] init]; suppressChangeNotification = NO; licensing = [MPT_Demo sharedDemo]; if( ![NSBundle loadNibNamed:@"PluginErrors" owner:self] ) { NSLog( @"Unable to load PluginErrors nib in MPT_PluginController" ); } } return self; } - (void)dealloc { [plugins release]; [reverseDependencies release]; [tileView release]; [errorWindow release]; [super dealloc]; } - (MPT_AutoTileView *)tileView { return tileView; } - (void)setTileView:(MPT_AutoTileView *)value { if( tileView != value ) { [tileView release]; tileView = [value retain]; } } - (NSString *)localizedNameForPluginID:(NSString *)pluginID { id plugin = [self pluginForPluginID:pluginID]; return [plugin localizedName]; } - (void)loadPluginFromPath:(NSString *)path { path = [[[path stringByStandardizingPath] stringByResolvingSymlinksInPath] stringByResolvingAliases]; NSBundle *plugin = [NSBundle bundleWithPath:path]; // make sure we haven't already loaded a plugin with this one's ID NSString *pID = [plugin bundleIdentifier]; if( pID ) { id existingPlugin = [self pluginForPluginID:pID]; if( existingPlugin ) { NSLog( @"Error trying to load a bundle. Duplicate plugin ID at path: %@", path ); return; } } [plugin load]; Class controller = [plugin principalClass]; if( !controller ) { // If a plugin has no code in it (like many SVM plugins) then the principalClass method won't return a // class object even if it's defined in the info.plist, so try manually. NSString *className = [plugin objectForInfoDictionaryKey:@"NSPrincipalClass"]; if( className ) controller = NSClassFromString(className); } id pc = [[controller alloc] initWithBundle:plugin]; if( pc ) { [plugins addObject:pc]; [pc release]; } else { NSLog( @"Error trying to load a bundle. No principal class found. Path: %@", path ); } } - (void)loadPluginsInDirectory:(NSString *)path { NSFileManager *fileManager = [NSFileManager defaultManager]; NSArray *plugs = [fileManager directoryContentsAtPath:path]; NSEnumerator *plugEnum = [plugs objectEnumerator]; NSString *plugPath; while( plugPath = [plugEnum nextObject] ) { if( [[plugPath pathExtension] isEqualToString:@"bundle"] || [[plugPath pathExtension] isEqualToString:@"mptmod"] ) { [self loadPluginFromPath:[path stringByAppendingPathComponent:plugPath]]; } } } - (void)loadPlugins { NSArray *paths = NSSearchPathForDirectoriesInDomains( NSApplicationSupportDirectory, NSAllDomainsMask, YES ); NSEnumerator *pathEnum = [paths objectEnumerator]; NSString *path; while( path = [pathEnum nextObject] ) { path = [path stringByAppendingPathComponent:@"MPT"]; path = [path stringByAppendingPathComponent:@"PlugIns"]; [self loadPluginsInDirectory:path]; } [self loadPluginsInDirectory:[[NSBundle mainBundle] builtInPlugInsPath]]; // Also add our 'fake' facefinder plugin MPT_FaceFinderStub *stub = [[MPT_FaceFinderStub alloc] initWithBundle:[NSBundle mainBundle]]; [plugins addObject:stub]; [stub release]; } - (BOOL)addPluginFromPath:(NSString *)path { return NO; } - (BOOL)removePlugin:(NSString *)pluginID { return NO; } - (BOOL)setEnable:(BOOL)enable forPlugin:(NSString *)pluginID { return NO; } - (void)initPlugins { [self willChangeValueForKey:@"plugins"]; [self willChangeValueForKey:@"visiblePlugins"]; [self loadPlugins]; id plugin; NSArray *sortedPlugins = [[self plugins] sortedArrayUsingDescriptors:[MPT_Preferences pluginTableSortDescriptors]]; NSEnumerator *pluginEnum = [sortedPlugins objectEnumerator]; while( plugin = [pluginEnum nextObject] ) { [self initOnePlugin:plugin]; } [[NSNotificationCenter defaultCenter] postNotificationName:[[self class] pluginStateChangedNotification] object:self userInfo:nil]; [self buildPluginMenuItems]; [self checkPluginDependencies]; pluginEnum = [plugins objectEnumerator]; id pluginObj; SEL finished = @selector(pluginsFinishedLoading:); while( pluginObj = [pluginEnum nextObject] ) { if( [pluginObj respondsToSelector:finished] ) { [pluginObj performSelector:finished withObject:sortedPlugins]; [self initPluginViews:pluginObj]; } } [self didChangeValueForKey:@"plugins"]; [self didChangeValueForKey:@"visiblePlugins"]; [tileView tileViews]; } - (void)initPluginViews:(id)plugin { NSEnumerator *labelEnum = [[plugin labels] objectEnumerator]; NSString *label; while( label = [labelEnum nextObject] ) { MPT_AUView *view = [plugin viewForLabel:label]; if( view ) { [tileView addView:view]; } } } - (void)initOnePlugin:(id)plugin { [self initPluginViews:plugin]; NSDictionary *prefs = [[NSUserDefaults standardUserDefaults] objectForKey:[plugin pluginID]]; if( prefs ) { NSNumber *enabledNum = [prefs objectForKey:@"enabled"]; if( enabledNum ) [plugin setEnabled:[enabledNum boolValue]]; else [plugin setEnabled:YES]; NSNumber *displayNum = [prefs objectForKey:@"display"]; if( displayNum ) [plugin setDisplay:[displayNum boolValue]]; } else { // If there is no pref for this plugin, assume this is the first time we've loaded it, so default to enabled. [plugin setEnabled:YES]; // [plugin setDisplay:YES]; } [plugin setDelegate:self]; // Make sure the directory is set for these SVM Weights even if the user prevented them from pre-loading. if( [[plugin step] isEqualTo:MPT_PluginStep_SVM] ) { setDirectoryForWeights( [[plugin internalName] UTF8String], [[((id)plugin) weightsDirectory] UTF8String] ); } } - (NSArray *)checkOnePluginDependency:(id)plugin { NSMutableArray *missingDeps = [NSMutableArray array]; NSEnumerator *depEnum = [[plugin dependencies] objectEnumerator]; NSString *oneDepID; while( oneDepID = [depEnum nextObject] ) { id dep = [self pluginForPluginID:oneDepID]; if( !dep ) { [missingDeps addObject:oneDepID]; } else if( ![dep enabled] ) { [dep setEnabled:YES]; if( ![dep enabled] ) { [missingDeps addObject:oneDepID]; } } } return missingDeps; } - (NSString *)errorForMissingDependencies:(NSArray *)missingDeps forPlugin:(id)pluginObj { NSMutableString *errorMsg = [NSMutableString string]; if( [missingDeps count] > 0 ) { [errorMsg appendFormat:@"The \"%@\" plugin depends on missing plugins with the following ID's:\n", [pluginObj localizedName]]; NSEnumerator *depEnum = [missingDeps objectEnumerator]; NSString *dep; while( dep = [depEnum nextObject] ) { [errorMsg appendFormat:@"\t%@\n", dep]; } [errorMsg appendString:@"\n"]; } return errorMsg; } - (void)checkPluginDependencies { NSMutableString *errorMsg = [NSMutableString string]; NSEnumerator *pluginEnum = [[self plugins] objectEnumerator]; id pluginObj; while( pluginObj = [pluginEnum nextObject] ) { if( [pluginObj enabled] ) { NSArray *missingDeps = [self checkOnePluginDependency:pluginObj]; if( [missingDeps count] > 0 ) { [errorMsg appendString:[self errorForMissingDependencies:missingDeps forPlugin:pluginObj]]; } } // set up the reverse dependencies for this plugin NSEnumerator *depEnum = [[pluginObj dependencies] objectEnumerator]; NSString *oneDepID; while( oneDepID = [depEnum nextObject] ) { NSMutableSet *revs = [reverseDependencies objectForKey:oneDepID]; if( !revs ) { revs = [NSMutableSet set]; [reverseDependencies setObject:revs forKey:oneDepID]; } [revs addObject:pluginObj]; id otherPlugin = [self pluginForPluginID:oneDepID]; [otherPlugin addDependent:pluginObj]; } } if( [errorMsg length] > 0 ) { [errorView setStringValue:errorMsg]; [errorWindow makeKeyAndOrderFront:self]; } } /** Plugin delegate method. * Plugins will call back to this method whenever their enabled state changes, so we can take * appropriate actions. In this case hiding or showing the plugin's views. */ - (void)didChangeEnabledStateForPlugin:(id)plugin { BOOL isEnabled = [plugin enabled]; // Check for and handle any dependencies. In either direction. if( isEnabled ) { NSArray *missingDeps = [self checkOnePluginDependency:plugin]; if( [missingDeps count] > 0 ) { NSString *errorMsg = [self errorForMissingDependencies:missingDeps forPlugin:plugin]; if( [errorMsg length] > 0 ) { [errorView setStringValue:errorMsg]; [errorWindow makeKeyAndOrderFront:self]; } } } else { NSSet *deps = [reverseDependencies objectForKey:[plugin pluginID]]; NSEnumerator *depsEnum = [deps objectEnumerator]; id depPlugin; while( depPlugin = [depsEnum nextObject] ) { if( [depPlugin enabled] ) { // Automatically disable it, or just provide a warning? NSLog( @"Dependent plugin still enabled: %@", [depPlugin localizedName] ); } } } [tileView tileViews]; if( !suppressChangeNotification ) [[NSNotificationCenter defaultCenter] postNotificationName:[[self class] pluginStateChangedNotification] object:self userInfo:[NSDictionary dictionaryWithObject:plugin forKey:@"plugin"]]; NSMutableDictionary *prefs = [[NSUserDefaults standardUserDefaults] objectForKey:[plugin pluginID]]; if( prefs ) prefs = [prefs mutableCopy]; else prefs = [[NSMutableDictionary alloc] init]; [prefs setObject:[NSNumber numberWithBool:isEnabled] forKey:@"enabled"]; [prefs setObject:[NSNumber numberWithBool:[plugin display]] forKey:@"display"]; [[NSUserDefaults standardUserDefaults] setObject:prefs forKey:[plugin pluginID]]; [prefs release]; int index = [pluginMenu indexOfItemWithRepresentedObject:plugin]; if( -1 != index ) { NSMenuItem *pluginMenuItem = [pluginMenu itemAtIndex:index]; [pluginMenuItem setState:([plugin display]?NSOnState:NSOffState)]; } } - (void)resetViews { NSEnumerator *pluginEnum = [[self plugins] objectEnumerator]; id plugin; while( plugin = [pluginEnum nextObject] ) { NSEnumerator *labelEnum = [[plugin labels] objectEnumerator]; NSString *label; while( label = [labelEnum nextObject] ) { MPT_AUView *view = [plugin viewForLabel:label]; [view resetDynamicRange]; } } } - (void)setPluginMenu:(NSMenu *)_menu { if( pluginMenu != _menu ) { [pluginMenu release]; pluginMenu = [_menu retain]; // set the target of the two first menu items to self NSMenuItem *item = [pluginMenu itemWithTag:1]; [item setTarget:self]; [item setAction:@selector(enableAllPlugins:)]; item = [pluginMenu itemWithTag:2]; [item setTarget:self]; [item setAction:@selector(disableAllPlugins:)]; } } - (void)clearPluginMenuItems { int minIndex = [pluginMenu indexOfItemWithTag:999]; ++minIndex; while( [pluginMenu numberOfItems] > minIndex ) { [pluginMenu removeItemAtIndex:([pluginMenu numberOfItems] - 1)]; } } - (void)buildPluginMenuItems { [self clearPluginMenuItems]; NSEnumerator *plugEnum = [[self sortedPlugins] objectEnumerator]; id plugin; while( plugin = [plugEnum nextObject] ) { if( ![[plugin step] isEqualToString:MPT_PluginStep_NoStep] && ![[plugin step] isEqualToString:MPT_PluginStep_Features] ) { NSMenuItem *pluginMenuItem = [[NSMenuItem alloc] initWithTitle:[plugin localizedName] action:@selector(toggleOnePlugin:) keyEquivalent:@""]; [pluginMenuItem setTarget:self]; [pluginMenuItem setRepresentedObject:plugin]; [pluginMenuItem setState:([plugin display]?NSOnState:NSOffState)]; [pluginMenu addItem:pluginMenuItem]; [pluginMenuItem release]; } } } - (void)setAllPluginsEnabled:(BOOL)enabled { NSEnumerator *plugEnum = [plugins objectEnumerator]; id plugin; suppressChangeNotification = YES; while( plugin = [plugEnum nextObject] ) { [plugin setDisplay:enabled]; } suppressChangeNotification = NO; [[NSNotificationCenter defaultCenter] postNotificationName:[[self class] pluginStateChangedNotification] object:self userInfo:nil]; } - (IBAction)enableAllPlugins:(id)sender { [self setAllPluginsEnabled:YES]; } - (IBAction)disableAllPlugins:(id)sender { [self setAllPluginsEnabled:NO]; } - (void)enablePluginsWithIDs:(NSArray *)ids { NSEnumerator *plugEnum = [ids objectEnumerator]; NSString *pluginID; suppressChangeNotification = YES; while( pluginID = [plugEnum nextObject] ) { id plugin = [self pluginForPluginID:pluginID]; [plugin setEnabled:YES]; } suppressChangeNotification = NO; [[NSNotificationCenter defaultCenter] postNotificationName:[[self class] pluginStateChangedNotification] object:self userInfo:nil]; } - (IBAction)toggleOnePlugin:(id)sender { id plugin = [sender representedObject]; [plugin setDisplay:![plugin display]]; // add logic here to enable/disable the plugin based on dependencies. // if nothing is dependent on it, then set enabled to the same as display // otherwise enabled needs to remain set to true } - (NSDictionary *)configForPrefs { NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults]; NSMutableDictionary *config = [NSMutableDictionary dictionary]; NSNumber *numPref; // numPref = [prefs objectForKey:@"doFeatureAveraging"]; // if( numPref ) // [config setObject:numPref forKey:@"doFeatureAveraging"]; numPref = [prefs objectForKey:@"windowMinScale"]; if( numPref ) [config setObject:numPref forKey:@"windowMinScale"]; numPref = [prefs objectForKey:@"windowMaxScale"]; if( numPref ) [config setObject:numPref forKey:@"windowMaxScale"]; numPref = [prefs objectForKey:@"alignmentMethod"]; if( numPref ) [config setObject:numPref forKey:@"alignmentMethod"]; // NSNumber *yesNum = [NSNumber numberWithBool:YES]; NSMutableArray *pluginConfig = [NSMutableArray array]; NSEnumerator *pluginEnum = [[self plugins] objectEnumerator]; id plugin; while( plugin = [pluginEnum nextObject] ) { if( [plugin enabled] && (MPT_PluginStep_NoStep != [plugin step]) && (MPT_PluginStep_Results != [plugin step]) ) { [pluginConfig addObject:[plugin pluginID]]; // [config setObject:yesNum forKey:[plugin pluginID]]; } } [config setObject:pluginConfig forKey:@"CERT_DO_Plugins"]; return config; } - (NSArray *)setConfig:(NSDictionary *)config { // enable/disable plugins based on config NSMutableArray *errors = [NSMutableArray array]; NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults]; // Disable all plugins, except the Distributed plugin NSEnumerator *plugEnum = [plugins objectEnumerator]; id plugin; suppressChangeNotification = YES; while( plugin = [plugEnum nextObject] ) { if( ![[plugin pluginID] isEqualToString:@"edu.ucsd.mplab.plugins.Distributed"] ) [plugin setEnabled:NO]; } suppressChangeNotification = NO; [[NSNotificationCenter defaultCenter] postNotificationName:[[self class] pluginStateChangedNotification] object:self userInfo:nil]; NSEnumerator *keyEnum = [[config allKeys] objectEnumerator]; id key; while( key = [keyEnum nextObject] ) { if( [key isEqualToString:@"CERT_DO_Plugins"] ) { NSArray *pluginArray = [config objectForKey:key]; if( ![pluginArray isKindOfClass:[NSArray class]] ) { [errors addObject:[NSError errorWithDomain:@"CERT_DO_Error" code:1 userInfo:nil]]; } else { NSEnumerator *pluginEnum = [pluginArray objectEnumerator]; NSString *pluginID; while( pluginID = [pluginEnum nextObject] ) { id plugin = [self pluginForPluginID:pluginID]; if( !plugin ) { [errors addObject:[NSError errorWithDomain:@"CERT_DO_Error" code:2 userInfo:[NSDictionary dictionaryWithObject:[NSString stringWithFormat:@"Missing plugin: %@", key] forKey:NSLocalizedDescriptionKey]]]; } } [self enablePluginsWithIDs:pluginArray]; } } else if( ![key isEqualToString:@"CERT DO Server ID"] ) { [prefs setObject:[config objectForKey:key] forKey:key]; } } return ([errors count] > 0)?errors:nil; } - (BOOL)isEvaluation { return [licensing isEvaluation]; } - (BOOL)isLicensed { return [licensing isLicensed]; } - (BOOL)isInternal { return [licensing isInternal]; } - (void)applicationWillTerminate:(NSNotification *)notification { NSEnumerator *plugEnum = [plugins objectEnumerator]; id plugin; suppressChangeNotification = YES; while( plugin = [plugEnum nextObject] ) { if( [plugin respondsToSelector:@selector(applicationWillTerminate:)] ) [plugin applicationWillTerminate:notification]; } suppressChangeNotification = NO; } - (void)reportPluginState { NSMutableString *res = [NSMutableString string]; NSEnumerator *pluginEnum = [[self plugins] objectEnumerator]; id plugin; [res appendString:@"Enabled\tDisplay\tPlugin ID\n"]; while( plugin = [pluginEnum nextObject] ) { [res appendFormat:@"%@\t%@\t%@\n", ([plugin enabled]?@"YES":@"NO"), ([plugin display]?@"YES":@"NO"), [plugin pluginID] ]; } NSLog( @"%@", res ); } @end