From 3e58c6cb3adc2db649bf71e5347a00f73d6fa72d Mon Sep 17 00:00:00 2001 From: Shelley Vohr Date: Fri, 20 Feb 2026 17:35:44 +0100 Subject: [PATCH] fix: refresh menu item state on key equivalent dispatch (#49875) --- .../ui/cocoa/electron_menu_controller.mm | 38 +++++++++++-------- 1 file changed, 22 insertions(+), 16 deletions(-) diff --git a/shell/browser/ui/cocoa/electron_menu_controller.mm b/shell/browser/ui/cocoa/electron_menu_controller.mm index 81bfad9463..ea32970ffb 100644 --- a/shell/browser/ui/cocoa/electron_menu_controller.mm +++ b/shell/browser/ui/cocoa/electron_menu_controller.mm @@ -258,7 +258,7 @@ NSArray* ConvertSharingItemToNS(const SharingItem& item) { // Empties the source menu items to the destination. - (void)moveMenuItems:(NSMenu*)source to:(NSMenu*)destination { - const NSInteger count = [source numberOfItems]; + const NSInteger count = source.numberOfItems; for (NSInteger index = 0; index < count; index++) { NSMenuItem* removedItem = [source itemAtIndex:0]; [source removeItemAtIndex:0]; @@ -269,25 +269,25 @@ NSArray* ConvertSharingItemToNS(const SharingItem& item) { // Replaces the item's submenu instance with the singleton recent documents // menu. Previously replaced menu items will be recovered. - (void)replaceSubmenuShowingRecentDocuments:(NSMenuItem*)item { - NSMenu* recentDocumentsMenu = [recentDocumentsMenuItem_ submenu]; + NSMenu* recentDocumentsMenu = recentDocumentsMenuItem_.submenu; // Remove menu items in recent documents back to swap menu [self moveMenuItems:recentDocumentsMenu to:recentDocumentsMenuSwap_]; // Swap back the submenu - [recentDocumentsMenuItem_ setSubmenu:recentDocumentsMenuSwap_]; + recentDocumentsMenuItem_.submenu = recentDocumentsMenuSwap_; // Retain the item's submenu for a future recovery - recentDocumentsMenuSwap_ = [item submenu]; + recentDocumentsMenuSwap_ = item.submenu; // Repopulate with items from the submenu to be replaced [self moveMenuItems:recentDocumentsMenuSwap_ to:recentDocumentsMenu]; // Update the submenu's title - [recentDocumentsMenu setTitle:[recentDocumentsMenuSwap_ title]]; + recentDocumentsMenu.title = recentDocumentsMenuSwap_.title; // Replace submenu - [item setSubmenu:recentDocumentsMenu]; + item.submenu = recentDocumentsMenu; - DCHECK_EQ([item action], @selector(submenuAction:)); - DCHECK_EQ([item target], recentDocumentsMenu); + DCHECK_EQ(item.action, @selector(submenuAction:)); + DCHECK_EQ(item.target, recentDocumentsMenu); // Remember the new menu item that carries the recent documents menu recentDocumentsMenuItem_ = item; @@ -303,7 +303,7 @@ NSArray* ConvertSharingItemToNS(const SharingItem& item) { NSArray* services = [NSSharingService sharingServicesForItems:items]; for (NSSharingService* service in services) [menu addItem:[self menuItemForService:service withItems:items]]; - [menu setDelegate:self]; + menu.delegate = self; return menu; } @@ -313,9 +313,9 @@ NSArray* ConvertSharingItemToNS(const SharingItem& item) { NSMenuItem* item = [[NSMenuItem alloc] initWithTitle:service.menuItemTitle action:@selector(performShare:) keyEquivalent:@""]; - [item setTarget:self]; - [item setImage:service.image]; - [item setRepresentedObject:@{@"service" : service, @"items" : items}]; + item.target = self; + item.image = service.image; + item.representedObject = @{@"service" : service, @"items" : items}; return item; } @@ -398,7 +398,7 @@ NSArray* ConvertSharingItemToNS(const SharingItem& item) { submenu.delegate = self; // Set submenu's role. - if ((role == u"window" || role == u"windowmenu") && [submenu numberOfItems]) + if ((role == u"window" || role == u"windowmenu") && submenu.numberOfItems) [NSApp setWindowsMenu:submenu]; else if (role == u"help") [NSApp setHelpMenu:submenu]; @@ -463,6 +463,14 @@ NSArray* ConvertSharingItemToNS(const SharingItem& item) { return item; } +// Called by AppKit before displaying a menu and when a key equivalent is +// pressed. This ensures menu item states (enabled, checked, hidden) are +// refreshed from the model even when the menu is closed, which is necessary +// since we set autoenablesItems = NO. +- (void)menuNeedsUpdate:(NSMenu*)menu { + [self refreshMenuTree:menu]; +} + - (void)applyStateToMenuItem:(NSMenuItem*)item { id represented = item.representedObject; if (!represented) @@ -487,7 +495,7 @@ NSArray* ConvertSharingItemToNS(const SharingItem& item) { // When the menu is closed, we need to allow shortcuts to be triggered even // if the menu item is disabled. So we only disable the menu item when the // menu is open. This matches behavior of |validateUserInterfaceItem|. - item.enabled = model->IsEnabledAt(index) || !isMenuOpen_; + item.enabled = model->IsEnabledAt(index); item.hidden = !model->IsVisibleAt(index); item.state = model->IsItemCheckedAt(index) ? NSControlStateValueOn : NSControlStateValueOff; @@ -566,7 +574,6 @@ NSArray* ConvertSharingItemToNS(const SharingItem& item) { [menu removeItem:item]; } - [self refreshMenuTree:menu]; if (model_) model_->MenuWillShow(); } @@ -577,7 +584,6 @@ NSArray* ConvertSharingItemToNS(const SharingItem& item) { return; isMenuOpen_ = NO; - [self refreshMenuTree:menu]; // There are two scenarios where we should emit menu-did-close: // 1. It's a popup and the top level menu is closed.