macdevcenter.com
oreilly.comSafari Books Online.Conferences.

advertisement

AddThis Social Bookmark Button

Inside Contextual Menu Items, Part 2
Pages: 1, 2

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:

  • 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.

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:
    • The gNumCommands variable. I'll be using process numbers to identify my menu items, so this ID holder isn't needed.
    • The following functions are also gone: 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.)
  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:
    • 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!

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