macdevcenter.com
oreilly.comSafari Books Online.Conferences.

advertisement

AddThis Social Bookmark Button

Plug It In, Plug It In
Pages: 1, 2, 3

Build Phases

In the Build Phases portion of the Targets tab we can specify what header and source files to include in the build, what frameworks and libraries to link to, and more. We've already met the Headers and Sources build phases previously when we moved the IAGrayscaleFilter class from ImageApp to the plug-in bundle. What we're interested in doing now is adding a Copy Files build phase to ImageApp.



By default there is no Copy Files build phase, so we have to add one. This is easily done by selecting from the Project menu New Build Phase > New Copy Files Build Phase. This will add the Copy Files build phase to your list of build phases. To add files to the copy phase, just drag them from the Groups & Files list onto the desired build phase. In our case we will drag Grayscale Filter.plug-in (or Grayscale Filter.bundle) into the Copy Files phase. If you haven't build the Grayscale Filter bundle yet, the filename will be red, indicating that the bundle has a meta-existence in the project, but not a physical existence in the filesystem.

After adding the Grayscale Filter to the Copy Files phase, you have to specify where we want the files copied to. Plug-ins go in the Plug-ins directory, which can be specified by selecting Plug-ins from the pop-up menu in the Copy Files view.

Screen shot.

We're almost to the point that we can get everything to build correctly. If you attempt to build now you will get compile errors with respect to the absence of IAGrayscaleFilter in ImageApp, since it is no longer included in the build process. The only instance of IAGrayscaleFilter is found in the class MyDocument. All we need to do is remove the method makeGrayscale from the interface and the implementation and remove any import statements importing IAGrayscaleFilter.h.

With all of these changes in place you should now be able to built ImageApp, have it automatically build the filter bundle to satisfy the target dependency, and copy Grayscale Filter.plug-in into the ImageApp bundle. You can check that everything went off without a hitch by showing the Info for ImageApp in the finder. If the plug-in copied correctly, you will see a Plug-ins section in the Info window containing Grayscale Filter.plug-in.

Screen shot.

The Plug-in Manager

What we have just described above is how to package code up in a bundle. In the remainder of this column we will discuss the creation of a class that will scan for plug-ins when ImageApp launches and add any discovered plug-ins to the filter menu in ImageApp. The class we create will be called IAPlug-inManager, and it will exist as an instance within MainMenu.nib.

In Interface Builder

Open MainMenu.nib now. From the Classes tab in the nib window, select NSObject and press return to create a subclass. Name the subclass IAPlug-inManager. To this class we want to add one outlet named filterMenu. This is the outlet we will use to access the filter menu in the menu bar. When you create this outlet, it will be helpful if we set the class of the outlet to NSMenu. Sometimes connecting an outlet to a menu can be tricky, and this will help to make sure we get it right. Next create the project files for IAPlug-inManager, and initiate it.

To connect the outlet to the filter menu, you have to make sure that the filter menu is open. You cannot make the connection to the label on the main menu bar that says filter. That is a menu item of the main menu. So click on the filter menu to open it. Now, when you drag your connection wire from IAPlug-inManager to the filter menu, you have to drop it on the filter menu label, not on any of the menu items.

Screen shot.

The last thing we need to do here is to remove the Make Grayscale item from the filter menu. Since all the items for this menu will be added programmatically by our plug-in manager, we want this to be empty. With that, save your work, and return to Project Builder.

Back in Project Builder

IAPlug-inManager is comprised of four methods:

  • awakeFromNib
  • discoverPlug-ins
  • setupFilterMenu
  • executeFilter:

The first one, awakeFromNib, is easy enough:

- (void)awakeFromNib { 
    [self discoverPlug-ins];  
    [self setupFilterMenu]; 
} 

This method simply invokes the method discoverPlug-ins and setupFilterMenu.

The purpose of discoverPlug-ins is to search for any installed plug-ins. In Mac OS X, plug-ins may be installed in a number of well-known locations:

  • ImageApp.app/Contents/Plug-ins: This is where the developer of the application would put plug-ins that ship with the product.
  • ~/Library/Application Support/ImageApp/Plug-ins: Where a user would keep personal plug-ins.
  • /Library/Application Support/ImageApp/Plug-ins: Where plug-ins would be kept so they are accessible to all users on a system.

