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.)
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.)
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.
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!)
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 AEDesc structure.
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.
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:
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.AddCommandToAEDescList, which adds a desired
menu item string, along with its ID value, to the outCommandPairs
AEDescList. 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!) 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.)AddCommandToAEDescList
function.AEDesc
pointed to by inContext. 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.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. CreateSampleSubmenu,
which creates the static submenu that comes after everything else in the final
contextual menu. 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.
|
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:
thisInstance: This, again, is a pointer to the currently executing
instance of the CMI.inContext: As before, this is a pointer to an AEDesc
structure. In fact, it's the same information that the SampleCMPlugInExamineContext
received when it was called. Getting this information twice allows your CMI
to act on the selection without having to remember it. inCommandID: This is an integer containing the ID value of
the menu item that the user selected.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.
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.)
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.)

com.apple.SampleCMPlugIn
to com.EGOSystems.QuitCMI. (You would, of course, use your own
company and product name here.) 
CFPlugInFactories
and CFPlugInTypes entries. When you do, you should see something
like this (note that I've highlighted these entries in the screen shot):

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.)
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*/
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.
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.)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.)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?
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:
pInfo.
(Lines 74 to 78.) gNumCommands variable. I'll be using process numbers
to identify my menu items, so this ID holder isn't needed.Copy_CFStringRefToCString,
CreateFileSubmenu, HandleFileSubmenu, CreateSampleSubMenu,
and HandleSampleSubMenu. I'm not building lists of files
or anything like that (and most of my string manipulation is of CF strings),
so none of these functions is necessary. (Note, however, that the CreateFileSubmenu
function actually lives on as the CreateProcessSubmenu function,
discussed below.) AddCommandToAEDescList function so that
it no longer adds ID values to the menu item strings that it stuffs into outCommandPairs.
(Lines 298 to 300.) QuitCMIExamineContext: This is very similar to SampleCMPluginExamineContext.
One difference is that since we are providing a "Quit" service,
we don't really care about the context. All we want to do is build our
list of processes and return that to the operating system. This is done
by the CreateProcessSubmenu function (discussed below). One
other trick in this function is an example of how to determine the name
of the host application in which the CMI was invoked. In this case, it's
checking for the Finder, but it could be any application that you wish.
Nothing is done with this information; I've just left it in as an example
of how it's done.CreateProcessSubmenu: This function (a derivative of the
old CreateFileSubmenu function) receives the name of our
CMI (in a parameter named theSupercommandText) and uses it
to build a menu item with a submenu that contains the names of all of
our active applications. As mentioned before, the menu IDs that are used
are the actual process IDs of each application. (You'll note that a simple
bubble sort is used to sort the names of the applications alphabetically.
Bubble sorts are slow for large numbers of items, but, hopefully, no one
out there will have 1,000 applications open at any given time!) For the
truly gory details of what's going on in this function, refer to Apple's
online Interapplication
Communication documentation. QuitCMIHandleSelection: This function receives the ID of
the menu item that was selected (which, again, is the ID of the corresponding
process) and then builds a "Quit" Apple Event (type kAEQuitApplication)
and sends that to the selected process. That's all there is to it! 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:
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.