Skip to content

Commit cd3af2f

Browse files
committed
feat: display dev menu as a sheet when invoked from Metro
1 parent 9ed2ea2 commit cd3af2f

File tree

1 file changed

+123
-83
lines changed

1 file changed

+123
-83
lines changed

packages/react-native/React/CoreModules/RCTDevMenu.mm

Lines changed: 123 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -100,10 +100,10 @@ - (NSString *)title
100100
@end
101101

102102
#if !TARGET_OS_OSX // [macOS]
103-
104103
typedef 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
}
@@ -247,12 +249,16 @@ - (void)toggle
247249
[self show];
248250
}
249251
}
252+
#endif // macOS]
250253

251254
- (BOOL)isActionSheetShown
252255
{
256+
#if !TARGET_OS_OSX // [macOS]
253257
return _actionSheet != nil;
258+
#else // [macOS
259+
return _alert != nil;
260+
#endif // macOS]
254261
}
255-
#endif // [macOS]
256262

257263
- (void)addItem:(NSString *)title handler:(void (^)(void))handler
258264
{
@@ -433,43 +439,42 @@ - (void)setDefaultJSBundle
433439
[alert addButtonWithTitle:@"Cancel"];
434440
[alert setAlertStyle:NSAlertStyleWarning];
435441

436-
[alert beginSheetModalForWindow:[NSApp keyWindow]
437-
completionHandler:^(NSModalResponse response) {
438-
if (response == NSAlertFirstButtonReturn) {
439-
// Apply Changes
440-
NSString *ipAddress = ipTextField.stringValue;
441-
NSString *port = portTextField.stringValue;
442-
NSString *bundleRoot = entrypointTextField.stringValue;
443-
444-
if (ipAddress.length == 0 && port.length == 0) {
445-
[weakSelf setDefaultJSBundle];
446-
return;
447-
}
448-
449-
NSNumberFormatter *formatter = [NSNumberFormatter new];
450-
formatter.numberStyle = NSNumberFormatterDecimalStyle;
451-
NSNumber *portNumber = [formatter numberFromString:port];
452-
if (portNumber == nil) {
453-
portNumber = [NSNumber numberWithInt:RCT_METRO_PORT];
454-
}
455-
456-
[RCTBundleURLProvider sharedSettings].jsLocation =
457-
[NSString stringWithFormat:@"%@:%d", ipAddress, portNumber.intValue];
458-
459-
if (bundleRoot.length == 0) {
460-
[bundleManager resetBundleURL];
461-
} else {
462-
bundleManager.bundleURL = [[RCTBundleURLProvider sharedSettings]
463-
jsBundleURLForBundleRoot:bundleRoot];
464-
}
465-
466-
RCTTriggerReloadCommandListeners(@"Dev menu - apply changes");
467-
} else if (response == NSAlertSecondButtonReturn) {
468-
// Reset to Default
469-
[weakSelf setDefaultJSBundle];
470-
}
471-
// Cancel - do nothing
472-
}];
442+
NSModalResponse response = [alert runModal];
443+
444+
if (response == NSAlertFirstButtonReturn) {
445+
// Apply Changes
446+
NSString *ipAddress = ipTextField.stringValue;
447+
NSString *port = portTextField.stringValue;
448+
NSString *bundleRoot = entrypointTextField.stringValue;
449+
450+
if (ipAddress.length == 0 && port.length == 0) {
451+
[weakSelf setDefaultJSBundle];
452+
return;
453+
}
454+
455+
NSNumberFormatter *formatter = [NSNumberFormatter new];
456+
formatter.numberStyle = NSNumberFormatterDecimalStyle;
457+
NSNumber *portNumber = [formatter numberFromString:port];
458+
if (portNumber == nil) {
459+
portNumber = [NSNumber numberWithInt:RCT_METRO_PORT];
460+
}
461+
462+
[RCTBundleURLProvider sharedSettings].jsLocation =
463+
[NSString stringWithFormat:@"%@:%d", ipAddress, portNumber.intValue];
464+
465+
if (bundleRoot.length == 0) {
466+
[bundleManager resetBundleURL];
467+
} else {
468+
bundleManager.bundleURL = [[RCTBundleURLProvider sharedSettings]
469+
jsBundleURLForBundleRoot:bundleRoot];
470+
}
471+
472+
RCTTriggerReloadCommandListeners(@"Dev menu - apply changes");
473+
} else if (response == NSAlertSecondButtonReturn) {
474+
// Reset to Default
475+
[weakSelf setDefaultJSBundle];
476+
}
477+
// Cancel - do nothing
473478
#endif // macOS]
474479
}]];
475480

