Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
206 changes: 123 additions & 83 deletions packages/react-native/React/CoreModules/RCTDevMenu.mm
Original file line number Diff line number Diff line change
Expand Up @@ -100,10 +100,10 @@ - (NSString *)title
@end

#if !TARGET_OS_OSX // [macOS]

typedef void (^RCTDevMenuAlertActionHandler)(UIAlertAction *action);

#endif // [macOS]
#else // [macOS
typedef void (^RCTDevMenuAlertActionHandler)(NSModalResponse response);
#endif // macOS]

@interface RCTDevMenu () <RCTBridgeModule, RCTInvalidating, NativeDevMenuSpec>

Expand All @@ -112,6 +112,8 @@ @interface RCTDevMenu () <RCTBridgeModule, RCTInvalidating, NativeDevMenuSpec>
@implementation RCTDevMenu {
#if !TARGET_OS_OSX // [macOS]
UIAlertController *_actionSheet;
#else // [macOS
NSAlert *_alert;
#endif // [macOS]
NSMutableArray<RCTDevMenuItem *> *_extraMenuItems;
}
Expand Down Expand Up @@ -247,12 +249,16 @@ - (void)toggle
[self show];
}
}
#endif // macOS]

- (BOOL)isActionSheetShown
{
#if !TARGET_OS_OSX // [macOS]
return _actionSheet != nil;
#else // [macOS
return _alert != nil;
#endif // macOS]
}
#endif // [macOS]

- (void)addItem:(NSString *)title handler:(void (^)(void))handler
{
Expand Down Expand Up @@ -433,43 +439,42 @@ - (void)setDefaultJSBundle
[alert addButtonWithTitle:@"Cancel"];
[alert setAlertStyle:NSAlertStyleWarning];

[alert beginSheetModalForWindow:[NSApp keyWindow]
completionHandler:^(NSModalResponse response) {
if (response == NSAlertFirstButtonReturn) {
// Apply Changes
NSString *ipAddress = ipTextField.stringValue;
NSString *port = portTextField.stringValue;
NSString *bundleRoot = entrypointTextField.stringValue;

if (ipAddress.length == 0 && port.length == 0) {
[weakSelf setDefaultJSBundle];
return;
}

NSNumberFormatter *formatter = [NSNumberFormatter new];
formatter.numberStyle = NSNumberFormatterDecimalStyle;
NSNumber *portNumber = [formatter numberFromString:port];
if (portNumber == nil) {
portNumber = [NSNumber numberWithInt:RCT_METRO_PORT];
}

[RCTBundleURLProvider sharedSettings].jsLocation =
[NSString stringWithFormat:@"%@:%d", ipAddress, portNumber.intValue];

if (bundleRoot.length == 0) {
[bundleManager resetBundleURL];
} else {
bundleManager.bundleURL = [[RCTBundleURLProvider sharedSettings]
jsBundleURLForBundleRoot:bundleRoot];
}

RCTTriggerReloadCommandListeners(@"Dev menu - apply changes");
} else if (response == NSAlertSecondButtonReturn) {
// Reset to Default
[weakSelf setDefaultJSBundle];
}
// Cancel - do nothing
}];
NSModalResponse response = [alert runModal];

if (response == NSAlertFirstButtonReturn) {
// Apply Changes
NSString *ipAddress = ipTextField.stringValue;
NSString *port = portTextField.stringValue;
NSString *bundleRoot = entrypointTextField.stringValue;

if (ipAddress.length == 0 && port.length == 0) {
[weakSelf setDefaultJSBundle];
return;
}

NSNumberFormatter *formatter = [NSNumberFormatter new];
formatter.numberStyle = NSNumberFormatterDecimalStyle;
NSNumber *portNumber = [formatter numberFromString:port];
if (portNumber == nil) {
portNumber = [NSNumber numberWithInt:RCT_METRO_PORT];
}

[RCTBundleURLProvider sharedSettings].jsLocation =
[NSString stringWithFormat:@"%@:%d", ipAddress, portNumber.intValue];

if (bundleRoot.length == 0) {
[bundleManager resetBundleURL];
} else {
bundleManager.bundleURL = [[RCTBundleURLProvider sharedSettings]
jsBundleURLForBundleRoot:bundleRoot];
}

RCTTriggerReloadCommandListeners(@"Dev menu - apply changes");
} else if (response == NSAlertSecondButtonReturn) {
// Reset to Default
[weakSelf setDefaultJSBundle];
}
// Cancel - do nothing
#endif // macOS]
}]];

Expand All @@ -483,19 +488,29 @@ - (void)setDefaultJSBundle
if (_actionSheet || RCTRunningInAppExtension()) {
return;
}
#else // [macOS
if (_alert) {
return;
}
#endif // [macOS]

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