In today's column we set things up so that Grayscale Filter is copied to the first of these locations, within ImageApp's bundle. However, to require a user put all of their plug-ins in this single location causes a serious problem. When it comes time to upgrade the app, any user-installed plug-ins will be lost when the new version of the app is copied over the old. Thus, users should keep their plug-ins in one of the Library folders so they are available after upgrading the application.

The goal of discoverPlug-ins will be to enumerate the contents of each of the potential plug-in directories, and whenever we come across a bundle with the plug-in extension we will add an instance of that bundle's principal class to a dictionary with the filter menu item title as the key. Before getting to dicovers, go into IAPlug-inManager.h and add the NSMutableDictionary instance variable plug-ins to the class interface. Additionally import the headers for MyDocument and IAFilter.

Now, Here is what discoverPlug-ins looks like:

- (void)discoverPlug-ins
{		      
    Class filterClass;
			 NSString *appSupport = @"Library/Application Support/ImageApp/Plug-ins/";
			 NSString *appPath = [[NSBundle mainBundle] builtInPlug-insPath];
		  NSString *userPath = [NSHomeDirectory 
		          stringByAppendingPathComponent:appSupport];
		  NSString *sysPath = [@"/" 
			          stringByAppendingPathComponent:appSupport];
		  NSArray *paths = [NSArray arrayWithObjects:appPath,
			          userPath, sysPath, nil];
		  NSEnumerator *pathEnum = [paths objectEnumerator];
		  NSString *path;
			
		  plug-ins = [[NSMutableDictionary alloc] init];
			
		  while ( path = [pathEnum nextObject] ) {
		    NSEnumerator *e = [[[NSFileManager defaultManager]
			                directoryContentsAtPath:path] objectEnumerator];
		    NSString *name;

            while ( name = [e nextObject] )
		      if ( [[name pathExtension] isEqualToString:@"plugin"] ) {
		        NSBundle *plugin = [NSBundle bundleWithPath: name];
			        
		        if ( filterClass = [plug-in principalClass] )
		          if ( [filterClass 
                  instancesRespondToSelector:@selector(filterImage:)] ) {
	             NSString *menuTitle = [[plug-in infoDictionary]
			                  objectForKey:@"IAFilterMenuItemTitle"];
		            [plug-ins setObject:[filterClass filter] forKey:menuTitle];
		          }
        }
   
    }
} 

And there you have it. Letís take our time to pick this apart. It's actually simpler than all the enumerators. The variable filterClass holds the class object returned that we obtain from the bundle.

In the next line we define the path to the plug-in directory found in the Library paths. In the next three lines we define the three paths where we will look for plug-ins. The first path is the Plug-ins directory found in the ImageApp bundle. This path is obtained using the NSBundle method builtInPlug-insPath. This method is invoked in the object returned by the mainBundle class method of NSBundle. The method mainBundle returns the NSBundle object representing the application's main bundle, which in our case is the ImageApp application bundle.

The next path is the path to the Plug-ins directory in the user's Library. This path is created by appending the common appSupport path to the user's home directory, which is obtained using the Foundation function NSHomeDirectory.

The final path is the location of the system-wide Plug-ins directory found at /Library/Application Support/ImageApp/Plug-ins/. This path is created by appending the common appSupport path to the root directory, /.

These three paths are collected together into an array, from which we obtain an enumerator assigned to the variable pathEnum. Before getting to the nested control structures, we create an NSString variable path that will be used in the enumeration of paths, and we initialize the instance variable plug-ins to an empty mutable dictionary.

Now, the heart of it. What we're doing in this unattractive nest of control structures is first enumerating the three Plug-in paths, we've defined, and for each of those three paths we obtain the contents of that directory. We then enumerate through those contents and check the extension for each object in that directory. If the path extension is plug-in, then we create an instance of NSBundle from that path.

Next we try to obtain the principal class of the bundle and assign it to filterClass. This is done by sending a principalClass method to the bundle instance. If there is no principal class defined, then the principalClass method will return Nil (The uppercase "N" is not a typo. Nil is the equivalent of nil for class objects) and we continue with the enumeration.

If there is a principal class, then we check to see if an instance of this class responds to the filterImage: message, which is essential if this is all to function correctly. To check if an instance of a class implements a specific method, we can use the NSObject method instancesRespondToSelector:. This method takes a selector as an argument, and in the code above, we pass @selector(filterImage:) since that is the method in question.

