19

I have created a simple NSStatusBar with a NSMenu set as the menu. I have also added a few NSMenuItems to this menu, which work fine (including selectors and highlighting) but as soon as I add a custom view (setView:) no highlighting occurs.

CustomMenuItem *menuItem = [[CustomMenuItem alloc] initWithTitle:@"" action:@selector(openPreferences:) keyEquivalent:@""];
[menuItem foo];
[menuItem setTarget:self];
[statusMenu insertItem:menuItem atIndex:0];
[menuItem release];

And my foo method is:

- (void)foo {
  NSView *view = [[NSView alloc] initWithFrame:CGRectMake(5, 10, 100, 20)];
  [self setView:view];
}

If I remove the setView method, it will highlight.

I have searched and searched and cannot find a way of implementing/enabling this.

Edit

I implemented highlight by following the code in this question in my NSView SubClass:

An NSMenuItem's view (instance of an NSView subclass) isn't highlighting on hover

#define menuItem ([self enclosingMenuItem])

- (void) drawRect: (NSRect) rect {
    BOOL isHighlighted = [menuItem isHighlighted];
    if (isHighlighted) {
        [[NSColor selectedMenuItemColor] set];
        [NSBezierPath fillRect:rect];
    } else {
        [super drawRect: rect];
    }
}
Community
  • 1
  • 1
rdougan
  • 7,217
  • 2
  • 34
  • 63
  • 3
    That is one useless `#define`. – IluTov Feb 04 '14 at 12:21
  • possible duplicate of [An NSMenuItem's view (instance of an NSView subclass) isn't highlighting on hover](http://stackoverflow.com/questions/2917713/an-nsmenuitems-view-instance-of-an-nsview-subclass-isnt-highlighting-on-hove) – nschum Oct 25 '14 at 13:53
  • Even more difficult when Vibrancy comes into play: https://stackoverflow.com/questions/26851306/ – pkamb Aug 03 '18 at 19:23

4 Answers4

10

Here's a rather less long-winded version of the above. It's worked well for me. (backgroundColour is an ivar.)

- (void)drawRect:(NSRect)rect
{
    if ([[self enclosingMenuItem] isHighlighted]) {
        [[NSColor selectedMenuItemColor] set];
    } else if (backgroundColour) {
        [backgroundColour set];
    }
    NSRectFill(rect);
}
pkamb
  • 33,281
  • 23
  • 160
  • 191
  • Note that if your drawRect: isn't getting called as you move the mouse around, make sure your menu item is actually enabled. It needs an action set to a selector that exists. – robotspacer Sep 25 '16 at 10:32
9

Update for 2019:

class CustomMenuItemView: NSView {
    private var effectView: NSVisualEffectView

    override init(frame: NSRect) {
        effectView = NSVisualEffectView()
        effectView.state = .active
        effectView.material = .selection
        effectView.isEmphasized = true
        effectView.blendingMode = .behindWindow

        super.init(frame: frame)
        addSubview(effectView)
        effectView.frame = bounds
    }

    required init?(coder decoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    override func draw(_ dirtyRect: NSRect) {
        effectView.isHidden = !(enclosingMenuItem?.isHighlighted ?? false)
    }
}

Set one of those to your menuItem.view.

(Credit belongs to Sam Soffes who helped me figure this out and sent me almost that code verbatim.)

Andrew
  • 4,145
  • 2
  • 37
  • 42
  • 1
    does not work with macOS 11.0 Beta 3 (20A5323l). Anyone found a workaround yet or at least the reason why it isn't working anymore? Looks like draw() isn't being called. – Daniel Aug 01 '20 at 12:24
  • https://stackoverflow.com/questions/63775522/nsmenuitem-with-custom-view-in-macos-11-big-sur P.S.: I ended up manually recreating the new selection style – Vitalii Vashchenko Sep 19 '20 at 19:04
  • You should override a computed property allowsVibrancy and set it to 'false'. Otherwise this code wouldn't produce the exact result. – Vitalii Vashchenko Nov 04 '20 at 14:22
  • For this to work you need to add an action, or the item will be considered disabled. Also, it's good to set autolayout or add autorizingMask to both CustomMenuItemView and effectView to take up the entire space of the item. It is not necessary to override the allowsVibrancy property (at least on macOS Big Sur). – ericmaciel Sep 04 '21 at 17:30
6

If you're adding a view to a menu item, that view has to draw the highlight itself. You don't get that for free, I'm afraid. From the Menu Programming Topics:

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.

jscs
  • 63,694
  • 13
  • 151
  • 195
3

Yes, as mentioned earlier you must draw it yourself. I use AppKit's NSDrawThreePartImage(…) to draw, and also include checks to use the user's control appearance (blue or graphite.) To get the images, I just took them from a screenshot (if anyone knows a better way, please add a comment.) Here's a piece of my MenuItemView's drawRect:

    // draw the highlight gradient
if ([[self menuItem] isHighlighted]) {

    NSInteger tint = [[NSUserDefaults standardUserDefaults] integerForKey:@"AppleAquaColorVariant"];
    NSImage *image = (AppleAquaColorGraphite == tint) ? menuItemFillGray : menuItemFillBlue;

    NSDrawThreePartImage(dirtyRect, nil, image, nil, NO,
        NSCompositeSourceOver, 1.0, [self isFlipped]);
}
else if ([self backgroundColor]) {

    [[self backgroundColor] set];
    NSRectFill(dirtyRect);
}

EDIT

Should have defined these:

enum AppleAquaColorVariant {
    AppleAquaColorBlue = 1,
    AppleAquaColorGraphite = 6,
};

These correspond to the two appearance options in System Preferences. Also, menuItemFillGray & menuItemFillBlue are just NSImages of the standard menu item fill gradients.

IluTov
  • 6,807
  • 6
  • 41
  • 103
Francis McGrew
  • 7,264
  • 1
  • 33
  • 30
  • Could you please explain what AppleAquaColorGraphite or menuItemFillGray is? Maybe post the whole drawRect method? ;-) – tamasgal May 20 '11 at 23:13
  • I wrote some code to draw the highlighted background using selectedMenuItemColor and a gradient. See [my answer to Exactly matching the background of a selected NSMenuItem](http://stackoverflow.com/a/25984748/196844). – Daniel Trebbien Sep 22 '14 at 23:57