For the toolbar in my iCatalog Editor Mac app I wanted to have have a toolbar button that would show a drop down menu for selecting what kind of hot zone the user wants to insert. iBook Author has a button like this and I was searching for way to get a similar look.
AppKit does not have this as a standard component, but I found two approaches that would yield a similar looking result.
The first approach I found uses a regular NSButton as view of the NSToolbar item. Though this causes multiple visual problems, the size is off compared to other “normal” toolbar items, and pushing the button will always highlight the entire image area. We want only the image itself to darken, like other toolbar items do.
The second – winning – approach makes use of an NSPopUpButton. Most controls from AppKit employ so called NSCell subclasses which are more or less in charge of drawing the contents of the control when asked to. There are also specialized versions of cells that have extra functionality, like NSPopUpButton cell which has an NSMenu of NSMenuItems attached to it which it will present when clicked.
Unfortunately there is no look of NSPopUpButton which would work for us, it is meant to provide a drop down list, not an image button. Also it requires quite a bit of fiddling to get the settings to produce the result we want. Another annoying side effect of Interface Builder is that with almost every change it reduces the controls minimum height to 28, even though we want it to be no less than 32 to fit the Add button image we lifted from iBooks Author.
Let’s walk through the relevant settings:
We created the Toolbar Item by dragging a “Pop Up Button” from the object library to the toolbar. This gives you the default look of a drop down combo box.
On the Pop Up Button change:
- Type: Pull Down
- Arrow: No Arrow
- Style: Square
- Visual: Not Bordered
- Title: Remove, this blanks the first menu item as well
- Image: Set to the name of your image, “TB_Add” in our case
- Scaling: None
Change the Pop Up Button Cell class to the name of our custom cell class we’ll create below. If you don’t then it will display fine, but there will be no down arrow.
Finally – since all changes mess with the toolbar item sizing – adjust the minimum and maximum size to fit:
I found a width of 45 and height of 32 working well. I don’t know why Interface Builder goes about adjusting these values if you change something in the item, so be careful to check these values in the end, also you have to ignore that the button looks cut off in the main editor view. Trust me it works.
Now for the arrow. We need to do custom drawing for that. NSPopUpButtonCell inherits from NSMenuItemCell the drawImageWithFrame:inView: as well as imageRectForBounds: which we will have to override.
@implementation DTPopUpButtonCell - (void)drawImageWithFrame:(NSRect)cellFrame inView:(NSView *)controlView { // draw image with modified rect for left-aligning [super drawImageWithFrame:cellFrame inView:controlView]; // draw the triangle to the bottom/right of image NSRect imageBounds = [self imageRectForBounds:cellFrame]; NSBezierPath *path = [[NSBezierPath alloc] init]; [path moveToPoint:NSMakePoint(NSMaxX(imageBounds)-1, NSMaxY(imageBounds)-9)]; [path lineToPoint:NSMakePoint(NSMaxX(imageBounds)+5, NSMaxY(imageBounds)-9)]; [path lineToPoint:NSMakePoint(NSMaxX(imageBounds)+2, NSMaxY(imageBounds)-5)]; [path closePath]; [[NSColor colorWithDeviceWhite:20.0/255.0 alpha:0.9] set]; [path fill]; } - (NSRect)imageRectForBounds:(NSRect)theRect { NSRect rect = [super imageRectForBounds:theRect]; // make room for 5 pixels at the right rect.origin.x -= 5; return rect; } @end |
My first instinct had been to create a custom NSPopUpButton subclass and mess with the drawing in its drawRect: method. But the problem with this approach is that the above mentioned drawImageInFrame:inView: is hard-wirded to use its own an imageRectForBounds: which always appears to use the view bounds we are drawing into.
I was unable to get the image to display in a different position no matter what frame I passed to the drawing method. I only ended up with a still-centered but cut off image. The above subclass of NSPopUpButtonCell however works like a charm.
As I alluded to before this work because the first menu item is invisible due to having no text, so when building your custom menu contents you need to insure that there is always an empty item at the first position.
Putting your own dynamic menu items in there is easy. All you need is to connect the NSMenu to an outlet of your File’s Owner and then you can go about customizing it:
// remove all but the first item while ([self.hotZoneMenu.itemArray count]>1) { [self.hotZoneMenu removeItemAtIndex:1]; } // set up menu items from an array [document.hotZoneTemplates enumerateObjectsUsingBlock:^(NSDictionary *oneHotZoneDict, NSUInteger idx, BOOL *stop) { NSString *name = oneHotZoneDict[@"Name"]; NSMenuItem *menuItem = [[NSMenuItem alloc] initWithTitle:name action:@selector(createHotZoneFromSelection:) keyEquivalent:@""]; menuItem.representedObject = oneHotZoneDict; [self.hotZoneMenu addItem:menuItem]; }]; |
Since NSMenuItems can have an image you can add an icon representing each option as well.
Conclusion
Customizing a stock-controls drawing on for Mac typically involves fiddling with NSCells rather than the views themselves. By using our own subclass we preserve the highlighting and pop-up functionality of the NSPopUpButtonCell. Instead we tweaked the image rectangle to make room for our own disclosure triangle which gets added when the image is being drawn.
It would be nice if Apple offered this image-only version of NSPopUpButton which also does not require blanking the first menu item. But alas we have a workaround.
Categories: Recipes
With regards to the problem about Xcode changing the side of the toolbar item to 25, back from 32, I found a way to make it stop.
Given the heirarchy:
Main Window
–Toolbar
—-Toolbar Item – Custom View
——Pop Up Button
Select “Pop Up Button” and view the Size Inspector and change Height from 25 to 32.