macdevcenter.com
oreilly.comSafari Books Online.Conferences.

advertisement

AddThis Social Bookmark Button

Inside Contextual Menu Items, Part 2

by Steven Disbrow
06/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 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.

    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 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!)

    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.

Pages: 1, 2

Next Pagearrow