Inside Contextual Menu Items, Part 2

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 an AEDescstructure.
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, an AEDesc
is 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 be typeAlias,
which would mean that the data would be an alias to a file or device.) However,
it can be a list of AEDesc structures (also
known as an AEDescList), each of which has its own type of data.

So, for example, when you right-click on one or more Finder icons, the inContext
parameter will point directly to an AEDesc structure of typeAEList.
The data in that list will be an actual list of AEDesc items,
each of typeAlias. Each of those typeAlias items
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 an AEDescList
structure 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 an AEDescList of 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 inContext
is not NULL. 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 the outCommandPairsAEDescList. 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 the AddCommandToAEDescList function:
study this function when you get the chance. It's a good primer on how to
manipulate AEDesc and AEDescList structures. 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 AddCommandToAEDescList
function.

760 to 762: These three lines report the type found in the AEDesc
pointed to by inContext.

765 to 780: In these lines, the CMI checks to see if the type of
inContext is typeAEList. If it is, the function
CreateFileSubmenu is 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 checks inContext to see if it is typeAlias.
If so, it attempts to coerce the type to typeAEList. If that's
successful, it passes the list to CreateFileSubmenu to create
the list of files.

781 to 798: If the type of inContext isn't typeAEList
or typeAlias, the CMI checks to see if it's typeChar.
If so, it doesn't actually do anything with the text in the AEDesc,
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 outCommandPairs and 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.