Repository: electron/electron
Pull request: fix: refresh menu item state on key equivalent dispatch

#### Description of Change

Closes https://github.com/electron/electron/issues/49841
Refs https://github.com/electron/electron/issues/49549
Refs https://github.com/electron/electron/pull/48598

With `autoenablesItems = NO`, macOS doesn't re-validate menu items while the menu is closed. Previously this was worked around by forcing `item.enabled = YES` when closed `|| !isMenuOpen_`, which meant shortcuts could fire even for intentionally disabled items.

This replaces that workaround with `menuNeedsUpdate:`, which AppKit calls both before displaying a menu and during key equivalent matching — even while closed. This ensures item state is always synced from the model before AppKit decides whether to dispatch.

Also removes the now-redundant `refreshMenuTree:` calls in `menuWillOpen:`/`menuDidClose:`.

#### Checklist
<!-- Remove items that do not apply. For completed items, change [ ] to [x]. -->

- [x] PR description included
- [x] I have built and tested this PR
- [x] `npm test` passes
- [x] [PR release notes](https://github.com/electron/clerk/blob/main/README.md) describe the change in a way relevant to app developers, and are [capitalized, punctuated, and past tense](https://github.com/electron/clerk/blob/main/README.md#examples).

#### Release Notes

Notes: Fixed an issue where menu item enabled state wasn't updated during key equivalent dispatch when the menu was closed on macOS.

Changed files:
filename: shell/browser/ui/cocoa/electron_menu_controller.mm
patch: @@ -258,7 +258,7 @@ - (void)addSeparatorToMenu:(NSMenu*)menu atIndex:(int)index {
 
 // 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 @@ - (void)moveMenuItems:(NSMenu*)source to:(NSMenu*)destination {
 // 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 @@ - (NSMenu*)createShareMenuForItem:(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 @@ - (NSMenuItem*)menuItemForService:(NSSharingService*)service
   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 @@ - (NSMenuItem*)makeMenuItemForIndex:(NSInteger)index
     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 @@ - (NSMenuItem*)makeMenuItemForIndex:(NSInteger)index
   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 @@ - (void)applyStateToMenuItem:(NSMenuItem*)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 @@ - (void)menuWillOpen:(NSMenu*)menu {
       [menu removeItem:item];
   }
 
-  [self refreshMenuTree:menu];
   if (model_)
     model_->MenuWillShow();
 }
@@ -577,7 +584,6 @@ - (void)menuDidClose:(NSMenu*)menu {
     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.