Home > code, tutorial > NSMenuExtra – working with undocumented APIs

NSMenuExtra – working with undocumented APIs

After reading this article on how to build a SystemUIServer menu, I gave it a try, and built a cute menulet. Doing so, I bumped into some issues and I thought it would be nice to have a full step-by-step and up to date tutorial with source code attached.Let’s start with what a NSMenuExtra really is. Any program can put an icon, static or dynamic, in the menubar next to TimeMachine, Wi-fi, date/time and all the other icons Apple already put there for your use. You can do this, using the well documented NSStatusItem class. There are few limitations in doing so:

NSStatusItem (public API) NSMenuExtra (private API)
OS X will pile up the icons in the menu bar in a First come first served fashion, which means the order is not saved anywhere: the first program to request a space will get it. Between reboots, it might change. These kind of items keep their relative order between reboots.
You can’t rearrange this kind of items: wherever spot they get they stick to that as long as you restart the program. These icons can be moved (by cmd+click and drag) and rearranged by the user.
A NSStatusItem is a normal application so in order to keep it active between reboots you need to place it in your login items under “System Preferences – Accounts”. To install a NSMenuExtra you double click on its bundle, to uninstall it you just cmd+click and drag it out of the menubar as you would do with Dock items. When a NSMenuExtra is installed, SystemUIServer takes care of launching it at each system boot.
A NSStatusItem is a normal application so in order to keep it active between reboots you need to place it in your login items under “System Preferences – Accounts”. To install a NSMenuExtra you double click on its bundle, to uninstall it you just cmd+click and drag it out of the menubar as you would do with Dock items. When a NSMenuExtra is installed, SystemUIServer takes care of launching it at each system boot.

What exactly is a NSMenuExtra then? It’s a plugin, with “.menu” extension, that gets loaded by the system service SystemUIServer which is the process in charge of displaying system menulets on the menubar. That means that the plugin’s code will run inside a system service. If the code is buggy, the whole SystemUIServer will have issues. For example, if you install a NSMenuExtra which crashes, the whole SystemUIServer will keep crashing and restarting and all you will see is a half empty menubar that keep flashing. Apple understood this and made NSMenuExtra a private class. Users are smart and reverse-engineered the API. Starting with MacOS X 10.2 Apple made some efforts to prevent the loading of third-party MenuExtras so you now have to go through an extra step and use a MenuExtra enabler before being able to load your menulet (more on that later).

Let’s see how to build an easy NSMenuExtra plugin that, once loaded, will just show an image. We will also see how to put a contextual menu and to open windows as result of menu actions.

1. Obtain .h definitions for NSMenuExtra (a.k.a. reverse-engineer the API)

In order to create a menu extra, you have to subclass NSMenuExtra. In order to compile any code that subclasses NSMenuExtra, you need NSMenuExtra definition. Since we are dealing with a private API, don’t expect to find the NSMenuExtra.h in some known include path.

NSMenuExtra is part of the private framework called SystemUIPlugin. Fortunately, due to Obj-C nature, is possible to derive a class definition starting from a compiled framework. To do so, is necessary to use a command line utility called class-dump and available here. Unpack it somewhere then fire up Terminal.app and type:

$ cd [directory where you unpacked the utility];
$ ./class-dump -H /System/Library/PrivateFrameworks/SystemUIPlugin.framework/Versions/A/SystemUIPlugin

This will create a bunch of .h files in the current working directory. We are interested in NSMenuExtra.h and NSMenuExtraView.h only. DockExtra.h is related to Dock’s plugins which is now obsolete in Snow Leopard due to the introduction of Dock Tile Plug-in architecture and NSMenuExtraAnimated.h is a subclass of NSMenuExtra that helps in the creation of MenuExtra with animated images (TimeMachine for example). CDStructure.h contains well-knows struct GCPoint, GCRect, GCSize which are already defined in any cocoa project.

Open NSMenuExtra.h, go to line:

- (id)accessibilityHitTest:(struct CGPoint)arg1;

and change it to:

- (id)accessibilityHitTest:(CGPoint)arg1;

Then open NSMenuExtraView.h and do the same changing:

- (id)initWithFrame:(struct CGRect)arg1 menuExtra:(id)arg2;
- (void)drawRect:(struct CGRect)arg1;

to:

- (id)initWithFrame:(CGRect)arg1 menuExtra:(id)arg2;
- (void)drawRect:(CGRect)arg1;

Last thing, open “NSMenuExtra.h” and remove the following line:

#import "NSStatusItem.h"

then open “NSMenuExtraView.h” and remove the following line:

#import "NSView.h"

Now you have all the include files needed to work with a NSMenuExtra.

2. Create a XCode project

