react-native-macos/Libraries/Inspector/Inspector.js

349 строки
8.9 KiB
JavaScript

/**
* 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';
const Dimensions = require('../Utilities/Dimensions');
const InspectorOverlay = require('./InspectorOverlay');
const InspectorPanel = require('./InspectorPanel');
const Platform = require('../Utilities/Platform');
const React = require('react');
const ReactNative = require('../Renderer/shims/ReactNative');
const StyleSheet = require('../StyleSheet/StyleSheet');
const Touchable = require('../Components/Touchable/Touchable');
const View = require('../Components/View/View');
const invariant = require('invariant');
import type {
HostComponent,
TouchedViewDataAtPoint,
} from '../Renderer/shims/ReactNativeTypes';
type HostRef = React.ElementRef<HostComponent<mixed>>;
export type ReactRenderer = {
rendererConfig: {
getInspectorDataForViewAtPoint: (
inspectedView: ?HostRef,
locationX: number,
locationY: number,
callback: Function,
) => void,
...
},
};
const hook = window.__REACT_DEVTOOLS_GLOBAL_HOOK__;
const renderers = findRenderers();
// Required for React DevTools to view/edit React Native styles in Flipper.
// Flipper doesn't inject these values when initializing DevTools.
hook.resolveRNStyle = require('../StyleSheet/flattenStyle');
const viewConfig = require('../Components/View/ReactNativeViewViewConfig');
hook.nativeStyleEditorValidAttributes = Object.keys(
viewConfig.validAttributes.style,
);
function findRenderers(): $ReadOnlyArray<ReactRenderer> {
const allRenderers = Array.from(hook.renderers.values());
invariant(
allRenderers.length >= 1,
'Expected to find at least one React Native renderer on DevTools hook.',
);
return allRenderers;
}
function getInspectorDataForViewAtPoint(
inspectedView: ?HostRef,
locationX: number,
locationY: number,
callback: (viewData: TouchedViewDataAtPoint) => void,
) {
// Check all renderers for inspector data.
for (let i = 0; i < renderers.length; i++) {
const renderer = renderers[i];
if (renderer?.rendererConfig?.getInspectorDataForViewAtPoint != null) {
renderer.rendererConfig.getInspectorDataForViewAtPoint(
inspectedView,
locationX,
locationY,
viewData => {
// Only return with non-empty view data since only one renderer will have this view.
if (viewData && viewData.hierarchy.length > 0) {
callback(viewData);
}
},
);
}
}
}
class Inspector extends React.Component<
{
inspectedView: ?HostRef,
onRequestRerenderApp: (callback: (instance: ?HostRef) => void) => void,
...
},
{
devtoolsAgent: ?Object,
hierarchy: any,
panelPos: string,
inspecting: boolean,
selection: ?number,
perfing: boolean,
inspected: any,
inspectedView: ?HostRef,
networking: boolean,
...
},
> {
_hideTimeoutID: TimeoutID | null = null;
_subs: ?Array<() => void>;
_setTouchedViewData: ?(TouchedViewDataAtPoint) => void;
constructor(props: Object) {
super(props);
this.state = {
devtoolsAgent: null,
hierarchy: null,
panelPos: 'bottom',
inspecting: true,
perfing: false,
inspected: null,
selection: null,
inspectedView: this.props.inspectedView,
networking: false,
};
}
componentDidMount() {
hook.on('react-devtools', this._attachToDevtools);
// if devtools is already started
if (hook.reactDevtoolsAgent) {
this._attachToDevtools(hook.reactDevtoolsAgent);
}
}
componentWillUnmount() {
if (this._subs) {
this._subs.map(fn => fn());
}
hook.off('react-devtools', this._attachToDevtools);
this._setTouchedViewData = null;
}
UNSAFE_componentWillReceiveProps(newProps: Object) {
this.setState({inspectedView: newProps.inspectedView});
}
_attachToDevtools = (agent: Object) => {
agent.addListener('hideNativeHighlight', this._onAgentHideNativeHighlight);
agent.addListener('showNativeHighlight', this._onAgentShowNativeHighlight);
agent.addListener('shutdown', this._onAgentShutdown);
this.setState({
devtoolsAgent: agent,
});
};
_onAgentHideNativeHighlight = () => {
if (this.state.inspected === null) {
return;
}
// we wait to actually hide in order to avoid flicker
this._hideTimeoutID = setTimeout(() => {
this.setState({
inspected: null,
});
}, 100);
};
_onAgentShowNativeHighlight = node => {
clearTimeout(this._hideTimeoutID);
node.measure((x, y, width, height, left, top) => {
this.setState({
hierarchy: [],
inspected: {
frame: {left, top, width, height},
},
});
});
};
_onAgentShutdown = () => {
const agent = this.state.devtoolsAgent;
if (agent != null) {
agent.removeListener(
'hideNativeHighlight',
this._onAgentHideNativeHighlight,
);
agent.removeListener(
'showNativeHighlight',
this._onAgentShowNativeHighlight,
);
agent.removeListener('shutdown', this._onAgentShutdown);
this.setState({devtoolsAgent: null});
}
};
setSelection(i: number) {
const hierarchyItem = this.state.hierarchy[i];
// we pass in ReactNative.findNodeHandle as the method is injected
const {measure, props, source} = hierarchyItem.getInspectorData(
ReactNative.findNodeHandle,
);
measure((x, y, width, height, left, top) => {
this.setState({
inspected: {
frame: {left, top, width, height},
style: props.style,
source,
},
selection: i,
});
});
}
onTouchPoint(locationX: number, locationY: number) {
this._setTouchedViewData = viewData => {
const {
hierarchy,
props,
selectedIndex,
source,
frame,
pointerY,
touchedViewTag,
} = viewData;
// Sync the touched view with React DevTools.
// Note: This is Paper only. To support Fabric,
// DevTools needs to be updated to not rely on view tags.
if (this.state.devtoolsAgent && touchedViewTag) {
this.state.devtoolsAgent.selectNode(
ReactNative.findNodeHandle(touchedViewTag),
);
}
this.setState({
panelPos:
pointerY > Dimensions.get('window').height / 2 ? 'top' : 'bottom',
selection: selectedIndex,
hierarchy,
inspected: {
style: props.style,
frame,
source,
},
});
};
getInspectorDataForViewAtPoint(
this.state.inspectedView,
locationX,
locationY,
viewData => {
if (this._setTouchedViewData != null) {
this._setTouchedViewData(viewData);
this._setTouchedViewData = null;
}
},
);
}
setPerfing(val: boolean) {
this.setState({
perfing: val,
inspecting: false,
inspected: null,
networking: false,
});
}
setInspecting(val: boolean) {
this.setState({
inspecting: val,
inspected: null,
});
}
setTouchTargeting(val: boolean) {
Touchable.TOUCH_TARGET_DEBUG = val;
this.props.onRequestRerenderApp(inspectedView => {
this.setState({inspectedView});
});
}
setNetworking(val: boolean) {
this.setState({
networking: val,
perfing: false,
inspecting: false,
inspected: null,
});
}
render(): React.Node {
const panelContainerStyle =
this.state.panelPos === 'bottom'
? {bottom: 0}
: {top: Platform.OS === 'ios' ? 20 : 0};
return (
<View style={styles.container} pointerEvents="box-none">
{this.state.inspecting && (
<InspectorOverlay
inspected={this.state.inspected}
onTouchPoint={this.onTouchPoint.bind(this)}
/>
)}
<View style={[styles.panelContainer, panelContainerStyle]}>
<InspectorPanel
devtoolsIsOpen={!!this.state.devtoolsAgent}
inspecting={this.state.inspecting}
perfing={this.state.perfing}
setPerfing={this.setPerfing.bind(this)}
setInspecting={this.setInspecting.bind(this)}
inspected={this.state.inspected}
hierarchy={this.state.hierarchy}
selection={this.state.selection}
setSelection={this.setSelection.bind(this)}
touchTargeting={Touchable.TOUCH_TARGET_DEBUG}
setTouchTargeting={this.setTouchTargeting.bind(this)}
networking={this.state.networking}
setNetworking={this.setNetworking.bind(this)}
/>
</View>
</View>
);
}
}
const styles = StyleSheet.create({
container: {
position: 'absolute',
backgroundColor: 'transparent',
top: 0,
left: 0,
right: 0,
bottom: 0,
},
panelContainer: {
position: 'absolute',
left: 0,
right: 0,
},
});
module.exports = Inspector;