Trouble matching the vibrant background of a Yosemite NSMenuItem containing a custom view

3.5k Views Asked by At

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”.

The problem is that the background NSView is given Yosemite-style vibrancy/transparency when added to the menu. The NSTextfield label is not.

Background colors do not match

Through the use of NSRectFillUsingOperation I've gotten this to look good for some background colors in Yosemite. But others continue to not match. When it is working, after manually "highlighting" the view, the original colors change and no longer match. I can dig up some example code for this if needed.

Then, when it is looking somewhat good in Yosemite, it looks terrible in 10.9 Mavericks.

I've also tried setting the wantsLayer property to YES to turn the view into a CALayer-backed view. This creates other issues such as text not anti-aliasing correctly against a clear background.

My Question:

How do I display a label on top of a NSMenuItem custom view? The label's background must exactly match the view's background. Solution must work in Yosemite and Mavericks.

Example code below:

self.statusItem = [[NSStatusBar systemStatusBar]
statusItemWithLength:NSVariableStatusItemLength];
[self.statusItem setTitle:@"TEST"];
[self.statusItem setHighlightMode:YES];
[self.statusItem setEnabled:YES];
[self.statusItem setTarget:self];

NSMenu *menu = [[NSMenu alloc] init];
[menu addItemWithTitle:@"Disabled menu item" action:nil keyEquivalent:@""];
[menu addItemWithTitle:@"Enabled menu item" action:@selector(enabled) keyEquivalent:@""];

NSTextField *label = [[NSTextField alloc] initWithFrame:NSMakeRect(30, 20, 50, 20)];
label.stringValue = @"label";
label.editable = NO;
label.bordered = NO;
label.backgroundColor = [NSColor blueColor];
//label.backgroundColor = [NSColor clearColor];


PKMenuItemView *view = [[PKMenuItemView alloc] initWithFrame:NSMakeRect(0, 0, 200, 50)];
[view addSubview:label];

NSMenuItem *viewMenuItem = [[NSMenuItem alloc] init];
[viewMenuItem setView:view];
[menu addItem:viewMenuItem];

self.statusItem.menu = menu;

I've subclassed the NSView to override drawRect: and draw a colored background:

- (void)drawRect:(NSRect)dirtyRect {
    [super drawRect:dirtyRect];

    [[NSColor blueColor] setFill];
    NSRectFill(dirtyRect);
    //NSRectFillUsingOperation(dirtyRect, NSCompositeSourceOver);
}
7

There are 7 best solutions below

5
On

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.

Using an NSImageView

0
On

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.

0
On

Response from a Apple Developer Technical Support ticket I opened in 2015:

Re: DTS Auto-Ack - Vibrant background and highlighting of Custom View NSMenuItems

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.

4
On

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.

5
On

I've just discovered that +[NSColor keyboardFocusIndicatorColor] is the right color (on El Capitan at least), whereas the expected selectedMenuItemColor is by far too dark.

0
On

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

NSMenuItemView with <code>_viewHandlesEvents</code> set to false

There are some other references to _viewHandlesEvents on the internet:

0
On

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

Vibrant Window Background