MacDevCenter    
 Published on MacDevCenter (http://www.macdevcenter.com/)
 See this if you're having trouble printing code examples


Inside Contextual Menu Items, Part 2

by Steven Disbrow
06/04/2004

In my last article, I gave you a very high-level look at Contextual Menu Items (CMIs). This time out, I'm going to show you how to actually create your own CMI using Apple's Xcode IDE. (Note that this process assumes a basic familiarity with the C programming language.)

Start Here

When I was first dabbling in Macintosh programming, back in the early 1990s, my preferred method for learning how to create software was to crack open one of the many Inside Macintosh volumes and start reading. Soon the floor around my desk would be littered with books, each one overflowing with Post-It® notes that marked points of interest for my current project. Of course, those Inside Macintosh volumes are still available, but now instead of paying $50 a pop for wads of dead tree flesh, you can get them for free, in PDF form, from Apple's web site. And instead of bits of sticky paper, I've got scads of Safari bookmarks, each pointing to a different page on Apple's developer web site.

Of particular interest (at least as far as this article is concerned) is a page with a link to a sample source code archive that shows how to create a simple CMI. Since this is Apple's official example of how to create CMIs, we'll use this sample source code as our starting point. (Apple does provide information on the more generic subject of "plugins," which is also worth reading.)

So download the archive, unpack it, open the SampleCMPlugin folder, and then load the SampleCMPlugin.pbproj file into Xcode. (Note that this is a Project Builder project file, but Xcode will happily convert it for you. Just select the default conversion options as Xcode opens the file, and everything should be fine. If you are still using Project Builder, no problem! Most everything I'll discuss here has an obvious Project Builder equivalent.) Once you have the project open in Xcode, you should see a project window like the one shown below.

In this opening screen, we can see the frameworks that CMIs are based on, along with the implementation file: SampleCMPlugIn.c. This brings up a couple of important points: CMIs are based on Carbon, not Cocoa, and they are almost always written in good old C. (This isn't a problem per se, but if you were looking at CMIs as a way to learn Cocoa or Objective-C, you should look for another type of Mac program to write.)

What Does it Do?

Before we look at the code, let's see what the SampleCMPlugIn.plugin actually does. To do that, you'll need to install it. Assuming that the SampleCMPlugin folder ended up on your desktop, look inside there for a folder called build. Inside of that folder, you'll find the SampleCMPlugIn.plugin file. Copy this file into your ~/Library/Contextual Menu Items folder and then log out and back in. (For complete information on how to install CMIs, see my last article.) Once you've logged out and back in, go to the Finder, and right-click (or control-click, if you are still using a one-button mouse) on an icon on the desktop. When you do, you should see a menu something like the one shown below.

In the above picture, I've highlighted the five menu items that the SampleCMPlugIn adds to this contextual menu. The first is a divider line. You'll notice that this line comes right after the divider line that separates it from the Finder's Copy command. This is because in Mac OS X v10.3, Apple seems to have changed the way the Finder places its divider lines, but the SampleCMPlugIn code has not been changed to reflect this. (If you've got one or more third-party CMIs that exhibit this problem, this is your first clue that almost every third-party CMI in existence started life at the intersection of Apple's SampleCMPlugin and some text editor's global "Find and Replace" command.) We'll see how to correct this when we look at the source code in more detail.

The next two lines are informational. The first tells you that you are looking at output from inside of the SampleCMPlugin code, and the second tells you the basic type of information that was passed to the CMI from the operating system. In this case, it was a list. This is the type that you get whenever you invoke a contextual menu on one or more Finder icons. (The other basic type is text, but, strangely, almost no applications pass this type of information. Instead, you'll usually get a type of null when you right-click on a chunk of selected text.)

The last two lines are examples of submenus in a CMI. The first one reports the number of selected items in the menu item name, while the submenu itself is a list of the selected items. The last one is just a generic submenu, filled with three sample items.

The last thing to notice is that each individual menu item (except the divider) also has its internal menu item number shown as part of its name. When an item is selected by the user, this is the number that the CMI will receive, along with the message that the user made a selection.

A Look at the Source Code

If you aren't there already, load the SampleCMPlugin.pbproj file into Xcode again and double-click on the SampleCMPlugIn.c file. Once the file is open, scroll down to the definition for the value kSampleCMPlugInFactoryID. (You'll find it on line 72.) This factory id is a UUID. UUID stands for "Universal Unique IDentifier." Basically, a UUID is a unique 128-bit number that's created using a computer's MAC address, the time of day, and a random seed value. (For a more complete definition, check out this link.) This number is used to differentiate the CMI from any others that might be installed on your computer, so it's important that each CMI have its own UUID.

Apple provides a bit of sample code for generating a UUID, and there's even a uuidgen command-line tool built into Mac OS X, but my preferred solution is to use John C. Daub's UUID Generator. The reason for this is that, as you can see in the source code, the UUID must be in a very specific format. Neither Apple's sample code nor the uuidgen tool produce a value in this format. The UUID Generator tool however, can produce a UUID in several formats, including the one required for a CMI. It can even place the UUID directly on the clipboard for you, so that all you have to do it switch to Xcode and paste it in. (Oh, and it's free, too!)

Examining The Context

The "C" in CMI stands for "Contextual." When it's invoked, your CMI needs some way to evaluate the context in which it was called. In the SampleCMPlugIn, this is handled by a function named SampleCMPlugInExamineContext. When the user right-clicks her mouse, the system calls the equivalent of this function in every loaded CMI. The function then looks at the parameters that the system has passed it and determines whether or not its services make sense within the current context. If so, the function builds a list of menu items and passes it back to the system so that those items can be included in the final contextual menu. That said, let's look at those parameters:

Now that we know what the parameters are, let's see how the function actually works. Assuming that we're all looking at the same version of SampleCMPlugIn.c (and we should be; this sample code hasn't been updated in quite a while), refer to the following line numbers to see the highlights of the SampleCMPlugInExamineContext function:

Now, you are probably looking at the code in this function and thinking, "It's just a bunch of if statements!" Right you are! The only "magical" thing happening here is that the logic behind those if statements is driven by the contents of the inContext parameter.

Handling Selections

Once the menu is built and displayed, the user will be able to interact with it and pick an item from it. If one of our menu items is selected, the system will call the function SampleCMPlugInHandleSelection and pass it three parameters:

In the case of SampleCMPlugIn, nothing much happens when a menu item is selected. (It simply writes out some debugging information, which you can view in the Console application.) However, when writing your own CMIs, this is the place where you would carry out your custom actions based on the ID that you get in the inCommandID parameter.

The Glue That Holds it Together

At this point, you might be wondering what's so special about the SampleCMPlugInExamineContext and SampleCMPlugInHandleSelection functions. Well, as has been pointed out in another article here on macdevcenter.com, Mac OS X implements Microsoft's COM architecture as part of the Core Foundation framework. The main reason for this is to support, you guessed it, plugins, which is exactly what CMIs are.

These functions are simply implementations of two functions (generically known as ExamineContext and HandleSelection) required by this particular type of COM interface. A COM discussion is a bit beyond what we're talking about here, but if you would like a better understanding of how a CMI uses COM to interface with Mac OS X, start with a look at the SampleCMPlugInFactory function on line 279. (We'll come back to the SampleCMPlugInFactory function in just a bit.)

A Custom CMI

Now that we have an overview of what CMIs are and how one actually works, let's take that information and create a custom CMI that allows the user to gracefully quit any open application. Like just about every other Mac OS X CMI, we'll use the SampleCMPlugIn as our starting point. Customizing the SampleCMPlugIn project is a relatively simple, but lengthy, process, so let's look at each step individually. (Note that many of these steps could be combined with clever use of Xcode's Find and Replace functionality, but the point here is to understand everything that must be done, so we'll look at every step required.)

  1. Duplicate the SampleCMPlugin folder and rename it QuitCMI. Open the QuitCMI folder.
  2. Rename the SampleCMPlugin.pbproj file to QuitCMI.pbproj.
  3. Open the build folder and delete its contents.
  4. In the build folder, create a new folder called QuitCMI.plugin.
  5. Close the build folder and open the QuitCMI.pbproj file with Xcode.
  6. In Xcode (not the Finder!), open the "Implementation Files" group and rename the SampleCMPlugIn.c file to QuitCMI.c. (If you rename the file in the Finder, Xcode will lose track of it.)
  7. Open the project group (it should still be named "SampleCMPlugin" at this point) and open the "Resources" subgroup. Double-click the InfoPlist.strings file. In this file, change each occurrence of "SampleCMPlugIn" to "QuitCMI". (Edit the copyright notices as you see fit.)
  8. Open the "Targets" group and right-click on the "SampleCMPlugIn" target. Rename the target to "QuitCMI".
  9. At this point, if the Target information editor isn't visible in the project window, click the Show Editor button (it's to the left of the magnifying glass in the project window), or double-click the target to bring up the editor in its own window.
  10. In the editor, open the "Settings" section, and then open the "Simple View" section. Change the product name to "QuitCMI".

  11. Scroll down a bit to the GCC Compiler Settings section and notice that the flag to generate debugging signals is turned on. You don't have to turn it off now, but you'll definitely want to turn it off before you release the final version of your CMI.
  12. Now open the "Info.plist Entries" section of the editor. Under Basic Information, change the identifier from com.apple.SampleCMPlugIn to com.EGOSystems.QuitCMI. (You would, of course, use your own company and product name here.)
  13. Change the version number to "1.0".
  14. Under Display Information, change the Display Name to "QuitCMI".
  15. For the "Version Displayed in Finder info" field, enter whatever you want to appear when the user performs a "Get Info" operation on the CMI in the Finder.

  16. At the bottom of the "Info.plist Entries" section, find and click on the "Expert View" section. This will bring up a simple pList editor that shows the contents of Info.plist in a much more "raw" format than the Simple View we've been working with. Scroll down to the bottom of the pList entries and completely expand the CFPlugInFactories and CFPlugInTypes entries. When you do, you should see something like this (note that I've highlighted these entries in the screen shot):

    The CFPlugInTypes entry is an array that does two things: it identifies the type of plugin this is and supplies the UUID of the plugin. In this case, it's a CMI (identified by the Apple-defined value starting with "2F6522E9-") that has a UUID that starts with "3487BB5A-". The CFPlugInFactories entry ties that UUID to a particular function in the CMI that will instantiate an instance of the CMI (which is why they call it a "factory.") In this case that function is the SampleCMPluginFactory function. (Which takes us back to our earlier discussion of COM and how the SampleCMPlugInExamineContext and SampleCMPlugInHandleSelection functions get invoked.)
  17. We need to change these values to represent our new CMI. So the first order of business is to generate a new UUID. For that, fire up UUID Generator and paste the results into a new TextEdit document so that you can get to them later. When I ran UUID Generator, this is what I saw:

    And this is what ended up in my TextEdit document:

    F4054EF0-A9A4-11D8-BAE7-000393D128F2
    {0xF4,0x05,0x4E,0xF0,0xA9,0xA4,0x11,0xD8,0xBA,0xE7,0x00,0x03, 0x93,0xD1,0x28,0xF2}
    #define YOUR_CMPLUGINFACTORYID ( CFUUIDGetConstantUUIDWithBytes( NULL,0xF4,0x05,0x4E,0xF0,0xA9,0xA4,0x11,0xD8,0xBA,0xE7,0x00,0x03, 0x93,0xD1,0x28,0xF2 ) )
    /*F4054EF0-A9A4-11D8-BAE7-000393D128F2*/

  18. The first line of this is exactly what's needed for CFPlugInTypes and CFPlugInFactories. Simply copy this value into those fields and change the function name in CFPlugInFactories from SampleCMPlugInFactory to QuitCMIFactory, and you'll end up with this:

    Once you verify that this is all correct, save the project and close the Target.

  19. Open the QuitCMI.c source code file and select the "Single File Find" command from the Find menu. In the "Find" field, type "SampleCMPlugin" and in the "Replace" field, type "QuitCMI". Click the "Replace All" button.
  20. Once that's complete, put "com.apple" in the "Find" field and "com.EGOSystems" in the "Replace" field, and click the "Replace All" button again. You should end up with line 724 looking like this:
    CFStringRef bundleCFStringRef = CFSTR("com.EGOSystems.QuitCMI"); (Remember that this is the line that loads our bundle resource, so we have to change it to reflect the new identifier we gave the bundle in step 12.)
  21. Find the definition for kQuitCMIFactoryID and replace the old UUID with the one that you specified in the CFPlugInFactories pList entry. When you finish, it should look something like this:
    #define kQuitCMIFactoryID ( CFUUIDGetConstantUUIDWithBytes( NULL,0xF4,0x05,0x4E,0xF0,0xA9,0xA4,0x11,0xD8,0xBA,0xE7,0x00,0x03, 0x93,0xD1,0x28,0xF2 ) )
    (You might notice that the old value for kSampleCMPlugInFactoryID doesn't match what was in the Info.plist file. This is an error in the SampleCMPlugin code.)
  22. Finally (hurray!) look just below the kQuitCMIFactoryID definition and you'll see an enum definition. This definition (for a couple of contextual menu constants) has become redundant (these constants are now defined in the Menu.h header file) since SampleCMPlugIn was released, so comment it out. (If you leave it in, the code won't compile.)

