UI preview of selected element
Summary: This is a prototype for view preview within Flipper for iOS (Android next). If enabled, a preview of the selected element is rendered in the attribute inspector. Changelog: Add view preview/snapshot for the Layout plugin on iOS. Reviewed By: antonk52 Differential Revision: D34990372 fbshipit-source-id: 1984514fbf59041ad236008a8db10569c5fc5f94
This commit is contained in:
committed by
Facebook GitHub Bot
parent
c662f3679d
commit
aed7e7e6f2
@@ -18,7 +18,7 @@ import {
|
|||||||
Logger,
|
Logger,
|
||||||
} from 'flipper';
|
} from 'flipper';
|
||||||
import {Panel} from 'flipper-plugin';
|
import {Panel} from 'flipper-plugin';
|
||||||
import {PureComponent} from 'react';
|
import {PureComponent, useState} from 'react';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import {useMemo, useEffect} from 'react';
|
import {useMemo, useEffect} from 'react';
|
||||||
import {kebabCase} from 'lodash';
|
import {kebabCase} from 'lodash';
|
||||||
@@ -85,11 +85,19 @@ type Props = {
|
|||||||
client: PluginClient;
|
client: PluginClient;
|
||||||
realClient: Client;
|
realClient: Client;
|
||||||
logger: Logger;
|
logger: Logger;
|
||||||
|
inSnapshotMode: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ElementSnapshot = {
|
||||||
|
element: Element | null;
|
||||||
|
snapshot: String | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
const Sidebar: React.FC<Props> = (props: Props) => {
|
const Sidebar: React.FC<Props> = (props: Props) => {
|
||||||
const {element} = props;
|
const {element} = props;
|
||||||
|
|
||||||
|
const [elementSnapshot, setElementSnapshot] = useState<ElementSnapshot>();
|
||||||
|
|
||||||
const [sectionDefs, sectionKeys] = useMemo(() => {
|
const [sectionDefs, sectionKeys] = useMemo(() => {
|
||||||
const sectionKeys = [];
|
const sectionKeys = [];
|
||||||
const sectionDefs = [];
|
const sectionDefs = [];
|
||||||
@@ -135,7 +143,36 @@ const Sidebar: React.FC<Props> = (props: Props) => {
|
|||||||
return [sectionDefs, sectionKeys];
|
return [sectionDefs, sectionKeys];
|
||||||
}, [element]);
|
}, [element]);
|
||||||
|
|
||||||
const sections: Array<React.ReactNode> = (
|
useEffect(() => {
|
||||||
|
if (
|
||||||
|
props.inSnapshotMode &&
|
||||||
|
(!elementSnapshot || elementSnapshot.element != element)
|
||||||
|
) {
|
||||||
|
props.client
|
||||||
|
.call('getSnapshot', {
|
||||||
|
id: props.element?.id,
|
||||||
|
name: props.element?.name,
|
||||||
|
})
|
||||||
|
.then((response) => {
|
||||||
|
setElementSnapshot({element: element, snapshot: response.snapshot});
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
console.log(
|
||||||
|
'ElementsInspector unable to obtain screenshot for the selected item',
|
||||||
|
e,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
element,
|
||||||
|
elementSnapshot,
|
||||||
|
props.client,
|
||||||
|
props.element?.id,
|
||||||
|
props.element?.name,
|
||||||
|
props.inSnapshotMode,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const sidebarExtensions =
|
||||||
(SidebarExtensions &&
|
(SidebarExtensions &&
|
||||||
element?.data &&
|
element?.data &&
|
||||||
Object.entries(SidebarExtensions).map(([ext, Comp]) => (
|
Object.entries(SidebarExtensions).map(([ext, Comp]) => (
|
||||||
@@ -147,9 +184,9 @@ const Sidebar: React.FC<Props> = (props: Props) => {
|
|||||||
selectedNode={element}
|
selectedNode={element}
|
||||||
/>
|
/>
|
||||||
))) ||
|
))) ||
|
||||||
[]
|
[];
|
||||||
).concat(
|
|
||||||
sectionDefs.map((def) => (
|
const sidebarInspector = sectionDefs.map((def) => (
|
||||||
<InspectorSidebarSection
|
<InspectorSidebarSection
|
||||||
tooltips={props.tooltips}
|
tooltips={props.tooltips}
|
||||||
key={def.key}
|
key={def.key}
|
||||||
@@ -157,8 +194,22 @@ const Sidebar: React.FC<Props> = (props: Props) => {
|
|||||||
data={def.data}
|
data={def.data}
|
||||||
onValueChanged={props.onValueChanged}
|
onValueChanged={props.onValueChanged}
|
||||||
/>
|
/>
|
||||||
)),
|
));
|
||||||
);
|
|
||||||
|
const sidebarPreview =
|
||||||
|
props.inSnapshotMode && elementSnapshot?.snapshot ? (
|
||||||
|
<Panel key="preview" title="Preview" pad>
|
||||||
|
<img
|
||||||
|
style={{
|
||||||
|
display: 'block',
|
||||||
|
marginLeft: 'auto',
|
||||||
|
marginRight: 'auto',
|
||||||
|
width: '100%',
|
||||||
|
}}
|
||||||
|
src={'data:image/png;base64,' + elementSnapshot?.snapshot}
|
||||||
|
/>
|
||||||
|
</Panel>
|
||||||
|
) : null;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
sectionKeys.map((key) =>
|
sectionKeys.map((key) =>
|
||||||
@@ -169,7 +220,13 @@ const Sidebar: React.FC<Props> = (props: Props) => {
|
|||||||
if (!element || !element.data) {
|
if (!element || !element.data) {
|
||||||
return <NoData grow>No data</NoData>;
|
return <NoData grow>No data</NoData>;
|
||||||
}
|
}
|
||||||
return <>{sections}</>;
|
return (
|
||||||
|
<>
|
||||||
|
{sidebarExtensions}
|
||||||
|
{sidebarInspector}
|
||||||
|
{sidebarPreview}
|
||||||
|
</>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Sidebar;
|
export default Sidebar;
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ import {getFlipperLib} from 'flipper-plugin';
|
|||||||
type State = {
|
type State = {
|
||||||
init: boolean;
|
init: boolean;
|
||||||
inTargetMode: boolean;
|
inTargetMode: boolean;
|
||||||
|
inSnapshotMode: boolean;
|
||||||
inAXMode: boolean;
|
inAXMode: boolean;
|
||||||
inAlignmentMode: boolean;
|
inAlignmentMode: boolean;
|
||||||
selectedElement: ElementID | null | undefined;
|
selectedElement: ElementID | null | undefined;
|
||||||
@@ -201,6 +202,7 @@ export default class LayoutPlugin extends FlipperPlugin<
|
|||||||
state: State = {
|
state: State = {
|
||||||
init: false,
|
init: false,
|
||||||
inTargetMode: false,
|
inTargetMode: false,
|
||||||
|
inSnapshotMode: false,
|
||||||
inAXMode: false,
|
inAXMode: false,
|
||||||
inAlignmentMode: false,
|
inAlignmentMode: false,
|
||||||
selectedElement: null,
|
selectedElement: null,
|
||||||
@@ -317,6 +319,10 @@ export default class LayoutPlugin extends FlipperPlugin<
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
onToggleSnapshotMode = () => {
|
||||||
|
this.setState((prevState) => ({inSnapshotMode: !prevState.inSnapshotMode}));
|
||||||
|
};
|
||||||
|
|
||||||
onToggleAXMode = () => {
|
onToggleAXMode = () => {
|
||||||
this.setState({inAXMode: !this.state.inAXMode});
|
this.setState({inAXMode: !this.state.inAXMode});
|
||||||
};
|
};
|
||||||
@@ -476,14 +482,26 @@ export default class LayoutPlugin extends FlipperPlugin<
|
|||||||
<Toolbar>
|
<Toolbar>
|
||||||
{!this.props.isArchivedDevice && (
|
{!this.props.isArchivedDevice && (
|
||||||
<ToolbarIcon
|
<ToolbarIcon
|
||||||
|
key="targetMode"
|
||||||
onClick={this.onToggleTargetMode}
|
onClick={this.onToggleTargetMode}
|
||||||
title="Toggle target mode"
|
title="Toggle target mode"
|
||||||
icon="target"
|
icon="target"
|
||||||
active={this.state.inTargetMode}
|
active={this.state.inTargetMode}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
{!this.props.isArchivedDevice &&
|
||||||
|
this.realClient.query.os === 'iOS' && (
|
||||||
|
<ToolbarIcon
|
||||||
|
key="snapshotMode"
|
||||||
|
onClick={this.onToggleSnapshotMode}
|
||||||
|
title="Toggle to see view snapshots on the attribute inspector"
|
||||||
|
icon="eye"
|
||||||
|
active={this.state.inSnapshotMode}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
{this.realClient.query.os === 'Android' && (
|
{this.realClient.query.os === 'Android' && (
|
||||||
<ToolbarIcon
|
<ToolbarIcon
|
||||||
|
key="axMode"
|
||||||
onClick={this.onToggleAXMode}
|
onClick={this.onToggleAXMode}
|
||||||
title="Toggle to see the accessibility hierarchy"
|
title="Toggle to see the accessibility hierarchy"
|
||||||
icon="accessibility"
|
icon="accessibility"
|
||||||
@@ -492,6 +510,7 @@ export default class LayoutPlugin extends FlipperPlugin<
|
|||||||
)}
|
)}
|
||||||
{!this.props.isArchivedDevice && (
|
{!this.props.isArchivedDevice && (
|
||||||
<ToolbarIcon
|
<ToolbarIcon
|
||||||
|
key="alignmentMode"
|
||||||
onClick={this.onToggleAlignmentMode}
|
onClick={this.onToggleAlignmentMode}
|
||||||
title="Toggle AlignmentMode to show alignment lines"
|
title="Toggle AlignmentMode to show alignment lines"
|
||||||
icon="borders"
|
icon="borders"
|
||||||
@@ -500,6 +519,7 @@ export default class LayoutPlugin extends FlipperPlugin<
|
|||||||
)}
|
)}
|
||||||
{this.props.isArchivedDevice && this.state.visualizerScreenshot && (
|
{this.props.isArchivedDevice && this.state.visualizerScreenshot && (
|
||||||
<ToolbarIcon
|
<ToolbarIcon
|
||||||
|
key="visualizer"
|
||||||
onClick={this.onToggleVisualizer}
|
onClick={this.onToggleVisualizer}
|
||||||
title="Toggle visual recreation of layout"
|
title="Toggle visual recreation of layout"
|
||||||
icon="mobile"
|
icon="mobile"
|
||||||
@@ -534,6 +554,7 @@ export default class LayoutPlugin extends FlipperPlugin<
|
|||||||
client={this.getClient()}
|
client={this.getClient()}
|
||||||
realClient={this.realClient}
|
realClient={this.realClient}
|
||||||
element={element}
|
element={element}
|
||||||
|
inSnapshotMode={this.state.inSnapshotMode}
|
||||||
onValueChanged={this.onDataValueChanged}
|
onValueChanged={this.onDataValueChanged}
|
||||||
logger={this.props.logger}
|
logger={this.props.logger}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -155,6 +155,16 @@ NSObject* flattenLayoutEditorMessage(NSObject* field);
|
|||||||
},
|
},
|
||||||
responder);
|
responder);
|
||||||
}];
|
}];
|
||||||
|
|
||||||
|
[connection receive:@"getSnapshot"
|
||||||
|
withBlock:^(NSDictionary* params, id<FlipperResponder> responder) {
|
||||||
|
FlipperPerformBlockOnMainThread(
|
||||||
|
^{
|
||||||
|
[weakSelf onCallGetSnapshot:params[@"id"]
|
||||||
|
withResponder:responder];
|
||||||
|
},
|
||||||
|
responder);
|
||||||
|
}];
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)didDisconnect {
|
- (void)didDisconnect {
|
||||||
@@ -320,6 +330,46 @@ NSObject* flattenLayoutEditorMessage(NSObject* field) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
- (void)onCallGetSnapshot:(NSString*)objectId
|
||||||
|
withResponder:(id<FlipperResponder>)responder {
|
||||||
|
if (objectId == nil || [objectId isKindOfClass:[NSNull class]]) {
|
||||||
|
[responder error:@{@"error" : @"unable to get snapshot for object"}];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
id object = [_trackedObjects objectForKey:objectId];
|
||||||
|
if (object == nil) {
|
||||||
|
[responder error:@{@"error" : @"unable to get snapshot for object"}];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
id lastHighlightedObject = nil;
|
||||||
|
id lastHighlightedDescriptor = nil;
|
||||||
|
if (_lastHighlightedNode != nil) {
|
||||||
|
lastHighlightedObject = [_trackedObjects objectForKey:_lastHighlightedNode];
|
||||||
|
if (lastHighlightedObject != nil) {
|
||||||
|
lastHighlightedDescriptor = [self->_descriptorMapper
|
||||||
|
descriptorForClass:[lastHighlightedObject class]];
|
||||||
|
[lastHighlightedDescriptor setHighlighted:NO
|
||||||
|
forNode:lastHighlightedObject];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SKNodeDescriptor* descriptor =
|
||||||
|
[self->_descriptorMapper descriptorForClass:[object class]];
|
||||||
|
UIImage* snapshot = [descriptor getSnapshot:YES forNode:object];
|
||||||
|
NSData* snapshotData = UIImagePNGRepresentation(snapshot);
|
||||||
|
NSString* snapshotBase64 = [snapshotData
|
||||||
|
base64EncodedStringWithOptions:NSDataBase64Encoding64CharacterLineLength];
|
||||||
|
|
||||||
|
if (lastHighlightedDescriptor != nil) {
|
||||||
|
[lastHighlightedDescriptor setHighlighted:YES
|
||||||
|
forNode:lastHighlightedObject];
|
||||||
|
}
|
||||||
|
|
||||||
|
[responder success:@{@"snapshot" : snapshotBase64, @"id" : objectId}];
|
||||||
|
}
|
||||||
|
|
||||||
- (void)onCallSetHighlighted:(NSString*)objectId
|
- (void)onCallSetHighlighted:(NSString*)objectId
|
||||||
withResponder:(id<FlipperResponder>)responder {
|
withResponder:(id<FlipperResponder>)responder {
|
||||||
if (_lastHighlightedNode != nil) {
|
if (_lastHighlightedNode != nil) {
|
||||||
|
|||||||
@@ -105,6 +105,12 @@ typedef void (^SKNodeUpdateData)(id value);
|
|||||||
*/
|
*/
|
||||||
- (void)setHighlighted:(BOOL)highlighted forNode:(T)node;
|
- (void)setHighlighted:(BOOL)highlighted forNode:(T)node;
|
||||||
|
|
||||||
|
/**
|
||||||
|
Used to grab a snapshot of the specified node which is currently selected in
|
||||||
|
the Flipper application.
|
||||||
|
*/
|
||||||
|
- (UIImage*)getSnapshot:(BOOL)includeChildren forNode:(T)node;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Perform hit testing on the given node. Either continue the search in
|
Perform hit testing on the given node. Either continue the search in
|
||||||
one of the children of the node, or finish the hit testing on this
|
one of the children of the node, or finish the hit testing on this
|
||||||
|
|||||||
@@ -68,6 +68,10 @@
|
|||||||
return @[];
|
return @[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
- (UIImage*)getSnapshot:(BOOL)includeChildren forNode:(id)node {
|
||||||
|
return nil;
|
||||||
|
}
|
||||||
|
|
||||||
- (void)setHighlighted:(BOOL)highlighted forNode:(id)node {
|
- (void)setHighlighted:(BOOL)highlighted forNode:(id)node {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -32,6 +32,11 @@
|
|||||||
[windowDescriptor setHighlighted:highlighted forNode:[node keyWindow]];
|
[windowDescriptor setHighlighted:highlighted forNode:[node keyWindow]];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
- (UIImage*)getSnapshot:(BOOL)includeChildren forNode:(UIApplication*)node {
|
||||||
|
SKNodeDescriptor* descriptor = [self descriptorForClass:[UIView class]];
|
||||||
|
return [descriptor getSnapshot:includeChildren forNode:[node keyWindow]];
|
||||||
|
}
|
||||||
|
|
||||||
- (void)hitTest:(SKTouch*)touch forNode:(UIApplication*)node {
|
- (void)hitTest:(SKTouch*)touch forNode:(UIApplication*)node {
|
||||||
bool finish = true;
|
bool finish = true;
|
||||||
for (NSInteger index = [self childCountForNode:node] - 1; index >= 0;
|
for (NSInteger index = [self childCountForNode:node] - 1; index >= 0;
|
||||||
|
|||||||
@@ -87,6 +87,11 @@ return mutations;
|
|||||||
[viewDescriptor setHighlighted:highlighted forNode:node];
|
[viewDescriptor setHighlighted:highlighted forNode:node];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
- (UIImage*)getSnapshot:(BOOL)includeChildren forNode:(UIButton*)node {
|
||||||
|
SKNodeDescriptor* descriptor = [self descriptorForClass:[UIView class]];
|
||||||
|
return [descriptor getSnapshot:includeChildren forNode:node];
|
||||||
|
}
|
||||||
|
|
||||||
- (void)hitTest:(SKTouch*)touch forNode:(UIButton*)node {
|
- (void)hitTest:(SKTouch*)touch forNode:(UIButton*)node {
|
||||||
[touch finish];
|
[touch finish];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -83,6 +83,11 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
- (UIImage*)getSnapshot:(BOOL)includeChildren forNode:(UIScrollView*)node {
|
||||||
|
SKNodeDescriptor* descriptor = [self descriptorForClass:[UIView class]];
|
||||||
|
return [descriptor getSnapshot:includeChildren forNode:node];
|
||||||
|
}
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -36,6 +36,11 @@
|
|||||||
[descriptor setHighlighted:highlighted forNode:node.view];
|
[descriptor setHighlighted:highlighted forNode:node.view];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
- (UIImage*)getSnapshot:(BOOL)includeChildren forNode:(UIViewController*)node {
|
||||||
|
SKNodeDescriptor* descriptor = [self descriptorForClass:[UIView class]];
|
||||||
|
return [descriptor getSnapshot:includeChildren forNode:node.view];
|
||||||
|
}
|
||||||
|
|
||||||
- (void)hitTest:(SKTouch*)touch forNode:(UIViewController*)node {
|
- (void)hitTest:(SKTouch*)touch forNode:(UIViewController*)node {
|
||||||
[touch continueWithChildIndex:0 withOffset:(CGPoint){0, 0}];
|
[touch continueWithChildIndex:0 withOffset:(CGPoint){0, 0}];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -497,6 +497,21 @@ return dataMutations;
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
- (UIImage*)getSnapshot:(BOOL)includeChildren forNode:(UIView*)node {
|
||||||
|
if ([[UIScreen mainScreen] respondsToSelector:@selector(scale)]) {
|
||||||
|
UIGraphicsBeginImageContextWithOptions(
|
||||||
|
node.bounds.size, node.isOpaque, 0.0);
|
||||||
|
} else {
|
||||||
|
UIGraphicsBeginImageContext(node.bounds.size);
|
||||||
|
}
|
||||||
|
|
||||||
|
[node.layer renderInContext:UIGraphicsGetCurrentContext()];
|
||||||
|
|
||||||
|
UIImage* img = UIGraphicsGetImageFromCurrentImageContext();
|
||||||
|
UIGraphicsEndImageContext();
|
||||||
|
return img;
|
||||||
|
}
|
||||||
|
|
||||||
- (void)hitTest:(SKTouch*)touch forNode:(UIView*)node {
|
- (void)hitTest:(SKTouch*)touch forNode:(UIView*)node {
|
||||||
bool finish = true;
|
bool finish = true;
|
||||||
for (NSInteger index = [self childCountForNode:node] - 1; index >= 0;
|
for (NSInteger index = [self childCountForNode:node] - 1; index >= 0;
|
||||||
|
|||||||
Reference in New Issue
Block a user