Customize NSToolbar context menu on Sonoma

Is there a way to customize the context menu that pops up when right clicking on a toolbar (or the title bar of the window containing it)? As of Sonoma, the steps that worked (described here: https://developer.apple.com/forums/thread/21887) up to Ventura no longer work. I'm developing in Objective-C.

Is the allowsUserCustomization property set to YES on the toolbar?

self.myNSToolbar.allowsUserCustomization = YES;
self.myNSToolbar.autosavesConfiguration = YES; //<--Most likely want this to YES too.

If you are using Interface Builder make sure the checkboxes corresponding to these properties are checked.

Accepted Answer

Is the allowsUserCustomization property set to YES on the toolbar?

self.myNSToolbar.allowsUserCustomization = YES;
self.myNSToolbar.autosavesConfiguration = YES; //<--Most likely want this to YES too.

If you are using Interface Builder make sure the checkboxes corresponding to these properties are checked.

I see. I don't think there is a good way to do this using public API. I could think of a way to hack it but the implementation would be fragile. Unless customizing this menu is really important to your app I wouldn't do it.

It is important. I'm fully aware the way we do it is a hack and it was not that unexpected to see it stop working at some point...

It is important. I'm fully aware the way we do it is a hack and it was not that unexpected to see it stop working at some point...

Then instead of doing this:

NSView * theContentView = myWindow.contentView; 
NSMenu * theCustomizeMenu = theContentView.superview.menu;

You can try can try enumerating subviews from the content view superview until you find it. Ideally you should isolate this hack in a category because the code is really nasty. Something like this:

//Public
@interface NSWindow (FindCustomizeToolbarMenu)

-(nullable NSMenu*)findToolbarContextMenu;

@end

**//Put all the in the .m**
#import "NSWindow+FindCustomizeToolbarMenu.h"
#import <objc/runtime.h>

@interface NSMenu (NSToolbarMenuMarkerHack)

@property (nonatomic) BOOL isCustomizeToolbarMenu;

@end

@implementation NSMenu (NSToolbarMenuMarkerHack)

static char const * const ToolbarMenuHackKey = "ToolbarMenuHackKey";

-(BOOL)isCustomizeToolbarMenu
{
    NSNumber *result = objc_getAssociatedObject(self, ToolbarMenuHackKey);
    return (result != nil) ? result.boolValue : NO;
}

-(void)setIsCustomizeToolbarMenu:(BOOL)isCustomizeToolbarMenu
{
    objc_setAssociatedObject(self,
                             ToolbarMenuHackKey,
                             @(isCustomizeToolbarMenu),
                             OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

@end

@implementation NSWindow (FindCustomizeToolbarMenu)

-(NSMenu*)searchForCustomizeToolbarMenuInSubviews:(NSArray<NSView*>*)subviews
{
    if (subviews.count == 0) { return nil; }
    
    NSMenu *foundMenu = nil;
    
    for (NSView *aSubview in subviews)
    {
        NSMenu *theMenu = aSubview.menu;
        if (theMenu.isCustomizeToolbarMenu
            || [theMenu.itemArray.lastObject.title isEqualToString:@"Customize Toolbar…"]) // <--Should compare a localized string! If you append on the found menu later this check will fail because "Customize Toolbar…" will no longer be the last object!
        {
            foundMenu.isCustomizeToolbarMenu = YES; //Mark it in case you mutate the menu later and "Customize Toolbar…" is not the last menu item.
            foundMenu = theMenu;
            break;
        }
    }
    
    //Found it?
    if (foundMenu != nil)
    {
        return foundMenu;
    }
    else
    {
        //Didn't find it. Dig through descendant views.
        for (NSView *aSubviewToDigIn in subviews)
        {
            NSMenu *nested = [self searchForCustomizeToolbarMenuInSubviews:aSubviewToDigIn.subviews];
            if (nested != nil)
            {
                foundMenu = nested;
                break;
            }
        }
        return foundMenu;
    }
}

-(NSMenu*)findToolbarContextMenu
{
    NSView *theContentView = self.contentView;
    NSView *contentViewSuperview = theContentView.superview;
    NSMutableArray *theSuperViewSubviews = [contentViewSuperview.subviews mutableCopy];
    [theSuperViewSubviews removeObject:theContentView]; //Don't need to search the content view since we know it doesn't have it.
 
    NSMenu *foundMenu = [self searchForCustomizeToolbarMenuInSubviews:theSuperViewSubviews];
    return foundMenu;
}

This is very ugly and fragile and untested so inspect the code and use at your own risk. But in theory you should be able to do something like this using the category:

NSMenu *toolbarMenu = [self.window findToolbarContextMenu];
[toolbarMenu addItemWithTitle:@"CUSTOM" action:nil keyEquivalent:@""];

You could try filing a Feedback and ask Apple to improve the API but I have my doubts about that. You'd at least have to wait a year for them to add it and there's a good chance they never will.

Customize NSToolbar context menu on Sonoma
 
 
Q