At this point, save the project and build it. If all goes well, you can install and test it, and you should see something like this:

Whew! That's a lot of work just to change the name of the thing, isn't it?

Quit That!

With all that behind us, we can finally add our custom code to create our CMI. I certainly don't expect you to type all of this code, so I'm making the entire project available here. Download it and then follow along in the QuitCMI.c file as I go over the steps below:

  1. Every CMI will be somewhat different, so the first thing you need to do is figure out which additional headers your project will need. In this case, we'll need access to process information (to figure out which process are active and therefore, "quittable"), and we'll need to fiddle with Core Foundation (CF) style strings, so I added includes for Process.h, CFBase.h, and CFString.h. (Lines 16, 18 and 19.)
  2. Since I'll be juggling process, I also need some way to keep a list of them in memory. Towards that end, I also created a new structure called pInfo. (Lines 74 to 78.)
  3. You'll also notice that there are several things missing from QuitCMI.c that were in SampleCMPlugin.c. Namely:
  4. I've also changed the AddCommandToAEDescList function so that it no longer adds ID values to the menu item strings that it stuffs into outCommandPairs. (Lines 298 to 300.)
  5. Finally, these three functions make up the "meat" of QuitCMI:

Final Tweaks

There are a few more things to do before the project is finished. Since SampleCMPlugIn was intended to be a comprehensive example, it comes complete with a lot of debugging statements that write information out to the console. To remove these, search for "DEBUGSTR" and "printf" and comment out each line that you find. (You should also go back and turn off debug code generation in the compiler as well.)

Finally, if you build the project as it stands at this point, you'll notice that the menu item that appears in the contextual menu is named "QuitCMI". While this is fine for an internal name, it would be better if the menu item were simply named Quit. To make this change, return to the Expert View of the Info.plist editor and change the CFBundleName from "QuitCMI" to just "Quit". Then rebuild the project. When you install and test it, you should see something like this:

That's It!

At first glance, it might seem that creating a CMI is a lot of work. Well, at first, it is!

However, once you get a couple of them under your belt, you'll find yourself breezing through the tedious bits more and more quickly. Once that's done, you can get right to creating your own "Why didn't Apple include this?" CMI.

I hope that you've enjoyed these two articles and found the information useful. I look forward to seeing your contextual menu items pop up when I right-click my mouse!

Steven Disbrow is the President of EGO Systems, Inc., a computer consulting firm in Chattanooga, TN.


Return to the Mac DevCenter

Copyright © 2009 O'Reilly Media, Inc.