Inside Contextual Menu Items, Part 2
by Steven Disbrow06/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:
thisInstance: This is a pointer to the currently executing instance of the CMI.inContext: This is a pointer to anAEDescstructure. As the "AE" in the name implies, this is a data type that's used to support Apple Events in Mac OS X. In its simplest form, anAEDescis a structure that contains a handle to some data and a four-character description of what that data actually is. (For example, the type could betypeAlias, which would mean that the data would be an alias to a file or device.) However, it can be a list ofAEDescstructures (also known as anAEDescList), each of which has its own type of data.
So, for example, when you right-click on one or more Finder icons, theinContextparameter will point directly to anAEDescstructure oftypeAEList. The data in that list will be an actual list ofAEDescitems, each oftypeAlias. Each of thosetypeAliasitems will contain an alias that refers to one of the objects that was selected in the Finder. Of course, that's just the Finder. Every other application will supply its own type of information. Regardless of what that the type is, however, this parameter is the primary source of information that a CMI will use to determining the context in which it was invoked.outCommandPairs: This is a pointer to anAEDescListstructure that's supplied by the system. This is where you will place any menu items that you want displayed in the final contextual menu. If you've written Macintosh apps before, you should realize that these aren't menu items in the traditional sense. Instead, it's anAEDescListof Unicode strings. The system takes this list and converts them into the menu items found in the contextual menu. (Among other things, this means that you can't easily assign key equivalents to your contextual menu items.)
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:
- 724 and 725: These lines load the bundle that's associated with this CMI. (Note that, as in many other places in their frameworks, Apple is following the Java package-naming convention. You should follow this convention, as well.)
- 730: This variable will hold the ID values used by our menu items. Note that SampleCMPlugIn uses sequentially numbered items (with different starting values for each submenu). How you number your menu items, however, is completely up to you.
- 740: A quick check to make sure that the value of
inContextis notNULL. If it is, nothing is selected. Of course, the whole point of a CMI is to act upon some selection, but if you are trying to provide a service that doesn't require a selection (a shortcut to the "Shutdown" or "Log Out" commands, perhaps), this check wouldn't be necessary. - 746: Assuming that we do have a selection, this line calls a handy
little function,
AddCommandToAEDescList, which adds a desired menu item string, along with its ID value, to theoutCommandPairsAEDescList. In this case, it's adding a dash (-), which is the equivalent of a dividing line. (This is why the dividing line appears before all the other items in the final contextual menu; it's added first!)
An important note about theAddCommandToAEDescListfunction: study this function when you get the chance. It's a good primer on how to manipulateAEDescandAEDescListstructures. If you write a lot of CMIs, you'll find yourself using it over and over again. (Of course, you'll need to modify the function slightly to keep menu ID numbers from showing up. We'll do that a bit later.) - 750, 751, and 754: Remember those lines that loaded the bundle for
the CMI? Line 750 extracts the name of the bundle so that it can be used in
the next line to create the "Inside 'SampleCMPlugIn'" menu item.
Line 754 actually adds the menu item via the
AddCommandToAEDescListfunction. - 760 to 762: These three lines report the type found in the
AEDescpointed to byinContext. - 765 to 780: In these lines, the CMI checks to see if the type of
inContextistypeAEList. If it is, the functionCreateFileSubmenuis called to create the first submenu with the list of selected files. (We'll look at the creation of submenus a bit later on.) If not, the CMI checksinContextto see if it istypeAlias. If so, it attempts to coerce the type totypeAEList. If that's successful, it passes the list toCreateFileSubmenuto create the list of files. - 781 to 798: If the type of
inContextisn'ttypeAEListortypeAlias, the CMI checks to see if it'stypeChar. If so, it doesn't actually do anything with the text in theAEDesc, it simply adds a menu item that reports that text was found. - 801: This line calls the function
CreateSampleSubmenu, which creates the static submenu that comes after everything else in the final contextual menu. - 810: When the function returns, the system takes the information
the function has written to
outCommandPairsand adds it to the contextual menu that the user sees.
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.
Pages: 1, 2 |

