Inside Contextual Menu Items, Part 2
Pages: 1, 2
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
thisInstance: This, again, is a pointer to the currently executing instance of the CMI.
inContext: As before, this is a pointer to an
AEDescstructure. In fact, it's the same information that the
SampleCMPlugInExamineContextreceived 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 Glue That Holds it Together
At this point, you might be wondering what's so special about the
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
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
function on line 279. (We'll come back to the
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.)
- Duplicate the SampleCMPlugin folder and rename it QuitCMI. Open the QuitCMI folder.
- Rename the SampleCMPlugin.pbproj file to QuitCMI.pbproj.
- Open the build folder and delete its contents.
- In the build folder, create a new folder called QuitCMI.plugin.
- Close the build folder and open the QuitCMI.pbproj file with Xcode.
- 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.)
- 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.)
- Open the "Targets" group and right-click on the "SampleCMPlugIn" target. Rename the target to "QuitCMI".
- 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.
- In the editor, open the "Settings" section, and then open the
"Simple View" section. Change the product name to "QuitCMI".
- 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.
- Now open the "Info.plist Entries" section
of the editor. Under Basic Information, change the identifier from
com.EGOSystems.QuitCMI. (You would, of course, use your own company and product name here.)
- Change the version number to "1.0".
- Under Display Information, change the Display Name to "QuitCMI".
- 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.
- 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
CFPlugInTypesentries. When you do, you should see something like this (note that I've highlighted these entries in the screen shot):
CFPlugInTypesentry 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
CFPlugInFactoriesentry 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
SampleCMPluginFactoryfunction. (Which takes us back to our earlier discussion of COM and how the
SampleCMPlugInHandleSelectionfunctions get invoked.)
- 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:
#define YOUR_CMPLUGINFACTORYID ( CFUUIDGetConstantUUIDWithBytes( NULL,0xF4,0x05,0x4E,0xF0,0xA9,0xA4,0x11,0xD8,0xBA,0xE7,0x00,0x03, 0x93,0xD1,0x28,0xF2 ) )
- The first line of this is exactly what's needed for
CFPlugInFactories. Simply copy this value into those fields and change the function name in
QuitCMIFactory, and you'll end up with this:
Once you verify that this is all correct, save the project and close the Target.
- 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.
- 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.)
- Find the definition for
kQuitCMIFactoryIDand replace the old UUID with the one that you specified in the
CFPlugInFactoriespList 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
kSampleCMPlugInFactoryIDdoesn't match what was in the Info.plist file. This is an error in the SampleCMPlugin code.)
- Finally (hurray!) look just below the
kQuitCMIFactoryIDdefinition 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:
- 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.)
- 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.)
- You'll also notice that there are several things missing from QuitCMI.c
that were in SampleCMPlugin.c. Namely:
gNumCommandsvariable. I'll be using process numbers to identify my menu items, so this ID holder isn't needed.
- The following functions are also gone:
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
CreateFileSubmenufunction actually lives on as the
CreateProcessSubmenufunction, discussed below.)
- I've also changed the
AddCommandToAEDescListfunction so that it no longer adds ID values to the menu item strings that it stuffs into
outCommandPairs. (Lines 298 to 300.)
- Finally, these three functions make up the "meat" of QuitCMI:
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
CreateProcessSubmenufunction (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
CreateFileSubmenufunction) 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
"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!
Return to the Mac DevCenter
Add an icon to contextual menu item
2006-02-01 23:48:55 piti118 [View]
Add an icon to contextual menu item
2006-02-02 07:14:51 EGOSysDiz [View]
2005-03-23 21:57:09 Matthew Russell | [View]
Are the old Apple docs still useful?
2005-03-25 12:14:33 EGOSysDiz [View]
2004-07-02 08:07:39 GlobalMouser [View]
Copy & Paste
2004-07-02 20:36:36 EGOSysDiz [View]
2004-06-13 10:22:48 edalytical [View]
2004-06-14 07:14:41 EGOSysDiz [View]