Finally, if there is a principal class in the bundle and it responds to filterImage:, we add an instance of the principal class to the dictionary plug-ins. The key for the filter instance in the dictionary is the string for the menu item title. To get this string we get the bundle's infoDictionary, which is an NSDictionary that contains all of the information contained in the bundles Info.plist, and we use the same keys in Info.plist to access objects in the info dictionary. Therefore, we use the key IAFilterMenuItemTitle to get the NSString object containing the title we set for the plug-in.

And that's discoverPlug-ins.

Next we have setupFilterMenu. This method is pretty straightforward. The idea here is to enumerate the contents of the dictionary plug-ins and add menu items to the filter menu corresponding to each plug-in. Lets take a look to see how it looks:

- (void)setupFilterMenu { NSEnumerator *e = [plug-ins keyEnumerator]; NSString *name; while ( name = [e nextObject] ) { NSMenuItem *item = [[[NSMenuItem alloc] init] autorelease]; [item setTitle:name]; [item setTarget:self]; [item setAction:@selector(executeFilter:)]; [filterMenu insertItem:item atIndex:0]; } }

In this menu we went through each key in the dictionary, created a new NSMenuItem with that title, set the target of the menu item to self, the action to executeFilter:, and inserted the item into the filter menu.

The last method to put in IAPlug-inManager is executeFilter:. This is the action of every filter menu item. The purpose of executeFilter: is to get the correct filter instance from the plug-ins dictionary, using the menu item title as the key, and then to invoke in the MyDocument instance associated with the frontmost window the method filterImageUsingFilter: (which hasn't been defined yet). The method looks like the following:

- (void)executeFilter:(id)sender
{
    NSString *name = [sender title];
    IAFilter *filter = [plug-ins objectForKey:name];
    MyDocument *doc = [[NSDocumentController sharedDocumentController] currentDocument];
    [doc filterImageUsingFilter:filter];
}

In the method above we got the name of the filter from the title of the sender object, which will always be one of the menu items in the filter menu added by the method setupFilterMenu. Using that name we obtain the filter object corresponding to the menu item from dictionary plug-ins. Finally, we get the current document, and invoke the as-of-yet-undefined method filterImageUsingFilter: with the filter object passed as the parameter. The last piece of this puzzle is to add to MyDocument the following implementation of the method filterImageUsingFilter:

- (void)filterImageUsingFilter:(IAFilter *)filter
{
    activeImage = [[filter filterImage:activeImage] retain];
    [windowController setImageToDraw:activeImage];
}

Here we follow the pattern of our old method makeGrayscale: of passing the image assigned to activeImage to the filter through filterImage: and setting the activeImage to the returned image. We then tell the window controller for the document to update the displayed image to the new activeImage. And there you have it. Compile away, and play around with this to your heart's content.

One Last Thing

In the column before last, where we first learned about image filters, we created a Grayscale Filter. We did this by simply averaging together the magnitudes of the red, green, and blue components of the color into a single gray value. However, as many of you know and pointed out to me, the human eye is not equally sensitive to all colors of light. The correct thing to do is to do a weighted average of the three color components. So, where we had:

grayValue = ( red + green + blue ) / 3

equivalent (very nearly) to:

grayValue = 0.333*red + 0.333*green + 0.333*blue

we should have had:

grayValue = 0.222*red + 0.707*green + 0.071*blue.

So, while the details of the colorspace conversion were incorrect, the rest of our technique was correct. One link provided in the comments of that column containing lots of useful information about color that I found interesting is the Color space FAQ.

The End

What we have seen in today's column is how to create an plug-in architecture for an application. You can download the application here. For another perspective on this subject, have a look at this article by Rainer Brockerhoff. Where we created a public class for plug-in developers to subclass, he provides a formal protocol for developers to conform to.

Coming up in the next two columns we will take another detour from our Image App series to learn about networking in Cocoa: in the first we will look at the Foundation classes that let us use rendezvous, and in the second we will learn how to make two apps talk to each other using sockets. See you then!

Michael Beam is a software engineer in the energy industry specializing in seismic application development on Linux with C++ and Qt. He lives in Houston, Texas with his wife and son.


Read more Programming With Cocoa columns.

Return to the Mac DevCenter.