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:
Lorenzo Blasa
2022-03-28 05:17:23 -07:00
committed by Facebook GitHub Bot
parent c662f3679d
commit aed7e7e6f2
10 changed files with 188 additions and 15 deletions

View File

@@ -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,18 +184,32 @@ 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}
id={def.id} id={def.id}
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;

View File

@@ -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}
/> />

View File

@@ -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) {

View File

@@ -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

View File

@@ -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 {
} }

View File

@@ -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;

View File

@@ -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];
} }

View File

@@ -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

View File

@@ -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}];
} }

View File

@@ -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;