@@ -100,10 +100,10 @@ - (NSString *)title
100100@end
101101
102102#if !TARGET_OS_OSX // [macOS]
103-
104103typedef void (^RCTDevMenuAlertActionHandler)(UIAlertAction *action);
105-
106- #endif // [macOS]
104+ #else // [macOS
105+ typedef void (^RCTDevMenuAlertActionHandler)(NSModalResponse response);
106+ #endif // macOS]
107107
108108@interface RCTDevMenu () <RCTBridgeModule, RCTInvalidating, NativeDevMenuSpec>
109109
@@ -112,6 +112,8 @@ @interface RCTDevMenu () <RCTBridgeModule, RCTInvalidating, NativeDevMenuSpec>
112112@implementation RCTDevMenu {
113113#if !TARGET_OS_OSX // [macOS]
114114 UIAlertController *_actionSheet;
115+ #else // [macOS
116+ NSAlert *_alert;
115117#endif // [macOS]
116118 NSMutableArray <RCTDevMenuItem *> *_extraMenuItems;
117119}
@@ -249,12 +251,16 @@ - (void)toggle
249251 [self show ];
250252 }
251253}
254+ #endif // macOS]
252255
253256- (BOOL )isActionSheetShown
254257{
258+ #if !TARGET_OS_OSX // [macOS]
255259 return _actionSheet != nil ;
260+ #else // [macOS
261+ return _alert != nil ;
262+ #endif // macOS]
256263}
257- #endif // [macOS]
258264
259265- (void )addItem : (NSString *)title handler : (void (^)(void ))handler
260266{
@@ -435,43 +441,42 @@ - (void)setDefaultJSBundle
435441 [alert addButtonWithTitle: @" Cancel" ];
436442 [alert setAlertStyle: NSAlertStyleWarning];
437443
438- [alert beginSheetModalForWindow: [NSApp keyWindow ]
439- completionHandler: ^(NSModalResponse response) {
440- if (response == NSAlertFirstButtonReturn ) {
441- // Apply Changes
442- NSString *ipAddress = ipTextField.stringValue ;
443- NSString *port = portTextField.stringValue ;
444- NSString *bundleRoot = entrypointTextField.stringValue ;
445-
446- if (ipAddress.length == 0 && port.length == 0 ) {
447- [weakSelf setDefaultJSBundle ];
448- return ;
449- }
450-
451- NSNumberFormatter *formatter = [NSNumberFormatter new ];
452- formatter.numberStyle = NSNumberFormatterDecimalStyle;
453- NSNumber *portNumber = [formatter numberFromString: port];
454- if (portNumber == nil ) {
455- portNumber = [NSNumber numberWithInt: RCT_METRO_PORT];
456- }
457-
458- [RCTBundleURLProvider sharedSettings ].jsLocation =
459- [NSString stringWithFormat: @" %@ :%d " , ipAddress, portNumber.intValue];
460-
461- if (bundleRoot.length == 0 ) {
462- [bundleManager resetBundleURL ];
463- } else {
464- bundleManager.bundleURL = [[RCTBundleURLProvider sharedSettings ]
465- jsBundleURLForBundleRoot: bundleRoot];
466- }
467-
468- RCTTriggerReloadCommandListeners (@" Dev menu - apply changes" );
469- } else if (response == NSAlertSecondButtonReturn ) {
470- // Reset to Default
471- [weakSelf setDefaultJSBundle ];
472- }
473- // Cancel - do nothing
474- }];
444+ NSModalResponse response = [alert runModal ];
445+
446+ if (response == NSAlertFirstButtonReturn ) {
447+ // Apply Changes
448+ NSString *ipAddress = ipTextField.stringValue ;
449+ NSString *port = portTextField.stringValue ;
450+ NSString *bundleRoot = entrypointTextField.stringValue ;
451+
452+ if (ipAddress.length == 0 && port.length == 0 ) {
453+ [weakSelf setDefaultJSBundle ];
454+ return ;
455+ }
456+
457+ NSNumberFormatter *formatter = [NSNumberFormatter new ];
458+ formatter.numberStyle = NSNumberFormatterDecimalStyle;
459+ NSNumber *portNumber = [formatter numberFromString: port];
460+ if (portNumber == nil ) {
461+ portNumber = [NSNumber numberWithInt: RCT_METRO_PORT];
462+ }
463+
464+ [RCTBundleURLProvider sharedSettings ].jsLocation =
465+ [NSString stringWithFormat: @" %@ :%d " , ipAddress, portNumber.intValue];
466+
467+ if (bundleRoot.length == 0 ) {
468+ [bundleManager resetBundleURL ];
469+ } else {
470+ bundleManager.bundleURL = [[RCTBundleURLProvider sharedSettings ]
471+ jsBundleURLForBundleRoot: bundleRoot];
472+ }
473+
474+ RCTTriggerReloadCommandListeners (@" Dev menu - apply changes" );
475+ } else if (response == NSAlertSecondButtonReturn ) {
476+ // Reset to Default
477+ [weakSelf setDefaultJSBundle ];
478+ }
479+ // Cancel - do nothing
475480#endif // macOS]
476481 }]];
477482
@@ -485,19 +490,29 @@ - (void)setDefaultJSBundle
485490 if (_actionSheet || RCTRunningInAppExtension ()) {
486491 return ;
487492 }
493+ #else // [macOS
494+ if (_alert) {
495+ return ;
496+ }
497+ #endif // [macOS]
488498
489499 NSString *bridgeDescription = _bridge.bridgeDescription ;
490500 NSString *description =
491501 bridgeDescription.length > 0 ? [NSString stringWithFormat: @" Running %@ " , bridgeDescription] : nil ;
492502
503+ #if !TARGET_OS_OSX // [macOS]
493504 // On larger devices we don't have an anchor point for the action sheet
494505 UIAlertControllerStyle style = [[UIDevice currentDevice ] userInterfaceIdiom ] == UIUserInterfaceIdiomPhone
495506 ? UIAlertControllerStyleActionSheet
496507 : UIAlertControllerStyleAlert;
508+ #else // [macOS
509+ NSAlertStyle style = NSAlertStyleInformational;
510+ #endif // macOS]
497511
498512 NSString *devMenuType = [self .bridge isKindOfClass: RCTBridge.class ] ? @" Bridge" : @" Bridgeless" ;
499513 NSString *devMenuTitle = [NSString stringWithFormat: @" React Native Dev Menu (%@ )" , devMenuType];
500514
515+ #if !TARGET_OS_OSX // [macOS]
501516 _actionSheet = [UIAlertController alertControllerWithTitle: devMenuTitle message: description preferredStyle: style];
502517
503518 NSArray <RCTDevMenuItem *> *items = [self _menuItemsToPresent ];
@@ -515,20 +530,38 @@ - (void)setDefaultJSBundle
515530
516531 _presentedItems = items;
517532 [RCTPresentedViewController () presentViewController: _actionSheet animated: YES completion: nil ];
518-
519533#else // [macOS
520- NSMenu *menu = [self menu ];
521- NSWindow *window = [NSApp keyWindow ];
522- NSEvent *event = [NSEvent mouseEventWithType: NSEventTypeLeftMouseUp
523- location: CGPointMake (0 , 0 )
524- modifierFlags: 0
525- timestamp: NSTimeIntervalSince1970
526- windowNumber: [window windowNumber ]
527- context: nil
528- eventNumber: 0
529- clickCount: 0
530- pressure: 0.1 ];
531- [NSMenu popUpContextMenu: menu withEvent: event forView: [window contentView ]];
534+ _alert = [NSAlert new ];
535+ [_alert setMessageText: devMenuTitle];
536+ [_alert setInformativeText: description];
537+ [_alert setAlertStyle: NSAlertStyleInformational];
538+
539+ NSArray <RCTDevMenuItem *> *items = [self _menuItemsToPresent ];
540+ for (RCTDevMenuItem *item in items) {
541+ [_alert addButtonWithTitle: item.title];
542+ }
543+
544+ [_alert addButtonWithTitle: @" Cancel" ];
545+
546+ _presentedItems = items;
547+
548+ // If Invoked from Metro, both the key window and main window may be nil, so we fallback to the first window in that case
549+ NSWindow *window = RCTKeyWindow () ?: [NSApp mainWindow ] ?: [[NSApp windows ] firstObject ];
550+
551+
552+ [_alert beginSheetModalForWindow: window completionHandler: ^(NSModalResponse response) {
553+ // Button responses are NSAlertFirstButtonReturn, NSAlertSecondButtonReturn, etc.
554+ // The last button (Cancel) will have response = NSAlertFirstButtonReturn + menuItems.count
555+ NSInteger buttonIndex = response - NSAlertFirstButtonReturn ;
556+
557+ RCTDevMenuItem *selectedItem = nil ;
558+ if (buttonIndex >= 0 && buttonIndex < self->_presentedItems .count ) {
559+ // Execute the corresponding menu item
560+ selectedItem = self->_presentedItems [buttonIndex];
561+ }
562+ RCTDevMenuAlertActionHandler handler = [self alertActionHandlerForDevItem: selectedItem];
563+ handler (response);
564+ }];
532565#endif // macOS]
533566
534567 [_callableJSModules invokeModule: @" RCTNativeAppEventEmitter" method: @" emit" withArgs: @[ @" RCTDevMenuShown" ]];
@@ -546,37 +579,44 @@ - (RCTDevMenuAlertActionHandler)alertActionHandlerForDevItem:(RCTDevMenuItem *__
546579 };
547580}
548581#else // [macOS
582+ - (RCTDevMenuAlertActionHandler)alertActionHandlerForDevItem : (RCTDevMenuItem *__nullable)item
583+ {
584+ return ^(NSModalResponse response) {
585+ if (item) {
586+ [item callHandler ];
587+ }
588+
589+ self->_alert = nil ;
590+ };
591+ }
592+ #endif // [macOS]
593+
594+ #if TARGET_OS_OSX // [macOS
549595- (NSMenu *)menu
550596{
551- if ([_bridge.devSettings isSecondaryClickToShowDevMenuEnabled ]) {
552- NSMenu *menu = nil ;
553- if (_bridge) {
554- NSString *desc = _bridge.bridgeDescription ;
555- if (desc.length == 0 ) {
556- desc = NSStringFromClass ([_bridge class ]);
557- }
558- NSString *title = [NSString stringWithFormat: @" React Native: Development\n (%@ )" , desc];
559-
560- menu = [NSMenu new ];
561-
562- NSMutableAttributedString *attributedTitle = [[NSMutableAttributedString alloc ] initWithString: title];
563- [attributedTitle setAttributes: @{NSFontAttributeName : [NSFont menuFontOfSize: 0 ]}
564- range: NSMakeRange (0 , [attributedTitle length ])];
565- NSMenuItem *titleItem = [NSMenuItem new ];
566- [titleItem setAttributedTitle: attributedTitle];
567- [menu addItem: titleItem];
568-
569- [menu addItem: [NSMenuItem separatorItem ]];
570-
571- NSArray <RCTDevMenuItem *> *items = [self _menuItemsToPresent ];
572- for (RCTDevMenuItem *item in items) {
573- NSMenuItem *menuItem = [[NSMenuItem alloc ] initWithTitle: [item title ]
574- action: @selector (menuItemSelected: )
575- keyEquivalent: @" " ];
576- [menuItem setTarget: self ];
577- [menuItem setRepresentedObject: item];
578- [menu addItem: menuItem];
579- }
597+ if ([((RCTDevSettings *)[_moduleRegistry moduleForName: " DevSettings" ]) isSecondaryClickToShowDevMenuEnabled ]) {
598+ NSMenu *menu = [NSMenu new ];
599+
600+ NSString *devMenuType = [self .bridge isKindOfClass: RCTBridge.class ] ? @" Bridge" : @" Bridgeless" ;
601+ NSString *devMenuTitle = [NSString stringWithFormat: @" React Native Dev Menu (%@ )" , devMenuType];
602+
603+ NSMenuItem *titleItem = [NSMenuItem sectionHeaderWithTitle: devMenuTitle];
604+ if (@available (macOS 14.4 , *)) {
605+ NSString *bridgeDescription = _bridge.bridgeDescription ;
606+ NSString *description =
607+ bridgeDescription.length > 0 ? [NSString stringWithFormat: @" Running %@ " , bridgeDescription] : nil ;
608+ [titleItem setSubtitle: description];
609+ }
610+ [menu addItem: titleItem];
611+
612+ NSArray <RCTDevMenuItem *> *items = [self _menuItemsToPresent ];
613+ for (RCTDevMenuItem *item in items) {
614+ NSMenuItem *menuItem = [[NSMenuItem alloc ] initWithTitle: [item title ]
615+ action: @selector (menuItemSelected: )
616+ keyEquivalent: @" " ];
617+ [menuItem setTarget: self ];
618+ [menuItem setRepresentedObject: item];
619+ [menu addItem: menuItem];
580620 }
581621 return menu;
582622 }
0 commit comments