« Building NSMenuExtra - A Small Tutorial
By Rustam Muginov
// What you should understand before creating a MenuExtra
- This is the software which is using private, undocumented MacOS X API. Where are no any guarantees what this software would work in the following major MacOS X versions.
- In the MacOS X 10.2, Apple made some effort to prevent third-party MenuExtra being loaded. Under MacOS 10.2, you would need to use Menu Extra Enabler from Unsanity or MenuCracker from james_007_bond.
- The NSMenuExtra class is the subclass of NSStatusItem, which is the recommended way of putting custom menus to the right side of the menu bar. Although not as convinient and powerful as NSMenuExtra (you can not Command-drag to rearrange or remove NSStatusItem, you have to have a running application to keep it in the menu bar, e.t.c.), it is still documented (at least partialy) and recommended way. Perharbs you should consider if NSStatusItem is sufficient for your requirements first. A nice tutorial is available at Cocoa Dev Central.
// Available implementations of NSMenuExtra
Where are several implementations available with source code. They are:
- Application Menu Switcher by Frank Vercruesse
- ClassicSpy by Konstantin Anoshkin
- MenuMeters by Alex Harper
They are very valuable examples, but personaly I've found that they are a bit too complex for the beginner. Another drawback, they are all distributed under GNU License, making it impossible for shareware/commercial developer to use their source code.
You will definitly want to look at their source code at a later time to learn more about the NSMenuExtra functionality.
// Reverse-engineering the API
The NSMenuExtra class resides in the SustemUIPlugin private framework. Objective-C keeps plenty of information about class names, inheritance, methods, e.t.c., so it is possible to get this information from the compiled binary. To obtain this framework API, we would use very useful class-dump utility by Steve Nygard. It is available here or here. Download the archive and unpack it. The class-dump is a command-line utility, so we will run it in terminal. If you put the "class-dump" binary inside the /Applications folder, then the command line would be /Applications/class-dump -e /System/Library/PrivateFrameworks/SystemUIPlugin.framework/Versions/A/SystemUIPlugin > SystemUIPlugin.h. Run this command, and check inside your home folder. You would find the "SystemUIPlugin.h" file in it.
We need to do some editing before it will be compilable. Open it in Project Builder and look at its nice content. First, add the #import <AppKit/AppKit.h> just under the comments. Then, correct the following:
- Remove the "?" sign in the struct declarations in the NSMenuExtra and NSDockExtra interfaces
- Replace the "expanded" struct _NSRect types in the initWithFrame and drawRect methods of NSMenuExtraView class. They should be:
- initWithFrame:(NSRect)fp12 menuExtra:fp28;
- (void)drawRect:(NSRect)fp12;
Now we have the API to the NSMenuExtra (and the whole SystemUIPlugin) private framework.
// Creating the project
Create a new ProjectBuilder project using the Cocoa Bundle template. Name it "Sample". We need to tune some parameters, so select the "Targets" tab and click the "Sample" target.
- In Settings/Expert View, change the "WRAPPER_EXTENSION" from "bundle" to "menu"
- In Info.plist entries/Basic Information, set Identifier to something like "com.MyCompany.SampleMenu"
- In Info.plist entries/Cocoa Specific, set the Principal class to "SampleMenuExtra".
We are done with project settings. Now click the "Files" tab. Add the "SystemUIPlugin.framework" either by drag-n-dropping it from Finder or via "Project/Add FrameWorks..." menu. Then remove the "main.c" file from the project. Now copy the "SystemUIPlugin.h" file from your home folder to the project folder and add it to Classes group.
// Creating source files
Our project will have two classes.
Create the new class from the "Objective-C class" template. Name it "SampleMenuExtra". In the header import the "SystemUIPlugin.h" and make our class a subclass of NSMenuExtra. Add the @class SampleMenuExtraView; forward declaration. We will add only two instance variable to our class:
NSMenu *theMenu;
SampleMenuExtraView *theView;
In the .m file import two interfaces - SampleMenuExtra.h and SampleMenuExtraView.h (we will make it later). We will implement three methods here.
- (id)initWithBundle:(NSBundle *)bundle { self = [super initWithBundle:bundle]; if( self == nil ) return nil; // we will create and set the MenuExtraView theView = [[SampleMenuExtraView alloc] initWithFrame: [[self view] frame] menuExtra:self]; [self setView:theView]; // prepare "dummy" menu, without any actions theMenu = [[NSMenu alloc] initWithTitle: @""]; [theMenu setAutoenablesItems: NO]; [theMenu addItemWithTitle: @"1" action: nil keyEquivalent: @""]; [theMenu addItemWithTitle: @"2" action: nil keyEquivalent: @""]; [theMenu addItemWithTitle: @"3" action: nil keyEquivalent: @""]; return self; }
- (void)dealloc { [theMenu release]; [theView release]; [super dealloc]; }
Finally, menu is just an accessor called by system when our menu need to be drawn.
- (NSMenu *)menu { return theMenu; }
Now create another class, SampleMenuExtraView, using the Objective-C class template. In the header add #import "SystemUIPlugin.h". Make the class a subclass of NSMenuExtraView.
In the source file, we will implement a single method. This method will be called when our menu extra has to draw its "title picture". We will not make something that is very complex and will just draw a circle that is sligtly smaller then the rect itself.
- (void)drawRect:(NSRect)rect { [[NSColor purpleColor] set]; NSRect smallerRect = NSInsetRect( rect, 4.0, 4.0 ); [[NSBezierPath bezierPathWithOvalInRect: smallerRect] fill]; }
Thats all of the class implementation!
Build the project. You will get a link warning that can be ignored. Now go to your build folder. You will see the "Sample.menu" bundle in it. Double-click it and enjoy.
// Removing MenuExtra
You can simply Command-drag the menu extra away from the menu bar. Then you have done it, the SystemUIServer still keep the cached information about the menuextra though. So if you change and recompile it, it will load the old version. Also, you would not be able to put the menuesxtra into the Trash and empty it because file is busy. You will need to stop the SystemUIServer with the terminal killall SystemUIServer command. This command will kill the process and MacOS X will automaticaly relaunch the SystemUIServer but without your menuextra. If you are doing some code editing/recompilation, remember to kill SystemUIServer before you run a changed menuextra.
// Where to go next?
The example above is very basic. It only demonstrates a "minimal" menu extra. What you would like to do is assign a target and action to menu command. Also, it is worth to load two images (general and "menu down") at the menuextra initialization and use them to draw in the SampleMenuView draw method. Use isMenuDown to check if user pressed the mouse in our menu.
Finally, study the already mentioned menu extras and check what they do.
Cool tutorial, but ummm.....Where's the source?
Posted by: Evol on November 7, 2003 07:32 PMHow often does Apple break compatibility in their APIs?
I'd love to make a replacement for the 'fast user switching' menulet, but not if I need to update it once every two months. Lazy? No. Easily annoyed? Yes.
Posted by: KVN on November 12, 2003 02:06 AMhow can i open a window, with this empty menu extra?
Posted by: Dave on November 13, 2003 03:10 PMFast user switching menulet: already done! :)
See:
http://0x2a.no-ip.org/mt/archives/000057.html
Evol - where are no sources on purpose. I tryed to make tutorial as understandable as possible, and in the way what anyone could reproduce it without any sources. If you having any problems building sample MenuExtra with this tutorial, please let me know.
Including the source mean where should be some kind of license, i was too lazy to write one :) Because where are no source code and no license, anyone is free to use the described tutorial for any kind of software - freeware, shareware, commercial... Just remember what you are using it at your own risk :)
Dave - what do you mean by "open the window?" You can do anything what you usualy do in cocoa, just assign target/actions to the menu items. And write the "action handlers" which would perform what you need.
i'm doing:
[theMenu addItemWithTitle: @"1" action:selector(myAction:) keyEquivalent: @""];
and
- (void)myAction:(id)sender
{
[NSBundle loadNibNamed:@"myNib.nib" owner:self];
}
and this won't work.. :-/
Dave, the Cocoa messaging require two "parameters", action and target. You have specified action, but did not specified target - the object which should receive the message. Please try the following:
NSMenuItem *theItem = [theMenu addItemWithTitle: @"1" action:selector(myAction:) keyEquivalent: @""];
[theItem setTarget:self];
This should solve the problem. To ensure what your action is called, put a debug string in it, something like:
- (void)myAction:(id)sender
{
NSLog( @"myAction just called! Woohooo!!! :)" );
[NSBundle loadNibNamed:@"myNib.nib" owner:self];
}
and look for debug output in the Console.
Posted by: Rustam Muginov on November 14, 2003 07:41 AMthanks rustam... this works :)
but the nib will be loaded every time i click on the menu item. is there a way to prevent this?
thx
Wicked tool man!
Posted by: Josef on November 30, 2003 06:27 AM