@@ -483,19 +488,29 @@ - (void)setDefaultJSBundle
483488
if (_actionSheet || RCTRunningInAppExtension()) {
484489
return;
485490
}
491+
#else // [macOS
492+
if (_alert) {
493+
return;
494+
}
495+
#endif // [macOS]
486496

487497
NSString *bridgeDescription = _bridge.bridgeDescription;
488498
NSString *description =
489499
bridgeDescription.length > 0 ? [NSString stringWithFormat:@"Running %@", bridgeDescription] : nil;
490500

501+
#if !TARGET_OS_OSX // [macOS]
491502
// On larger devices we don't have an anchor point for the action sheet
492503
UIAlertControllerStyle style = [[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPhone
493504
? UIAlertControllerStyleActionSheet
494505
: UIAlertControllerStyleAlert;
506+
#else // [macOS
507+
NSAlertStyle style = NSAlertStyleInformational;
508+
#endif // macOS]
495509

496510
NSString *devMenuType = [self.bridge isKindOfClass:RCTBridge.class] ? @"Bridge" : @"Bridgeless";
497511
NSString *devMenuTitle = [NSString stringWithFormat:@"React Native Dev Menu (%@)", devMenuType];
498512

513+
#if !TARGET_OS_OSX // [macOS]
499514
_actionSheet = [UIAlertController alertControllerWithTitle:devMenuTitle message:description preferredStyle:style];
500515

501516
NSArray<RCTDevMenuItem *> *items = [self _menuItemsToPresent];
@@ -513,20 +528,38 @@ - (void)setDefaultJSBundle
513528

514529
_presentedItems = items;
515530
[RCTPresentedViewController() presentViewController:_actionSheet animated:YES completion:nil];
516-
517531
#else // [macOS
518-
NSMenu *menu = [self menu];
519-
NSWindow *window = [NSApp keyWindow];
520-
NSEvent *event = [NSEvent mouseEventWithType:NSEventTypeLeftMouseUp
521-
location:CGPointMake(0, 0)
522-
modifierFlags:0
523-
timestamp:NSTimeIntervalSince1970
524-
windowNumber:[window windowNumber]
525-
context:nil
526-
eventNumber:0
527-
clickCount:0
528-
pressure:0.1];
529-
[NSMenu popUpContextMenu:menu withEvent:event forView:[window contentView]];
532+
_alert = [NSAlert new];
533+
[_alert setMessageText:devMenuTitle];
534+
[_alert setInformativeText:description];
535+
[_alert setAlertStyle:NSAlertStyleInformational];
536+
537+
NSArray<RCTDevMenuItem *> *items = [self _menuItemsToPresent];
538+
for (RCTDevMenuItem *item in items) {
539+
[_alert addButtonWithTitle:item.title];
540+
}
541+
542+
[_alert addButtonWithTitle:@"Cancel"];
543+
544+
_presentedItems = items;
545+
546+
// If Invoked from Metro, both the key window and main window may be nil, so we fallback to the first window in that case
547+
NSWindow *window = RCTKeyWindow() ?: [NSApp mainWindow] ?: [[NSApp windows] firstObject];
548+
549+
550+
[_alert beginSheetModalForWindow:window completionHandler:^(NSModalResponse response) {
551+
// Button responses are NSAlertFirstButtonReturn, NSAlertSecondButtonReturn, etc.
552+
// The last button (Cancel) will have response = NSAlertFirstButtonReturn + menuItems.count
553+
NSInteger buttonIndex = response - NSAlertFirstButtonReturn;
554+
555+
RCTDevMenuItem *selectedItem = nil;
556+
if (buttonIndex >= 0 && buttonIndex < self->_presentedItems.count) {
557+
// Execute the corresponding menu item
558+
RCTDevMenuItem *selectedItem = self->_presentedItems[buttonIndex];
559+
}
560+
RCTDevMenuAlertActionHandler handler = [self alertActionHandlerForDevItem:selectedItem];
561+
handler(response);
562+
}];
530563
#endif // macOS]
531564

532565
[_callableJSModules invokeModule:@"RCTNativeAppEventEmitter" method:@"emit" withArgs:@[ @"RCTDevMenuShown" ]];
@@ -544,37 +577,44 @@ - (RCTDevMenuAlertActionHandler)alertActionHandlerForDevItem:(RCTDevMenuItem *__
544577
};
545578
}
546579
#else // [macOS
580+
- (RCTDevMenuAlertActionHandler)alertActionHandlerForDevItem:(RCTDevMenuItem *__nullable)item
581+
{
582+
return ^(NSModalResponse response) {
583+
if (item) {
584+
[item callHandler];
585+
}
586+
587+
self->_alert = nil;
588+
};
589+
}
590+
#endif // [macOS]
591+
592+
#if TARGET_OS_OSX // [macOS
547593
- (NSMenu *)menu
548594
{
549-
if ([_bridge.devSettings isSecondaryClickToShowDevMenuEnabled]) {
550-
NSMenu *menu = nil;
551-
if (_bridge) {
552-
NSString *desc = _bridge.bridgeDescription;
553-
if (desc.length == 0) {
554-
desc = NSStringFromClass([_bridge class]);
555-
}
556-
NSString *title = [NSString stringWithFormat:@"React Native: Development\n(%@)", desc];
557-
558-
menu = [NSMenu new];
559-
560-
NSMutableAttributedString *attributedTitle = [[NSMutableAttributedString alloc] initWithString:title];
561-
[attributedTitle setAttributes:@{NSFontAttributeName : [NSFont menuFontOfSize:0]}
562-
range:NSMakeRange(0, [attributedTitle length])];
563-
NSMenuItem *titleItem = [NSMenuItem new];
564-
[titleItem setAttributedTitle:attributedTitle];
565-
[menu addItem:titleItem];
566-
567-
[menu addItem:[NSMenuItem separatorItem]];
568-
569-
NSArray<RCTDevMenuItem *> *items = [self _menuItemsToPresent];
570-
for (RCTDevMenuItem *item in items) {
571-
NSMenuItem *menuItem = [[NSMenuItem alloc] initWithTitle:[item title]
572-
action:@selector(menuItemSelected:)
573-
keyEquivalent:@""];
574-
[menuItem setTarget:self];
575-
[menuItem setRepresentedObject:item];
576-
[menu addItem:menuItem];
577-
}
595+
if ([((RCTDevSettings *)[_moduleRegistry moduleForName:"DevSettings"]) isSecondaryClickToShowDevMenuEnabled]) {
596+
NSMenu *menu = [NSMenu new];
597+
598+
NSString *devMenuType = [self.bridge isKindOfClass:RCTBridge.class] ? @"Bridge" : @"Bridgeless";
599+
NSString *devMenuTitle = [NSString stringWithFormat:@"React Native Dev Menu (%@)", devMenuType];
600+
601+
NSMenuItem *titleItem = [NSMenuItem sectionHeaderWithTitle:devMenuTitle];
602+
if (@available(macOS 14.4, *)) {
603+
NSString *bridgeDescription = _bridge.bridgeDescription;
604+
NSString *description =
605+
bridgeDescription.length > 0 ? [NSString stringWithFormat:@"Running %@", bridgeDescription] : nil;
606+
[titleItem setSubtitle:description];
607+
}
608+
[menu addItem:titleItem];
609+
610+
NSArray<RCTDevMenuItem *> *items = [self _menuItemsToPresent];
611+
for (RCTDevMenuItem *item in items) {
612+
NSMenuItem *menuItem = [[NSMenuItem alloc] initWithTitle:[item title]
613+
action:@selector(menuItemSelected:)
614+
keyEquivalent:@""];
615+
[menuItem setTarget:self];
616+
[menuItem setRepresentedObject:item];
617+
[menu addItem:menuItem];
578618
}
579619
return menu;
580620
}

0 commit comments

Comments
 (0)