I am attempting to add a custom view to an NSMenuItem in the OS X 10.10 Yosemite menu bar.
The custom view is simply an NSView background with an NSTextField “label”
At the WWDC 2019 AppKit Lab I worked through this issue with engineers from the AppKit team.
They were surprised that it did not work by default, and encouraged me to file (more) radars:
FB6143574 - Expose private API for NSMenuItem
_viewHandlesEvents
They were aware of a private API _viewHandlesEvents
on NSMenuItem.
// VibrantMenuBar-Bridging-Header.h
#import <AppKit/AppKit.h>
@interface NSMenuItem ()
@property (setter=_setViewHandlesEvents:) BOOL _viewHandlesEvents;
@end
Set viewHandlesEvents
to false
and the background of the custom view in the NSMenuItem will be selected and appear (somewhat) as expected.
There are still issues with how labels and other subviews react to the selection. Text View text is not properly changing color.
let menuItem = NSMenuItem(title: "", action: nil, keyEquivalent: "")
menuItem.view = label
menuItem._viewHandlesEvents = false
There are some other references to _viewHandlesEvents
on the internet:
Unfortunately there are currently several problems in Yosemite. As Matthes already mentioned, you can use labelColor()
and secondaryLabelColor()
. Using those colors do not cause the label to draw the strange background you are seeing.
However, labelColor()
only works fine for VibrantDark because there the label color is white when a NSMenuItem is both highlighted and when not highlighted. With VibrantLight the labelColor
is black and is therefore very difficult to read on on top of the blue highlight.
For the highlight color of the custom view of your NSMenuItem one might think that you should use selectedMenuItemColor()
given its name. The problem with this is that the color doesn't actually match the menu highlight color that you see in NSMenuItems without a custom view. The color is completely wrong for both VibrantLight and VibrantDark.
Tl;dr: So how can you create a custom NSMenuItem that uses the exact same text color and highlight color? You can't. You should use labelColor()
and selectedMenuItemColor()
but the former only works correctly for VibrantDark, and the latter doesn't match at all.
I really hope I am wrong because I am trying to achieve the same thing :(
Edit: Here is an example project if people want to have a look.
It is surely kinda hack, but it worked for me. Try adding an NSImageView with empty image to your custom view. Image view must be occupy the whole view.
Per AppKit engineers at WWDC, this doesn't really work with NSMenuItem. I added that answer to this question as well.
They suggested to instead use an NSPopover
to create a faux-NSMenu attached to an NSStatusItem menu bar helper.
Using code similar to the below results in vibrant background selection:
override func viewDidLoad() {
super.viewDidLoad()
let visualEffectView = NSVisualEffectView()
visualEffectView.material = .selection
// .menu or .popover for the non-selected background.
visualEffectView.state = .active
visualEffectView.blendingMode = .behindWindow
visualEffectView.isEmphasized = true
let label = NSTextField(labelWithString: "Hello, world!")
label.cell?.backgroundStyle = .emphasized
visualEffectView.addSubview(label)
visualEffectView.frame = view.bounds
label.setFrameOrigin(.zero)
view.addSubview(visualEffectView)
}
Response from a Apple Developer Technical Support ticket I opened in 2015:
This is a difficult problem to tackle, especially in light of the fact that menu selection drawing was not intended for menu items with custom views, and menu selection drawing (colors, etc.) may change in the future. This is why we ask you to file bug reports so that menu selection will be honored with custom views, if asked for, so that future changes to OS X won’t require developers to continually maintain their code to match future color appearances.
The “Application Menu and Pop-up List Programming Topics” says this:
Views in Menu Items -
“A menu item with a view does not draw its title, state, font, or other standard drawing attributes, and assigns drawing responsibility entirely to the view. Keyboard equivalents and type-select continue to use the key equivalent and title as normal.”
Since all drawing is up to the developer, custom views in menu items aren’t necessarily supposed to draw “selected”.
The APIs to obtain the right selection color is obviously not doing what it’s supposed to, hence the request to file a bug report. I wish we could offer more concrete solutions to the problem but a workaround offered today may not hold up tomorrow and we don’t want to set a bad precedent on workarounds that are risky. Apple apps have access to lower level private APIs that achieve their results. We cannot offer you these solutions as they are private.
If selectedMenuItemColor() does not match the menu highlight color with Vibrant light and dark, that’s a bug to be filed and to be fixed.
Lastly, Apple recommends to use NSMenuItem’s APIs as much as possible to achieve what you want in menus. The screenshots you included can likely be done without applying custom views.
I think I have less "hackish" solution. It's indeed caused by the new NSVisualEffectView and Vibrancy stuff in Yosemite. I learned that there are quite complex rules how views are drawn when they're subviews of NSVisualEffectView. It was discussed on WWDC 2014 in session 220 - Adopting Advanced Features of the New UI of OS X Yosemite. I recommend you to watch this session video to get comprehensive explanation.
Shortly, it seems that your problem may be caused by colors you use. There are two new system colors - [NSColor labelColor]
and [NSColor secondaryLabelColor]
. These two are automatically adjusted when drawn inside NSVisualEffectView. Also, your custom view should support Vibrancy effect. This is done by overriding - (BOOL)allowsVibrancy
method and returning YES.
Please check the session video mentioned above or download session slides in PDF to get precise information. This stuff is discussed from slide 124 in PDF and near the middle of the video.