11

The contents of a NSPopupButton are bound to an NSArray of strings.

How can we insert a separator item via bindings?

The "-" strings (like in the olden/Classic days) doesn't work, i.e. shows up literally as a "-" menu item.

Is there any out-of-the-box solution with standard Cocoa classes and bindings?

This should be a trivial problem but I can't find any solution to the problem that doesn't involve silly hacks like subclassing NSMenu, NSPopupButton or other non-intuitive work arounds.

ATV
  • 4,116
  • 3
  • 23
  • 42
  • Nope - went back to a lame outlet + manually build menu approach. Still interested, though. If deadlines are less of an issue I'll try something involving value transformers.. – ATV Oct 18 '14 at 09:04

5 Answers5

11

I couldn't find a clean way to dynamically add separators to a menu when using bindings. The easiest (and most reusable) way I've found is to use an NSMenuDelegate to dynamically swap out NSMenuItems with a specific title like @"---" with separator items in the menuNeedsUpdate: delegate method.

Step 1: Create an NSObject that conforms to NSMenuDelegate protocol

#import <Cocoa/Cocoa.h>

@interface SeparatorMenuDelegate : NSObject <NSMenuDelegate>
@end
@implementation SeparatorMenuDelegate

-(void)menuNeedsUpdate:(NSMenu *)menu {
    NSArray* fakeSeparators = [[menu itemArray] filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:@"title == '---'"]];
    for (NSMenuItem* fakeSep in fakeSeparators) {
        [menu insertItem:[NSMenuItem separatorItem] atIndex:[menu indexOfItem:fakeSep]];
        [menu removeItem:fakeSep];
    }
}

@end

Step 2: Link things up in Interface Builder.

Drag out an Object into the scene that contains the NSPopupButton instance. Drag out an object

Set the object's class to SeparatorMenuDelegate

Set the object's class

Twirl open the NSPopupButton control in the Document Outline and select the Menu inside it. Then set the delegate for the Menu to the SeparatorMenuDelegate object that you dragged in earlier.

Set the menu's delegate

After this, all items in the menu with a title of @"---" will be converted to separator items.

If you have multiple NSPopupButton instances in the same scene, you can set the delegate of their Menu to the same object (you only need one SeparatorMenuDelegate per scene).

n.Drake
  • 2,585
  • 2
  • 15
  • 8
  • Works great (and extremely well explained). This should be accepted answer. Very helpful, because my 15-year old solution, subclassing NSMenu and overriding `addItemWithTitle`, no longer works. – matt Apr 29 '17 at 23:41
  • .. I suppose that's the best we can do with the current macOS API. Cheers! – ATV Jun 06 '17 at 05:43
  • @matt Why do you think your “15-year old solution” doesn’t work anymore? Subclassing **NSMenu** still works just fine on High Sierra, and from my POV, this isn’t a “silly hack”, but exactly what subclassing is for, and therefore the cleanest solution. Just remember to not only overwrite `addItemWithTitle` but also `insertItemWithTitle` … – Uli Zappe Oct 19 '17 at 23:29
  • @UliZappe I never said it was a silly hack. And may I suggest that if you have a solution that uses it, you give that as an answer? – matt Oct 19 '17 at 23:34
  • @matt “silly hack” was an expression the thread starter used in his question; I should have attributed this explicitly, sorry. As for the solution, well, it’s _your_ solution as outlined on e.g. http://www.cocoabuilder.com/archive/cocoa/275031-nspopupbutton-bindings-separator-items.html. Of course I could repeat that, but does that really make sense? – Uli Zappe Oct 19 '17 at 23:44
  • @UliZappe Well, cocoabuilder isn't Stack Overflow, and you've added something in your comment that I never knew. And comments die, but answer do not. I think you'd be doing the world a service by showing a solution that demonstrates how subclassing NSMenu _can_ work after all! – matt Oct 20 '17 at 00:27
  • @matt OK, if you don’t feel I’m stealing from you, I’ll write an answer to that effect, tonight or tomorrow. – Uli Zappe Oct 20 '17 at 00:36
  • @UliZappe Sounds great. You can credit my cocoabuilder answer if you want, but as an answer on Stack Overflow, I don't think this would be stealing at all. – matt Oct 20 '17 at 00:38