Launch XCode, create a new project from the “Bundle template” (under “Framework & Library”), select Cocoa as Framework and name it however you want (MyMenuExtra in the example). The project need some fine tuning before is ready for compile time:

  1. Double click “MyMenuExtra” under Targets in the left tree-view, go to “Build” tab and change the setting “Wrapper Extension” to “menu” (without quotes);
  2. Double click “info.plist” under Resource in the left tree-view, change the key “Principal class” to “MyMenuExtra”;
  3. On the same “info.plist” file, change the key “Bundle identifier” to something more meaningful as "com.duhanebel.MyMenuExtra". This time close the window and save the file.
  4. Left click on “Other Frameworks” under “Frameworks and Libraries” in the left tree-view, select “add – Existing frameworks..” then click on “Add other…”, navigate to "/System/Library/Private Frameworks/" and select "SystemUIPlugin.framework";
  5. Drag NSMenuExtra.h and NSMenuExtraView.h (from step 1) into “Other sources” in the left tree-view. When prompted, tick “Copy items into destination group’s folder” and click “Add”.

Your project is now ready to go.

3. Write the code

Our simple project is nothing fancy, so we basically need two classes: one NSMenuExtra, which is the main class of the project and one NSMenuExtraView, which handles the GUI of the plugin (a.k.a. the image we are going to show on the menubar).

Create the new class from the “Objective-C class” template. Name it “MyMenuExtra” and make it subclass of NSMenuExtra. In MyMenuExtra.h add the directives to include SystemUIPlugin framework’s class definitions and to forward declare the view we are going to use:

#import "NSMenuExtra.h"
#import "NSMenuExtraView.h"
@class MyMenuExtraView;

Add the following instance variable that holds our custom view:

MyMenuExtraView *theView;

In MyMenuExtra.m add the following import directive:

#import "MyMenuExtraView.h"

It’s time to write some setup code, implement NSMenuExtra’s method initWithBundle: with the following code:

- (id)initWithBundle:(NSBundle *)bundle
{
    self = [super initWithBundle:bundle];
    if(self == nil)
        return nil;

    // we will create and set the MenuExtraView
    theView = [[MyMenuExtraView alloc] initWithFrame:
			   [[self view] frame] menuExtra:self];
    [self setView:theView];

    return self;
}

- (void)dealloc
{
    [theView release];
    [super dealloc];
}

It’s time to work on the view that will draw our image on the menubar. Create a second new class from the “Objective-C class” template. Name it “MyMenuExtraView.h” and make it subclass of NSMenuExtraView

. As before, add include directives in MyMenuExtraView.h:

#import "NSMenuExtra.h"
#import "NSMenuExtraView.h"

In the source file we need to implement a single method that will take care of drawing a black circle on the menubar:

- (void)drawRect:(NSRect)rect
{
    if([_menuExtra isMenuDown]) {
        [[NSColor selectedMenuItemTextColor] set];
    } else {
        [[NSColor blackColor] set];
    }
    NSRect smallerRect = NSMakeRect(4, 4, rect.size.width-8, rect.size.height-8 );
    [[NSBezierPath bezierPathWithOvalInRect: smallerRect] fill];
}

Here we make use of the protected instance variable _menuExtra to access our instance of NSMenuExtra
NSMenuExtra-menuclick.png
and ask if the menu has been clicked in order to reverse the colour of the dot to follow the default behaviour of MenuExtras.

The codebase of our project is done: you can compile and try it. The problem is that it doesn’t do much except sit there as a black dot. Let’s see how to add some functionality.

4. Add a menu and open windows

When an user clicks on our black dot on the menubar, SystemUIServer visualises whatever menu we return with the (NSMenu *)menu of NSMenuExtra class.
Since our menu is going to be static, let’s create it once when the plugin loads, and then implement NSMenuExtra‘s (NSMenu *)menu method to return a reference whenever SystemUIServer calls it. If you need to make some changes to the menu, based on the time or some outer events, (NSMenu *)menu is the place to do such changes.

Add two instance variables to MyMenuExtra and a method that we will use as a callBack for user’s clicks the menu:

@interface MyMenuExtra : NSMenuExtra {
	MyMenuExtraView *theView;
	NSMenu *theMenu;
	NSWindow *theWindow;
}
- (void)showMyWindow:(id)sender;
@end

Then in “MyMenuExtra.m” change initWithBundle:, dealloc and implement menu as the following:

    //[...]
	theMenu = [[NSMenu alloc] initWithTitle: @""];
    [theMenu setAutoenablesItems: NO];
    [theMenu addItemWithTitle: @"Useless item 1"
                       action: nil
                keyEquivalent: @""];
	[theMenu addItem:[NSMenuItem separatorItem]];
	NSMenuItem *menuItem = [theMenu addItemWithTitle:@"Less useless item 2"
                                               action:@selector(showMyWindow:)
                                        keyEquivalent:@""];
    [menuItem setTarget:self];

    return self;
}
- (void)dealloc
{
    [theView release];
    [theMenu release];
    [myWindow release];
    [super dealloc];
}
- (NSMenu *)menu
{
	return theMenu;
}

