Export the DevSettings module, add `addMenuItem` method (#25848)
Summary: I wanted to configure the RN dev menu without having to write native code. This is pretty useful in a greenfield app since it avoids having to write a custom native module for both platforms (and might enable the feature for expo too). This ended up a bit more involved than planned since callbacks can only be called once. I needed to convert the `DevSettings` module to a `NativeEventEmitter` and use events when buttons are clicked. This means creating a JS wrapper for it. Currently it does not export all methods, they can be added in follow ups as needed. ## Changelog [General] [Added] - Export the DevSettings module, add `addMenuItem` method Pull Request resolved: https://github.com/facebook/react-native/pull/25848 Test Plan: Tested in an app using the following code. ```js if (__DEV__) { DevSettings.addMenuItem('Show Dev Screen', () => { dispatchNavigationAction( NavigationActions.navigate({ routeName: 'dev', }), ); }); } ``` Added an example in RN tester ![devmenu](https://user-images.githubusercontent.com/2677334/62000297-71624680-b0a1-11e9-8403-bc95c4747f0c.gif) Differential Revision: D17394916 Pulled By: cpojer fbshipit-source-id: f9d2c548b09821c594189d1436a27b97cf5a5737
This commit is contained in:
Родитель
15343863b0
Коммит
cc068b0551
|
@ -801,6 +801,10 @@ namespace facebook {
|
|||
return static_cast<ObjCTurboModule&>(turboModule).invokeObjCMethod(rt, VoidKind, "toggleElementInspector", @selector(toggleElementInspector), args, count);
|
||||
}
|
||||
|
||||
static facebook::jsi::Value __hostFunction_NativeDevSettingsSpecJSI_addMenuItem(facebook::jsi::Runtime& rt, TurboModule &turboModule, const facebook::jsi::Value* args, size_t count) {
|
||||
return static_cast<ObjCTurboModule&>(turboModule).invokeObjCMethod(rt, VoidKind, "addMenuItem", @selector(addMenuItem:), args, count);
|
||||
}
|
||||
|
||||
static facebook::jsi::Value __hostFunction_NativeDevSettingsSpecJSI_setIsShakeToShowDevMenuEnabled(facebook::jsi::Runtime& rt, TurboModule &turboModule, const facebook::jsi::Value* args, size_t count) {
|
||||
return static_cast<ObjCTurboModule&>(turboModule).invokeObjCMethod(rt, VoidKind, "setIsShakeToShowDevMenuEnabled", @selector(setIsShakeToShowDevMenuEnabled:), args, count);
|
||||
}
|
||||
|
@ -824,6 +828,9 @@ namespace facebook {
|
|||
methodMap_["toggleElementInspector"] = MethodMetadata {0, __hostFunction_NativeDevSettingsSpecJSI_toggleElementInspector};
|
||||
|
||||
|
||||
methodMap_["addMenuItem"] = MethodMetadata {1, __hostFunction_NativeDevSettingsSpecJSI_addMenuItem};
|
||||
|
||||
|
||||
methodMap_["setIsShakeToShowDevMenuEnabled"] = MethodMetadata {1, __hostFunction_NativeDevSettingsSpecJSI_setIsShakeToShowDevMenuEnabled};
|
||||
|
||||
|
||||
|
|
|
@ -743,6 +743,7 @@ namespace facebook {
|
|||
- (void)setIsDebuggingRemotely:(BOOL)isDebuggingRemotelyEnabled;
|
||||
- (void)setProfilingEnabled:(BOOL)isProfilingEnabled;
|
||||
- (void)toggleElementInspector;
|
||||
- (void)addMenuItem:(NSString *)title;
|
||||
- (void)setIsShakeToShowDevMenuEnabled:(BOOL)enabled;
|
||||
|
||||
@end
|
||||
|
|
|
@ -19,6 +19,7 @@ export interface Spec extends TurboModule {
|
|||
+setIsDebuggingRemotely: (isDebuggingRemotelyEnabled: boolean) => void;
|
||||
+setProfilingEnabled: (isProfilingEnabled: boolean) => void;
|
||||
+toggleElementInspector: () => void;
|
||||
+addMenuItem: (title: string) => void;
|
||||
|
||||
// iOS only.
|
||||
+setIsShakeToShowDevMenuEnabled: (enabled: boolean) => void;
|
||||
|
|
|
@ -0,0 +1,52 @@
|
|||
/**
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* @format
|
||||
*/
|
||||
|
||||
import NativeDevSettings from '../NativeModules/specs/NativeDevSettings';
|
||||
import NativeEventEmitter from '../EventEmitter/NativeEventEmitter';
|
||||
|
||||
class DevSettings extends NativeEventEmitter {
|
||||
_menuItems: Map<string, () => mixed>;
|
||||
|
||||
constructor() {
|
||||
super(NativeDevSettings);
|
||||
|
||||
this._menuItems = new Map();
|
||||
}
|
||||
|
||||
addMenuItem(title: string, handler: () => mixed) {
|
||||
// Make sure items are not added multiple times. This can
|
||||
// happen when hot reloading the module that registers the
|
||||
// menu items. The title is used as the id which means we
|
||||
// don't support multiple items with the same name.
|
||||
const oldHandler = this._menuItems.get(title);
|
||||
if (oldHandler != null) {
|
||||
this.removeListener('didPressMenuItem', oldHandler);
|
||||
} else {
|
||||
NativeDevSettings.addMenuItem(title);
|
||||
}
|
||||
|
||||
this._menuItems.set(title, handler);
|
||||
this.addListener('didPressMenuItem', event => {
|
||||
if (event.title === title) {
|
||||
handler();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
reload() {
|
||||
NativeDevSettings.reload();
|
||||
}
|
||||
|
||||
// TODO: Add other dev setting methods exposed by the native module.
|
||||
}
|
||||
|
||||
// Avoid including the full `NativeDevSettings` class in prod.
|
||||
class NoopDevSettings {
|
||||
addMenuItem(title: string, handler: () => mixed) {}
|
||||
reload() {}
|
||||
}
|
||||
|
||||
module.exports = __DEV__ ? new DevSettings() : new NoopDevSettings();
|
|
@ -0,0 +1,47 @@
|
|||
/**
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
* @format
|
||||
* @flow
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import * as React from 'react';
|
||||
import {Alert, Button, DevSettings} from 'react-native';
|
||||
|
||||
exports.title = 'DevSettings';
|
||||
exports.description = 'Customize the development settings';
|
||||
exports.examples = [
|
||||
{
|
||||
title: 'Add dev menu item',
|
||||
render(): React.Element<any> {
|
||||
return (
|
||||
<Button
|
||||
title="Add"
|
||||
onPress={() => {
|
||||
DevSettings.addMenuItem('Show Secret Dev Screen', () => {
|
||||
Alert.alert('Showing secret dev screen!');
|
||||
});
|
||||
}}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Reload the app',
|
||||
render(): React.Element<any> {
|
||||
return (
|
||||
<Button
|
||||
title="Reload"
|
||||
onPress={() => {
|
||||
DevSettings.reload();
|
||||
}}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
|
@ -156,6 +156,10 @@ const APIExamples: Array<RNTesterExample> = [
|
|||
key: 'DatePickerAndroidExample',
|
||||
module: require('../examples/DatePicker/DatePickerAndroidExample'),
|
||||
},
|
||||
{
|
||||
key: 'DevSettings',
|
||||
module: require('../examples/DevSettings/DevSettingsExample'),
|
||||
},
|
||||
{
|
||||
key: 'Dimensions',
|
||||
module: require('../examples/Dimensions/DimensionsExample'),
|
||||
|
|
|
@ -235,6 +235,10 @@ const APIExamples: Array<RNTesterExample> = [
|
|||
module: require('../examples/Crash/CrashExample'),
|
||||
supportsTVOS: false,
|
||||
},
|
||||
{
|
||||
key: 'DevSettings',
|
||||
module: require('../examples/DevSettings/DevSettingsExample'),
|
||||
},
|
||||
{
|
||||
key: 'Dimensions',
|
||||
module: require('../examples/Dimensions/DimensionsExample'),
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
|
||||
#import <React/RCTBridge.h>
|
||||
#import <React/RCTDefines.h>
|
||||
#import <React/RCTEventEmitter.h>
|
||||
|
||||
@protocol RCTPackagerClientMethod;
|
||||
|
||||
|
@ -29,7 +30,7 @@
|
|||
|
||||
@end
|
||||
|
||||
@interface RCTDevSettings : NSObject
|
||||
@interface RCTDevSettings : RCTEventEmitter
|
||||
|
||||
- (instancetype)initWithDataSource:(id<RCTDevSettingsDataSource>)dataSource;
|
||||
|
||||
|
|
|
@ -16,6 +16,8 @@
|
|||
#import "RCTProfile.h"
|
||||
#import "RCTUtils.h"
|
||||
|
||||
#import <React/RCTDevMenu.h>
|
||||
|
||||
static NSString *const kRCTDevSettingProfilingEnabled = @"profilingEnabled";
|
||||
static NSString *const kRCTDevSettingHotLoadingEnabled = @"hotLoadingEnabled";
|
||||
static NSString *const kRCTDevSettingIsInspectorShown = @"showInspector";
|
||||
|
@ -111,8 +113,6 @@ static NSString *const kRCTDevSettingsUserDefaultsKey = @"RCTDevMenu";
|
|||
|
||||
@implementation RCTDevSettings
|
||||
|
||||
@synthesize bridge = _bridge;
|
||||
|
||||
RCT_EXPORT_MODULE()
|
||||
|
||||
+ (BOOL)requiresMainQueueSetup
|
||||
|
@ -152,8 +152,7 @@ RCT_EXPORT_MODULE()
|
|||
|
||||
- (void)setBridge:(RCTBridge *)bridge
|
||||
{
|
||||
RCTAssert(_bridge == nil, @"RCTDevSettings module should not be reused");
|
||||
_bridge = bridge;
|
||||
[super setBridge:bridge];
|
||||
|
||||
#if ENABLE_PACKAGER_CONNECTION
|
||||
RCTBridge *__weak weakBridge = bridge;
|
||||
|
@ -197,6 +196,11 @@ RCT_EXPORT_MODULE()
|
|||
[[NSNotificationCenter defaultCenter] removeObserver:self];
|
||||
}
|
||||
|
||||
- (NSArray<NSString *> *)supportedEvents
|
||||
{
|
||||
return @[@"didPressMenuItem"];
|
||||
}
|
||||
|
||||
- (void)_updateSettingWithValue:(id)value forKey:(NSString *)key
|
||||
{
|
||||
[_dataSource updateSettingWithValue:value forKey:key];
|
||||
|
@ -210,7 +214,7 @@ RCT_EXPORT_MODULE()
|
|||
- (BOOL)isNuclideDebuggingAvailable
|
||||
{
|
||||
#if RCT_ENABLE_INSPECTOR
|
||||
return _bridge.isInspectable;
|
||||
return self.bridge.isInspectable;
|
||||
#else
|
||||
return false;
|
||||
#endif // RCT_ENABLE_INSPECTOR
|
||||
|
@ -227,12 +231,12 @@ RCT_EXPORT_MODULE()
|
|||
|
||||
- (BOOL)isHotLoadingAvailable
|
||||
{
|
||||
return _bridge.bundleURL && !_bridge.bundleURL.fileURL; // Only works when running from server
|
||||
return self.bridge.bundleURL && !self.bridge.bundleURL.fileURL; // Only works when running from server
|
||||
}
|
||||
|
||||
RCT_EXPORT_METHOD(reload)
|
||||
{
|
||||
[_bridge reload];
|
||||
[self.bridge reload];
|
||||
}
|
||||
|
||||
RCT_EXPORT_METHOD(setIsShakeToShowDevMenuEnabled : (BOOL)enabled)
|
||||
|
@ -285,10 +289,10 @@ RCT_EXPORT_METHOD(setProfilingEnabled : (BOOL)enabled)
|
|||
BOOL enabled = self.isProfilingEnabled;
|
||||
if (self.isHotLoadingAvailable && enabled != RCTProfileIsProfiling()) {
|
||||
if (enabled) {
|
||||
[_bridge startProfiling];
|
||||
[self.bridge startProfiling];
|
||||
} else {
|
||||
[_bridge stopProfiling:^(NSData *logData) {
|
||||
RCTProfileSendResult(self->_bridge, @"systrace", logData);
|
||||
[self.bridge stopProfiling:^(NSData *logData) {
|
||||
RCTProfileSendResult(self.bridge, @"systrace", logData);
|
||||
}];
|
||||
}
|
||||
}
|
||||
|
@ -302,9 +306,9 @@ RCT_EXPORT_METHOD(setHotLoadingEnabled : (BOOL)enabled)
|
|||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
|
||||
if (enabled) {
|
||||
[_bridge enqueueJSCall:@"HMRClient" method:@"enable" args:@[] completion:NULL];
|
||||
[self.bridge enqueueJSCall:@"HMRClient" method:@"enable" args:@[] completion:NULL];
|
||||
} else {
|
||||
[_bridge enqueueJSCall:@"HMRClient" method:@"disable" args:@[] completion:NULL];
|
||||
[self.bridge enqueueJSCall:@"HMRClient" method:@"disable" args:@[] completion:NULL];
|
||||
}
|
||||
#pragma clang diagnostic pop
|
||||
}
|
||||
|
@ -329,6 +333,14 @@ RCT_EXPORT_METHOD(toggleElementInspector)
|
|||
}
|
||||
}
|
||||
|
||||
RCT_EXPORT_METHOD(addMenuItem:(NSString *)title)
|
||||
{
|
||||
__weak __typeof(self) weakSelf = self;
|
||||
[self.bridge.devMenu addItem:[RCTDevMenuItem buttonItemWithTitle:title handler:^{
|
||||
[weakSelf sendEventWithName:@"didPressMenuItem" body:@{@"title": title}];
|
||||
}]];
|
||||
}
|
||||
|
||||
- (BOOL)isElementInspectorShown
|
||||
{
|
||||
return [[self settingForKey:kRCTDevSettingIsInspectorShown] boolValue];
|
||||
|
@ -347,17 +359,17 @@ RCT_EXPORT_METHOD(toggleElementInspector)
|
|||
- (void)setExecutorClass:(Class)executorClass
|
||||
{
|
||||
_executorClass = executorClass;
|
||||
if (_bridge.executorClass != executorClass) {
|
||||
if (self.bridge.executorClass != executorClass) {
|
||||
// TODO (6929129): we can remove this special case test once we have better
|
||||
// support for custom executors in the dev menu. But right now this is
|
||||
// needed to prevent overriding a custom executor with the default if a
|
||||
// custom executor has been set directly on the bridge
|
||||
if (executorClass == Nil && _bridge.executorClass != objc_lookUpClass("RCTWebSocketExecutor")) {
|
||||
if (executorClass == Nil && self.bridge.executorClass != objc_lookUpClass("RCTWebSocketExecutor")) {
|
||||
return;
|
||||
}
|
||||
|
||||
_bridge.executorClass = executorClass;
|
||||
[_bridge reload];
|
||||
self.bridge.executorClass = executorClass;
|
||||
[self.bridge reload];
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -386,7 +398,7 @@ RCT_EXPORT_METHOD(toggleElementInspector)
|
|||
|
||||
- (void)jsLoaded:(NSNotification *)notification
|
||||
{
|
||||
if (notification.userInfo[@"bridge"] != _bridge) {
|
||||
if (notification.userInfo[@"bridge"] != self.bridge) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
@ -52,4 +52,7 @@ public class ReactSettingsForTests implements DeveloperSettings {
|
|||
public boolean isStartSamplingProfilerOnInit() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addMenuItem(String title) {}
|
||||
}
|
||||
|
|
|
@ -139,7 +139,7 @@ import java.util.Map;
|
|||
case DeviceEventManagerModule.NAME:
|
||||
return new DeviceEventManagerModule(reactContext, mHardwareBackBtnHandler);
|
||||
case DevSettingsModule.NAME:
|
||||
return new DevSettingsModule(mReactInstanceManager.getDevSupportManager());
|
||||
return new DevSettingsModule(reactContext, mReactInstanceManager.getDevSupportManager());
|
||||
case ExceptionsManagerModule.NAME:
|
||||
return new ExceptionsManagerModule(mReactInstanceManager.getDevSupportManager());
|
||||
case HeadlessJsTaskSupportModule.NAME:
|
||||
|
|
|
@ -123,6 +123,11 @@ public class DevInternalSettings
|
|||
return mPreferences.getBoolean(PREFS_START_SAMPLING_PROFILER_ON_INIT, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addMenuItem(String title) {
|
||||
// Not supported.
|
||||
}
|
||||
|
||||
public interface Listener {
|
||||
void onInternalSettingsChanged();
|
||||
}
|
||||
|
|
|
@ -6,23 +6,31 @@
|
|||
*/
|
||||
package com.facebook.react.modules.debug;
|
||||
|
||||
import com.facebook.react.bridge.BaseJavaModule;
|
||||
import com.facebook.react.bridge.Arguments;
|
||||
import com.facebook.react.bridge.ReactApplicationContext;
|
||||
import com.facebook.react.bridge.ReactContextBaseJavaModule;
|
||||
import com.facebook.react.bridge.ReactMethod;
|
||||
import com.facebook.react.bridge.UiThreadUtil;
|
||||
import com.facebook.react.bridge.WritableMap;
|
||||
import com.facebook.react.devsupport.interfaces.DevOptionHandler;
|
||||
import com.facebook.react.devsupport.interfaces.DevSupportManager;
|
||||
import com.facebook.react.module.annotations.ReactModule;
|
||||
import com.facebook.react.modules.core.DeviceEventManagerModule.RCTDeviceEventEmitter;
|
||||
|
||||
/**
|
||||
* Module that exposes the URL to the source code map (used for exception stack trace parsing) to JS
|
||||
*/
|
||||
@ReactModule(name = DevSettingsModule.NAME)
|
||||
public class DevSettingsModule extends BaseJavaModule {
|
||||
public class DevSettingsModule extends ReactContextBaseJavaModule {
|
||||
|
||||
public static final String NAME = "DevSettings";
|
||||
|
||||
private final DevSupportManager mDevSupportManager;
|
||||
|
||||
public DevSettingsModule(DevSupportManager devSupportManager) {
|
||||
public DevSettingsModule(
|
||||
ReactApplicationContext reactContext, DevSupportManager devSupportManager) {
|
||||
super(reactContext);
|
||||
|
||||
mDevSupportManager = devSupportManager;
|
||||
}
|
||||
|
||||
|
@ -63,4 +71,20 @@ public class DevSettingsModule extends BaseJavaModule {
|
|||
public void toggleElementInspector() {
|
||||
mDevSupportManager.toggleElementInspector();
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void addMenuItem(final String title) {
|
||||
mDevSupportManager.addCustomDevOption(
|
||||
title,
|
||||
new DevOptionHandler() {
|
||||
@Override
|
||||
public void onOptionSelected() {
|
||||
WritableMap data = Arguments.createMap();
|
||||
data.putString("title", title);
|
||||
getReactApplicationContext()
|
||||
.getJSModule(RCTDeviceEventEmitter.class)
|
||||
.emit("didPressMenuItem", data);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -35,4 +35,7 @@ public interface DeveloperSettings {
|
|||
|
||||
/** @return Whether Start Sampling Profiler on App Start is enabled. */
|
||||
boolean isStartSamplingProfilerOnInit();
|
||||
|
||||
/** Add an item to the dev menu. */
|
||||
void addMenuItem(String title);
|
||||
}
|
||||
|
|
4
index.js
4
index.js
|
@ -56,6 +56,7 @@ import typeof BackHandler from './Libraries/Utilities/BackHandler';
|
|||
import typeof Clipboard from './Libraries/Components/Clipboard/Clipboard';
|
||||
import typeof DatePickerAndroid from './Libraries/Components/DatePickerAndroid/DatePickerAndroid';
|
||||
import typeof DeviceInfo from './Libraries/Utilities/DeviceInfo';
|
||||
import typeof DevSettings from './Libraries/Utilities/DevSettings';
|
||||
import typeof Dimensions from './Libraries/Utilities/Dimensions';
|
||||
import typeof Easing from './Libraries/Animated/src/Easing';
|
||||
import typeof ReactNative from './Libraries/Renderer/shims/ReactNative';
|
||||
|
@ -291,6 +292,9 @@ module.exports = {
|
|||
get DeviceInfo(): DeviceInfo {
|
||||
return require('./Libraries/Utilities/DeviceInfo');
|
||||
},
|
||||
get DevSettings(): DevSettings {
|
||||
return require('./Libraries/Utilities/DevSettings');
|
||||
},
|
||||
get Dimensions(): Dimensions {
|
||||
return require('./Libraries/Utilities/Dimensions');
|
||||
},
|
||||
|
|
Загрузка…
Ссылка в новой задаче