5

IMHO, the cleanest solution is still subclassing NSMenu – this kind of customization is exactly what subclassing is for. The following solution is based on what @matt wrote many years ago on Cocoabuilder and is updated to be more universally applicable, including on High Sierra.

First, define a “magic string” to represent the separator item in your code; do this in a header file that all affected classes will import. In this example, I’ve chosen “---”, but of course, this can be any string you like:

#define MY_MENU_SEPARATOR @"---"

Second, subclass NSMenu and overwrite the two methods that add menu items, in order to handle the special separator case:

@implementation MyMenu

- (NSMenuItem*)addItemWithTitle:(NSString*)aString action:(SEL)aSelector keyEquivalent:(NSString*)keyEquiv
    {
        if ([aString isEqualToString:MY_MENU_SEPARATOR])
        {
            NSMenuItem *separator = [NSMenuItem separatorItem];
            [self addItem:separator];
            return separator;
        }
        return [super addItemWithTitle:aString action:aSelector keyEquivalent:keyEquiv];
    }


- (NSMenuItem*)insertItemWithTitle:(NSString*)aString action:(SEL)aSelector keyEquivalent:(NSString*)keyEquiv atIndex:(NSInteger)index
    {
        if ([aString isEqualToString:MY_MENU_SEPARATOR])
        {
            NSMenuItem *separator = [NSMenuItem separatorItem];
            [self insertItem:separator atIndex:index];
            return separator;
        }
        return [super insertItemWithTitle:aString action:aSelector keyEquivalent:keyEquiv atIndex:index];
    }   

@end

And that’s it. Set the affected menus to be of the MyMenu class in the Identity inspector of Interface Builder, and they will insert separator items where desired. Works for menu bar menus as well as for pop-ups.

Uli Zappe
  • 306
  • 3
  • 8
  • @ATV It’s the _code for making a separator aware subclass of **NSMenu**_. As such, it is tiny – you’ll do it once in your lifetime. Then, the code for adding a separator – _which is something different_ – is simply adding `MY_MENU_SEPARATOR` where you need it. That doesn’t get any easier. – Uli Zappe Oct 23 '17 at 00:03
4

Here's n.Drake's answer in Swift 3.1:

class MySeparatorMenuDelegate : NSObject, NSMenuDelegate {
    func menuNeedsUpdate(_ menu: NSMenu) {
        for (ix,mi) in menu.items.enumerated() {
            if mi.title == "---" {
                menu.removeItem(at: ix)
                menu.insertItem(NSMenuItem.separator(), at: ix)
            }
        }
    }
}
matt
  • 515,959
  • 87
  • 875
  • 1,141
1

Take a look at the documentation for NSContentPlacementTagBindingOption, added in Mac OS X 10.5. In Interface Builder's Bindings inspector, this is available for elements such as pop-up menu buttons; go to the "Value Selection" section and look in any Content category (Content, Content Objects, Content Values), for "Content Placement Tag". The field value should be a number that matches some menu item's tag number.

The contents of the bound array will be inserted into the menu in place of whichever item has the indicated tag value. In this case, the static menu would contain your separator item and at least one other item to specify where the array values go.

Kevin Grant
  • 5,363
  • 1
  • 21
  • 24
  • 1
    Well, I know about that option. Doesn't help though if you have a generic list with more than one separator like *Item A*, Separator, *Item B*, Separator, *Item C*, ... – ATV Mar 29 '15 at 13:50
-2

[[popupButton menu] addItem:[NSMenuItem separatorItem]];

Borongaj
  • 451
  • 4
  • 16