9
votes

I have an NSMenu popping out of an NSStatusItem using popUpStatusItemMenu. These NSMenuItems show a bunch of different links, and each one is connected with setAction: to the openLink: method of a target. This arrangement has been working fine for a long time. The user chooses a link from the menu and the openLink: method then deals with it.

Unfortunately, I recently decided to experiment with using NSMenuItem's setView: method to provide a nicer/slicker interface. Basically, I just stopped setting the title, created the NSMenuItem, and then used setView: to display a custom view. This works perfectly, the menu items look great and my custom view is displayed.

However, when the user chooses a menu item and releases the mouse, the action no longer works (i.e., openLink: isn't called). If I just simply comment out the setView: call, then the actions work again (of course, the menu items are blank, but the action is executed properly). My first question, then, is why setting a view breaks the NSMenuItem's action.

No problem, I thought, I'll fix it by detecting the mouseUp event in my custom view and calling my action method from there. I added this method to my custom view:

- (void)mouseUp:(NSEvent *)theEvent {
  NSLog(@"in mouseUp");
  }

No dice! This method is never called.

I can set tracking rects and receive mouseEntered: events, though. I put a few tests in my mouseEntered routine, as follows:

if ([[self window] ignoresMouseEvents]) {  NSLog(@"ignoring mouse events");  }
else {  NSLog(@"not ignoring mouse events");  }
if ([[self window] canBecomeKeyWindow]) {  dNSLog((@"canBecomeKeyWindow"));  }
else {  NSLog(@"not canBecomeKeyWindow");  }
if ([[self window] isKeyWindow]) {  dNSLog((@"isKeyWindow"));  }
else {  NSLog(@"not isKeyWindow");  }

And got the following responses:

not ignoring mouse events
canBecomeKeyWindow
not isKeyWindow

Is this the problem? "not isKeyWindow"? Presumably this isn't good because Apple's docs say "If the user clicks a view that isn’t in the key window, by default the window is brought forward and made key, but the mouse event is not dispatched." But there must be a way do detect these events. HOW?

Adding:

[[self window] makeKeyWindow];

has no effect, despite the fact that canBecomeKeyWindow is YES.

7
There is more information about this problem at open radar: openradar.appspot.com/7128269 There is also a claimed workaround, but I can't make the workaround work. Basically the window just refuses to become key, even though it reports canBecomeKey.Dennis
Hi, I've faced the same problem, do you have any progress with it? I've tried some weired ways to solve it, I've found one workaround but its terrible ugly, and after showing the menu status bar item is not highlighted, but menu is shown and fully functional. I've set menu for status item to nil and manually control menu popup. If application is active I just call menu popup method, if no I send make application active message, and in callback I'm opening menu. Here is some code. Again its terrible and ugly. gist.github.com/224275iafonov
I have to say that iafonov's code is GREAT! Though one year before!!! Though the code seems irrelevant. Thanks! It resolves all the problems about NSVIew, NSTextEdit in NSMenuItem. At first, the NSTextEdit(in NSMenuItem) does not work normally, cannot receive key press and mouse does not change, Now everything works wonderful!!! No acceptsFirstResponder, No self window makeFirstResponder:xxx, Just iafonov'code. It is the way to pop up menu that matters. gist.github.com/224275user377808

7 Answers

15
votes

Add this method to your custom NSView and it will work fine with mouse events

- (void)mouseUp:(NSEvent*) event {
    NSMenuItem* mitem = [self enclosingMenuItem];
    NSMenu* m = [mitem menu];
    [m cancelTracking];
    [m performActionForItemAtIndex: [m indexOfItem: mitem]];
}

But i'm having problems with keyhandling, if you solved this problem maybe you can go to my question and help me a little bit.

10
votes

Add this to your custom view and you should be fine:

- (BOOL)acceptsFirstMouse:(NSEvent *)theEvent
{
    return YES;
}
9
votes

I added this method to my custom view, and now everything works beautifully:

- (void)viewDidMoveToWindow {
    [[self window] becomeKeyWindow];
}

Hope this helps!

2
votes

So far, the only way to achieve the goal, is to register a tracking area manually in updateTrackingAreas - that is thankfully called, like this:

override func updateTrackingAreas() {
    let trackingArea = NSTrackingArea(rect: bounds, options: [.enabledDuringMouseDrag, .mouseEnteredAndExited, .activeInActiveApp], owner: self, userInfo: nil)
    addTrackingArea(trackingArea)
}
2
votes

I've updated this version for SwiftUI Swift 5.3:

final class HostingView<Content: View>: NSHostingView<Content> {
    override func viewDidMoveToWindow() {
        window?.becomeKey()
    }
}

And then use like so:

let item = NSMenuItem()
let contentView = ContentView()
item.view = HostingView(rootView: contentView)

let menu = NSMenu()
menu.items = [item]
-1
votes

Recently I needed to show a Custom view for a NSStatusItem, show a regular NSMenu when clicking on it and supporting drag and drop operations on the Status icon.

I solved my problem using, mainly, three different sources that can be found in this question.

Hope it helps other people.

-1
votes

See the sample code from Apple named CustomMenus In there you'll find a good example in the ImagePickerMenuItemView class.

It's not simple or trivial to make a view in a menu act like a normal NSMenuItem. There are some real decisions and coding to do.