It’s time to implement showMyWindow to show a window that might be use for quick preferences or as an about box. In our example, it’s just an empty NSWindow. You might want to improve this code because it will span a different window each time the menu button is pressed. A better approach is to create a custom NSWindowController that manages the window, but here we keep it simple.

- (void)showMyWindow:(id)sender
{
    //Create the window if needed
    if(!theWindow) {
        NSRect windowRect = NSMakeRect(100, 200, 100, 200);
        theWindow = [[NSWindow alloc] initWithContentRect:windowRect
                                                styleMask:NSTitledWindowMask |
                                                          NSClosableWindowMask
                                                  backing:NSBackingStoreBuffered
                                                    defer:YES];
        //Tells NSWindow we want to reuse theWindow
        [theWindow setReleasedWhenClosed:NO];
	}

    // Bring SystemUIServer process on front to prevent the window to open on the background
    ProcessSerialNumber psn = { 0, kCurrentProcess };
    SetFrontProcess(&psn);

    //Show the window
	[theWindow makeKeyAndOrderFront:self];
}

The call to SetFrontProcess() is needed to force focus on the menubar. Since the window we are creating is span by the SystemUIServer process which, by default, doesn’t have focus, myWindow will pop-up in the background unless we force focus on it before calling for display.

Now you have a MenuExtra that actually does something. The source code for this demo project is available here under creative common license.

5. Use custom users defaults (and don’t mess with SystemUIServer’s ones)

Let’s think about theWindow as a preference window. You will need to save user’s preferences to users defaults. Apple makes it easy to load/save user defaults. Just retrieve the shared user defaults instance with [NSUserDefaults standardUserDefaults] and you are done. There is a problem, though. The defaults instance obtained that way will write data to SystemUIServer’s user defaults .plist file. It’s not polite to pollute systems preferences files with our settings.

Fortunately, someone thought that through already. Download BundleUserDefaults.h and BundleUserDefaults.m from here and add them to your project, under “Classes” as we did before.

BundleUserDefaults is a subclass of NSUserDefaults which wraps around CFPreferences and allows you to specify the persistent domain name for your defaults. Initialise it with your domain string, and use it like you would normally use NSUserDefaults. The domain string could be anything, but I suggest you to stick to your Bundle identifier value, in our case: "com.duhanebel.MyMenuExtra".

If you want your preferences to be saved to "~/Library/Preferences/com.duhanbel.MyMenuExtra.plist", then the code would look something like this:

BundleUserDefaults *customDefaults = [[BundleUserDefaults alloc] initWithPersistentDomainName:@"com.duhanebel.MyMenuExtra"];

// Load and save preferences..
[customDefaults setBool:YES forKey:@"somePref"];
NSInteger someValue = [customDefaults integerForKey:@"someOtherPref"];
// Your code here

If you would like to leverage the power of cocoa bindings and use NSUserDefaultsController in your code, you should know that there is an undocumented, private method in NSUserDefaultsController class, that let you set a custom NSUserDefaults (or, in this case, a BundleUserDefaults). That method is:

- (void) _setDefaults:(NSUserDefaults *)defaults;

and it’s ready to use if you included BundleUserDefaults.h in your source code.

6. Enable SystemUIServer to accept your plugins

If you just fire up the bundle double-clicking on MyMenuExtra.menu, SystemUIServer will refuse to load your plugin and you will see, well.. nothing! That’s because starting from OSX 10.2 Apple has tried to make it difficult to install third-party plugins on SystemUIServer for the reasons stated at the beginning of the article.

You need to crack the system, placing a loader between you and Apple, and that’s what menu cracker does. Download it and run MenuCracker.menu. Again, nothing will happen but if you try to load your MenuExtra, it will appears on the menubar this time. Hurray!!

MenuCracker.menu is just a NSMenuExtra plugin that, once loaded, tricks SystemUIServer into loading third-party .menu files. As a MenuExtra it will be remembered by the system and loaded at each reboot so you just need to open it once (as long as you don’t move/delete the bundle). It can detect already loaded instance so running it twice won’t have side effects.

7. Update MyMenuExtra

SystemUIServer caches loaded .menu files. That means, in order to update your MenuExtra and install the new version, you have to restart the SystemUIServer instance. Fire up Terminal.app and write:

$ killall SystemUIServer

you will see part of the menubar disappear, and come back with a shiny new version of your tiny program.

Advertisements
  1. June 5, 2010 at 9:44 am

    Neat tutorial! Glad to find something up-to-date on NSMenuExtra. One caveat: when I read your tutorial, I thought you would need to tell every user to ‘install’ the MenuCracker.menu extra… which is not necessary. MenuCracker 2 (which is in beta, and checked into the MenuCracker CVS as of late 2009) seems to provide an API for you to pull MenuCracker into your project as an external resource, which lets you ship just a single .menu. Much more user-friendly, don’t you think?

  1. No trackbacks yet.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

%d bloggers like this: