Popup menu implementation from NSButton

As I have commented I find the ButtonMadness example less than perfect. My implementation seems to work better. The menu is shown on mouse down, the button remains depressed throughout, the menu position can be specified and the menu is dismissed without subsequent spurious display.

To be honest NSPopupButton is a better choice in the majority of situations. I use this code mainly because of the convenience of having one class for buttons and popups and because the menu does not contain the popup control image and title. I load the menu from a separate nib and reuse it as is elsewhere in the app as required.

Note that it is trivial to add additional support for say a popover as well as menu.

NSButton subclass:

- (void)mouseDown:(NSEvent *)theEvent {

    // if a menu is defined let the cell handle its display
    if (self.menu) {
        if ([theEvent type] == NSLeftMouseDown) {
            [[self cell] setMenu:[self menu]];
        } else {
            [[self cell] setMenu:nil];
        }
    }

    [super mouseDown:theEvent];
}

NSButtonCell subclass:

- (BOOL)trackMouse:(NSEvent *)event inRect:(NSRect)cellFrame ofView:(NSView *)controlView untilMouseUp:(BOOL)untilMouseUp
{
    // if menu defined show on left mouse
    if ([event type] == NSLeftMouseDown && [self menu]) {

        NSPoint result = [controlView convertPoint:NSMakePoint(NSMidX(cellFrame), NSMidY(cellFrame)) toView:nil];

        NSEvent *newEvent = [NSEvent mouseEventWithType: [event type]
                                               location: result
                                          modifierFlags: [event modifierFlags]
                                              timestamp: [event timestamp]
                                           windowNumber: [event windowNumber]
                                                context: [event context]
                                            eventNumber: [event eventNumber]
                                             clickCount: [event clickCount]
                                               pressure: [event pressure]];

        // need to generate a new event otherwise selection of button
        // after menu display fails
        [NSMenu popUpContextMenu:[self menu] withEvent:newEvent forView:controlView];

        return YES;
    }

    return [super trackMouse:event inRect:cellFrame ofView:controlView untilMouseUp:untilMouseUp];
}

Yup.

On button action call

[NSMenu popUpContextMenu:menu withEvent:event forView:(NSButton *)sender];

where

  • menu : menu you want to show
  • sender : button you clicked
  • event : a new NSEvent you create

When you create the new NSEvent, specify the location as to where you want the popup menu to be shown.


Swift version of accepted answer

@IBAction func actionOccurred(sender: NSButton) {
    if let event = NSApplication.sharedApplication().currentEvent {
        NSMenu.popUpContextMenu(sender.menu!, withEvent: event, forView: sender)
    }
}

Updated answer

Add NSMenu to the NSViewController in the storyboard as seen in the image

Image

Connect the Outlet

Right-click on the button in the storyboard and connect the menu outlet to the menu that we have just added to the storyboard(Where arrow points in the image)

jj

Add IBAction

@IBAction func menuClicked(_ sender: NSButton) {
    var location = NSEvent.mouseLocation
    location.x -= 10; location.y -= 10 // Menu appears below the button
    let menu = sender.menu!
    menu.popUp(positioning: menu.item(at: 0), at: location, in: nil)
}