amis-rpc-design/node_modules/react-native/React/CoreModules/RCTDevMenu.mm
2023-10-07 19:42:30 +08:00

608 lines
22 KiB
Plaintext

/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#import <React/RCTDevMenu.h>
#import <FBReactNativeSpec/FBReactNativeSpec.h>
#import <React/RCTBridge+Private.h>
#import <React/RCTBundleURLProvider.h>
#import <React/RCTDefines.h>
#import <React/RCTDevSettings.h>
#import <React/RCTKeyCommands.h>
#import <React/RCTLog.h>
#import <React/RCTReloadCommand.h>
#import <React/RCTUtils.h>
#import "CoreModulesPlugins.h"
#if RCT_DEV_MENU
#if RCT_ENABLE_INSPECTOR
#import <React/RCTInspectorDevServerHelper.h>
#endif
NSString *const RCTShowDevMenuNotification = @"RCTShowDevMenuNotification";
@implementation UIWindow (RCTDevMenu)
- (void)RCT_motionEnded:(__unused UIEventSubtype)motion withEvent:(UIEvent *)event
{
if (event.subtype == UIEventSubtypeMotionShake) {
[[NSNotificationCenter defaultCenter] postNotificationName:RCTShowDevMenuNotification object:nil];
}
}
@end
@implementation RCTDevMenuItem {
RCTDevMenuItemTitleBlock _titleBlock;
dispatch_block_t _handler;
}
- (instancetype)initWithTitleBlock:(RCTDevMenuItemTitleBlock)titleBlock handler:(dispatch_block_t)handler
{
if ((self = [super init])) {
_titleBlock = [titleBlock copy];
_handler = [handler copy];
}
return self;
}
RCT_NOT_IMPLEMENTED(-(instancetype)init)
+ (instancetype)buttonItemWithTitleBlock:(NSString * (^)(void))titleBlock handler:(dispatch_block_t)handler
{
return [[self alloc] initWithTitleBlock:titleBlock handler:handler];
}
+ (instancetype)buttonItemWithTitle:(NSString *)title handler:(dispatch_block_t)handler
{
return [[self alloc]
initWithTitleBlock:^NSString * {
return title;
}
handler:handler];
}
- (void)callHandler
{
if (_handler) {
_handler();
}
}
- (NSString *)title
{
if (_titleBlock) {
return _titleBlock();
}
return nil;
}
@end
typedef void (^RCTDevMenuAlertActionHandler)(UIAlertAction *action);
@interface RCTDevMenu () <RCTBridgeModule, RCTInvalidating, NativeDevMenuSpec>
@end
@implementation RCTDevMenu {
UIAlertController *_actionSheet;
NSMutableArray<RCTDevMenuItem *> *_extraMenuItems;
}
@synthesize bridge = _bridge;
@synthesize moduleRegistry = _moduleRegistry;
@synthesize callableJSModules = _callableJSModules;
@synthesize bundleManager = _bundleManager;
RCT_EXPORT_MODULE()
+ (void)initialize
{
// We're swizzling here because it's poor form to override methods in a category,
// however UIWindow doesn't actually implement motionEnded:withEvent:, so there's
// no need to call the original implementation.
RCTSwapInstanceMethods([UIWindow class], @selector(motionEnded:withEvent:), @selector(RCT_motionEnded:withEvent:));
}
+ (BOOL)requiresMainQueueSetup
{
return YES;
}
- (instancetype)init
{
if ((self = [super init])) {
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(showOnShake)
name:RCTShowDevMenuNotification
object:nil];
_extraMenuItems = [NSMutableArray new];
[self registerHotkeys];
}
return self;
}
- (void)registerHotkeys
{
#if TARGET_OS_SIMULATOR || TARGET_OS_MACCATALYST
RCTKeyCommands *commands = [RCTKeyCommands sharedInstance];
__weak __typeof(self) weakSelf = self;
// Toggle debug menu
[commands registerKeyCommandWithInput:@"d"
modifierFlags:UIKeyModifierCommand
action:^(__unused UIKeyCommand *command) {
[weakSelf toggle];
}];
// Toggle element inspector
[commands registerKeyCommandWithInput:@"i"
modifierFlags:UIKeyModifierCommand
action:^(__unused UIKeyCommand *command) {
[(RCTDevSettings *)[weakSelf.moduleRegistry moduleForName:"DevSettings"]
toggleElementInspector];
}];
// Reload in normal mode
[commands registerKeyCommandWithInput:@"n"
modifierFlags:UIKeyModifierCommand
action:^(__unused UIKeyCommand *command) {
[(RCTDevSettings *)[weakSelf.moduleRegistry moduleForName:"DevSettings"]
setIsDebuggingRemotely:NO];
}];
#endif
}
- (void)unregisterHotkeys
{
#if TARGET_OS_SIMULATOR || TARGET_OS_MACCATALYST
RCTKeyCommands *commands = [RCTKeyCommands sharedInstance];
[commands unregisterKeyCommandWithInput:@"d" modifierFlags:UIKeyModifierCommand];
[commands unregisterKeyCommandWithInput:@"i" modifierFlags:UIKeyModifierCommand];
[commands unregisterKeyCommandWithInput:@"n" modifierFlags:UIKeyModifierCommand];
#endif
}
- (BOOL)isHotkeysRegistered
{
#if TARGET_OS_SIMULATOR || TARGET_OS_MACCATALYST
RCTKeyCommands *commands = [RCTKeyCommands sharedInstance];
return [commands isKeyCommandRegisteredForInput:@"d" modifierFlags:UIKeyModifierCommand] &&
[commands isKeyCommandRegisteredForInput:@"i" modifierFlags:UIKeyModifierCommand] &&
[commands isKeyCommandRegisteredForInput:@"n" modifierFlags:UIKeyModifierCommand];
#else
return NO;
#endif
}
- (dispatch_queue_t)methodQueue
{
return dispatch_get_main_queue();
}
- (void)invalidate
{
_presentedItems = nil;
[_actionSheet dismissViewControllerAnimated:YES
completion:^(void){
}];
}
- (void)showOnShake
{
if ([((RCTDevSettings *)[_moduleRegistry moduleForName:"DevSettings"]) isShakeToShowDevMenuEnabled]) {
for (UIWindow *window in [RCTSharedApplication() windows]) {
NSString *recursiveDescription = [window valueForKey:@"recursiveDescription"];
if ([recursiveDescription containsString:@"RCTView"]) {
[self show];
return;
}
}
}
}
- (void)toggle
{
if (_actionSheet) {
[_actionSheet dismissViewControllerAnimated:YES
completion:^(void){
}];
_actionSheet = nil;
} else {
[self show];
}
}
- (BOOL)isActionSheetShown
{
return _actionSheet != nil;
}
- (void)addItem:(NSString *)title handler:(void (^)(void))handler
{
[self addItem:[RCTDevMenuItem buttonItemWithTitle:title handler:handler]];
}
- (void)addItem:(RCTDevMenuItem *)item
{
[_extraMenuItems addObject:item];
}
- (void)setDefaultJSBundle
{
[[RCTBundleURLProvider sharedSettings] resetToDefaults];
self->_bundleManager.bundleURL = [[RCTBundleURLProvider sharedSettings] jsBundleURLForFallbackExtension:nil];
RCTTriggerReloadCommandListeners(@"Dev menu - reset to default");
}
- (NSArray<RCTDevMenuItem *> *)_menuItemsToPresent
{
NSMutableArray<RCTDevMenuItem *> *items = [NSMutableArray new];
// Add built-in items
__weak RCTDevSettings *devSettings = [_moduleRegistry moduleForName:"DevSettings"];
__weak RCTDevMenu *weakSelf = self;
__weak RCTBundleManager *bundleManager = _bundleManager;
[items addObject:[RCTDevMenuItem buttonItemWithTitle:@"Reload"
handler:^{
RCTTriggerReloadCommandListeners(@"Dev menu - reload");
}]];
if (!devSettings.isProfilingEnabled) {
#if RCT_ENABLE_INSPECTOR
if (devSettings.isDeviceDebuggingAvailable) {
// For on-device debugging we link out to Flipper.
// Since we're assuming Flipper is available, also include the DevTools.
// Note: For parity with the Android code.
[items addObject:[RCTDevMenuItem
buttonItemWithTitleBlock:^NSString * {
return @"Open Debugger";
}
handler:^{
[RCTInspectorDevServerHelper
openURL:@"flipper://null/Hermesdebuggerrn?device=React%20Native"
withBundleURL:bundleManager.bundleURL
withErrorMessage:@"Failed to open Flipper. Please check that Metro is running."];
}]];
[items addObject:[RCTDevMenuItem
buttonItemWithTitleBlock:^NSString * {
return @"Open React DevTools";
}
handler:^{
[RCTInspectorDevServerHelper
openURL:@"flipper://null/React?device=React%20Native"
withBundleURL:bundleManager.bundleURL
withErrorMessage:@"Failed to open Flipper. Please check that Metro is running."];
}]];
} else if (devSettings.isRemoteDebuggingAvailable) {
#else
if (devSettings.isRemoteDebuggingAvailable) {
#endif
// For remote debugging, we open up Chrome running the app in a web worker.
// Note that this requires async communication, which will not work for Turbo Modules.
[items addObject:[RCTDevMenuItem
buttonItemWithTitleBlock:^NSString * {
return devSettings.isDebuggingRemotely ? @"Stop Debugging" : @"Debug with Chrome";
}
handler:^{
devSettings.isDebuggingRemotely = !devSettings.isDebuggingRemotely;
}]];
} else {
// If neither are available, we're defaulting to a message that tells you about remote debugging.
[items
addObject:[RCTDevMenuItem
buttonItemWithTitle:@"Debugger Unavailable"
handler:^{
NSString *message = RCTTurboModuleEnabled()
? @"Debugging with Chrome is not supported when TurboModules are enabled."
: @"Include the RCTWebSocket library to enable JavaScript debugging.";
UIAlertController *alertController =
[UIAlertController alertControllerWithTitle:@"Debugger Unavailable"
message:message
preferredStyle:UIAlertControllerStyleAlert];
__weak __typeof__(alertController) weakAlertController = alertController;
[alertController
addAction:[UIAlertAction actionWithTitle:@"OK"
style:UIAlertActionStyleDefault
handler:^(__unused UIAlertAction *action) {
[weakAlertController
dismissViewControllerAnimated:YES
completion:nil];
}]];
[RCTPresentedViewController() presentViewController:alertController
animated:YES
completion:NULL];
}]];
}
}
[items addObject:[RCTDevMenuItem
buttonItemWithTitleBlock:^NSString * {
return devSettings.isElementInspectorShown ? @"Hide Inspector" : @"Show Inspector";
}
handler:^{
[devSettings toggleElementInspector];
}]];
if (devSettings.isHotLoadingAvailable) {
[items addObject:[RCTDevMenuItem
buttonItemWithTitleBlock:^NSString * {
// Previously known as "Hot Reloading". We won't use this term anymore.
return devSettings.isHotLoadingEnabled ? @"Disable Fast Refresh" : @"Enable Fast Refresh";
}
handler:^{
devSettings.isHotLoadingEnabled = !devSettings.isHotLoadingEnabled;
}]];
}
[items
addObject:[RCTDevMenuItem
buttonItemWithTitleBlock:^NSString * {
return @"Configure Bundler";
}
handler:^{
UIAlertController *alertController = [UIAlertController
alertControllerWithTitle:@"Configure Bundler"
message:@"Provide a custom bundler address, port, and entrypoint."
preferredStyle:UIAlertControllerStyleAlert];
[alertController addTextFieldWithConfigurationHandler:^(UITextField *textField) {
textField.placeholder = @"0.0.0.0";
}];
[alertController addTextFieldWithConfigurationHandler:^(UITextField *textField) {
textField.placeholder = @"8081";
}];
[alertController addTextFieldWithConfigurationHandler:^(UITextField *textField) {
textField.placeholder = @"index";
}];
[alertController
addAction:[UIAlertAction
actionWithTitle:@"Apply Changes"
style:UIAlertActionStyleDefault
handler:^(__unused UIAlertAction *action) {
NSArray *textfields = alertController.textFields;
UITextField *ipTextField = textfields[0];
UITextField *portTextField = textfields[1];
UITextField *bundleRootTextField = textfields[2];
NSString *bundleRoot = bundleRootTextField.text;
if (ipTextField.text.length == 0 && portTextField.text.length == 0) {
[weakSelf setDefaultJSBundle];
return;
}
NSNumberFormatter *formatter = [NSNumberFormatter new];
formatter.numberStyle = NSNumberFormatterDecimalStyle;
NSNumber *portNumber =
[formatter numberFromString:portTextField.text];
if (portNumber == nil) {
portNumber = [NSNumber numberWithInt:RCT_METRO_PORT];
}
[RCTBundleURLProvider sharedSettings].jsLocation = [NSString
stringWithFormat:@"%@:%d", ipTextField.text, portNumber.intValue];
if (bundleRoot.length == 0) {
[bundleManager resetBundleURL];
} else {
bundleManager.bundleURL = [[RCTBundleURLProvider sharedSettings]
jsBundleURLForBundleRoot:bundleRoot];
}
RCTTriggerReloadCommandListeners(@"Dev menu - apply changes");
}]];
[alertController addAction:[UIAlertAction actionWithTitle:@"Reset to Default"
style:UIAlertActionStyleDefault
handler:^(__unused UIAlertAction *action) {
[weakSelf setDefaultJSBundle];
}]];
[alertController addAction:[UIAlertAction actionWithTitle:@"Cancel"
style:UIAlertActionStyleCancel
handler:^(__unused UIAlertAction *action) {
return;
}]];
[RCTPresentedViewController() presentViewController:alertController animated:YES completion:NULL];
}]];
[items addObjectsFromArray:_extraMenuItems];
return items;
}
RCT_EXPORT_METHOD(show)
{
if (_actionSheet || RCTRunningInAppExtension()) {
return;
}
NSString *bridgeDescription = _bridge.bridgeDescription;
NSString *description =
bridgeDescription.length > 0 ? [NSString stringWithFormat:@"Running %@", bridgeDescription] : nil;
// On larger devices we don't have an anchor point for the action sheet
UIAlertControllerStyle style = [[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPhone
? UIAlertControllerStyleActionSheet
: UIAlertControllerStyleAlert;
NSString *devMenuType = self.bridge ? @"Bridge" : @"Bridgeless";
NSString *devMenuTitle = [NSString stringWithFormat:@"React Native Dev Menu (%@)", devMenuType];
_actionSheet = [UIAlertController alertControllerWithTitle:devMenuTitle message:description preferredStyle:style];
NSArray<RCTDevMenuItem *> *items = [self _menuItemsToPresent];
for (RCTDevMenuItem *item in items) {
[_actionSheet addAction:[UIAlertAction actionWithTitle:item.title
style:UIAlertActionStyleDefault
handler:[self alertActionHandlerForDevItem:item]]];
}
[_actionSheet addAction:[UIAlertAction actionWithTitle:@"Cancel"
style:UIAlertActionStyleCancel
handler:[self alertActionHandlerForDevItem:nil]]];
_presentedItems = items;
[RCTPresentedViewController() presentViewController:_actionSheet animated:YES completion:nil];
[_callableJSModules invokeModule:@"RCTNativeAppEventEmitter" method:@"emit" withArgs:@[ @"RCTDevMenuShown" ]];
}
- (RCTDevMenuAlertActionHandler)alertActionHandlerForDevItem:(RCTDevMenuItem *__nullable)item
{
return ^(__unused UIAlertAction *action) {
if (item) {
[item callHandler];
}
self->_actionSheet = nil;
};
}
#pragma mark - deprecated methods and properties
#define WARN_DEPRECATED_DEV_MENU_EXPORT() \
RCTLogWarn(@"Using deprecated method %s, use RCTDevSettings instead", __func__)
- (void)setShakeToShow:(BOOL)shakeToShow
{
((RCTDevSettings *)[_moduleRegistry moduleForName:"DevSettings"]).isShakeToShowDevMenuEnabled = shakeToShow;
}
- (BOOL)shakeToShow
{
return ((RCTDevSettings *)[_moduleRegistry moduleForName:"DevSettings"]).isShakeToShowDevMenuEnabled;
}
RCT_EXPORT_METHOD(reload)
{
WARN_DEPRECATED_DEV_MENU_EXPORT();
RCTTriggerReloadCommandListeners(@"Unknown from JS");
}
RCT_EXPORT_METHOD(debugRemotely : (BOOL)enableDebug)
{
WARN_DEPRECATED_DEV_MENU_EXPORT();
((RCTDevSettings *)[_moduleRegistry moduleForName:"DevSettings"]).isDebuggingRemotely = enableDebug;
}
RCT_EXPORT_METHOD(setProfilingEnabled : (BOOL)enabled)
{
WARN_DEPRECATED_DEV_MENU_EXPORT();
((RCTDevSettings *)[_moduleRegistry moduleForName:"DevSettings"]).isProfilingEnabled = enabled;
}
- (BOOL)profilingEnabled
{
return ((RCTDevSettings *)[_moduleRegistry moduleForName:"DevSettings"]).isProfilingEnabled;
}
RCT_EXPORT_METHOD(setHotLoadingEnabled : (BOOL)enabled)
{
WARN_DEPRECATED_DEV_MENU_EXPORT();
((RCTDevSettings *)[_moduleRegistry moduleForName:"DevSettings"]).isHotLoadingEnabled = enabled;
}
- (BOOL)hotLoadingEnabled
{
return ((RCTDevSettings *)[_moduleRegistry moduleForName:"DevSettings"]).isHotLoadingEnabled;
}
- (void)setHotkeysEnabled:(BOOL)enabled
{
if (enabled) {
[self registerHotkeys];
} else {
[self unregisterHotkeys];
}
}
- (BOOL)hotkeysEnabled
{
return [self isHotkeysRegistered];
}
- (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:
(const facebook::react::ObjCTurboModule::InitParams &)params
{
return std::make_shared<facebook::react::NativeDevMenuSpecJSI>(params);
}
@end
#else // Unavailable when not in dev mode
@interface RCTDevMenu () <NativeDevMenuSpec>
@end
@implementation RCTDevMenu
- (void)show
{
}
- (void)reload
{
}
- (void)addItem:(NSString *)title handler:(dispatch_block_t)handler
{
}
- (void)addItem:(RCTDevMenu *)item
{
}
- (void)debugRemotely:(BOOL)enableDebug
{
}
- (BOOL)isActionSheetShown
{
return NO;
}
+ (NSString *)moduleName
{
return @"DevMenu";
}
- (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:
(const facebook::react::ObjCTurboModule::InitParams &)params
{
return std::make_shared<facebook::react::NativeDevMenuSpecJSI>(params);
}
@end
@implementation RCTDevMenuItem
+ (instancetype)buttonItemWithTitle:(NSString *)title handler:(void (^)(void))handler
{
return nil;
}
+ (instancetype)buttonItemWithTitleBlock:(NSString * (^)(void))titleBlock handler:(void (^)(void))handler
{
return nil;
}
@end
#endif
@implementation RCTBridge (RCTDevMenu)
- (RCTDevMenu *)devMenu
{
#if RCT_DEV_MENU
return [self moduleForClass:[RCTDevMenu class]];
#else
return nil;
#endif
}
@end
Class RCTDevMenuCls(void)
{
return RCTDevMenu.class;
}