#if !TARGET_OS_OSX // [macOS]
// On larger devices we don't have an anchor point for the action sheet
UIAlertControllerStyle style = [[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPhone
? UIAlertControllerStyleActionSheet
: UIAlertControllerStyleAlert;
#else // [macOS
NSAlertStyle style = NSAlertStyleInformational;
#endif // macOS]

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

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

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

_presentedItems = items;
[RCTPresentedViewController() presentViewController:_actionSheet animated:YES completion:nil];

#else // [macOS
NSMenu *menu = [self menu];
NSWindow *window = [NSApp keyWindow];
NSEvent *event = [NSEvent mouseEventWithType:NSEventTypeLeftMouseUp
location:CGPointMake(0, 0)
modifierFlags:0
timestamp:NSTimeIntervalSince1970
windowNumber:[window windowNumber]
context:nil
eventNumber:0
clickCount:0
pressure:0.1];
[NSMenu popUpContextMenu:menu withEvent:event forView:[window contentView]];
_alert = [NSAlert new];
[_alert setMessageText:devMenuTitle];
[_alert setInformativeText:description];
[_alert setAlertStyle:NSAlertStyleInformational];

NSArray<RCTDevMenuItem *> *items = [self _menuItemsToPresent];
for (RCTDevMenuItem *item in items) {
[_alert addButtonWithTitle:item.title];
}

[_alert addButtonWithTitle:@"Cancel"];

_presentedItems = items;

// If Invoked from Metro, both the key window and main window may be nil, so we fallback to the first window in that case
NSWindow *window = RCTKeyWindow() ?: [NSApp mainWindow] ?: [[NSApp windows] firstObject];


[_alert beginSheetModalForWindow:window completionHandler:^(NSModalResponse response) {
// Button responses are NSAlertFirstButtonReturn, NSAlertSecondButtonReturn, etc.
// The last button (Cancel) will have response = NSAlertFirstButtonReturn + menuItems.count
NSInteger buttonIndex = response - NSAlertFirstButtonReturn;

RCTDevMenuItem *selectedItem = nil;
if (buttonIndex >= 0 && buttonIndex < self->_presentedItems.count) {
// Execute the corresponding menu item
selectedItem = self->_presentedItems[buttonIndex];
}
RCTDevMenuAlertActionHandler handler = [self alertActionHandlerForDevItem:selectedItem];
handler(response);
}];
#endif // macOS]

[_callableJSModules invokeModule:@"RCTNativeAppEventEmitter" method:@"emit" withArgs:@[ @"RCTDevMenuShown" ]];
Expand All @@ -544,37 +577,44 @@ - (RCTDevMenuAlertActionHandler)alertActionHandlerForDevItem:(RCTDevMenuItem *__
};
}
#else // [macOS
- (RCTDevMenuAlertActionHandler)alertActionHandlerForDevItem:(RCTDevMenuItem *__nullable)item
{
return ^(NSModalResponse response) {
if (item) {
[item callHandler];
}

self->_alert = nil;
};
}
#endif // [macOS]

#if TARGET_OS_OSX // [macOS
- (NSMenu *)menu
{
if ([_bridge.devSettings isSecondaryClickToShowDevMenuEnabled]) {
NSMenu *menu = nil;
if (_bridge) {
NSString *desc = _bridge.bridgeDescription;
if (desc.length == 0) {
desc = NSStringFromClass([_bridge class]);
}
NSString *title = [NSString stringWithFormat:@"React Native: Development\n(%@)", desc];

menu = [NSMenu new];

NSMutableAttributedString *attributedTitle = [[NSMutableAttributedString alloc] initWithString:title];
[attributedTitle setAttributes:@{NSFontAttributeName : [NSFont menuFontOfSize:0]}
range:NSMakeRange(0, [attributedTitle length])];
NSMenuItem *titleItem = [NSMenuItem new];
[titleItem setAttributedTitle:attributedTitle];
[menu addItem:titleItem];

[menu addItem:[NSMenuItem separatorItem]];

NSArray<RCTDevMenuItem *> *items = [self _menuItemsToPresent];
for (RCTDevMenuItem *item in items) {
NSMenuItem *menuItem = [[NSMenuItem alloc] initWithTitle:[item title]
action:@selector(menuItemSelected:)
keyEquivalent:@""];
[menuItem setTarget:self];
[menuItem setRepresentedObject:item];
[menu addItem:menuItem];
}
if ([((RCTDevSettings *)[_moduleRegistry moduleForName:"DevSettings"]) isSecondaryClickToShowDevMenuEnabled]) {
NSMenu *menu = [NSMenu new];

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

NSMenuItem *titleItem = [NSMenuItem sectionHeaderWithTitle:devMenuTitle];
if (@available(macOS 14.4, *)) {
NSString *bridgeDescription = _bridge.bridgeDescription;
NSString *description =
bridgeDescription.length > 0 ? [NSString stringWithFormat:@"Running %@", bridgeDescription] : nil;
[titleItem setSubtitle:description];
}
[menu addItem:titleItem];

NSArray<RCTDevMenuItem *> *items = [self _menuItemsToPresent];
for (RCTDevMenuItem *item in items) {
NSMenuItem *menuItem = [[NSMenuItem alloc] initWithTitle:[item title]
action:@selector(menuItemSelected:)
keyEquivalent:@""];
[menuItem setTarget:self];
[menuItem setRepresentedObject:item];
[menu addItem:menuItem];
}
return menu;
}
Expand Down
Loading