diff --git a/desktop/plugins/public/layout/InspectorSidebar.tsx b/desktop/plugins/public/layout/InspectorSidebar.tsx index f32842f4b..9f39536c2 100644 --- a/desktop/plugins/public/layout/InspectorSidebar.tsx +++ b/desktop/plugins/public/layout/InspectorSidebar.tsx @@ -18,7 +18,7 @@ import { Logger, } from 'flipper'; import {Panel} from 'flipper-plugin'; -import {PureComponent} from 'react'; +import {PureComponent, useState} from 'react'; import React from 'react'; import {useMemo, useEffect} from 'react'; import {kebabCase} from 'lodash'; @@ -85,11 +85,19 @@ type Props = { client: PluginClient; realClient: Client; logger: Logger; + inSnapshotMode: boolean; +}; + +type ElementSnapshot = { + element: Element | null; + snapshot: String | null; }; const Sidebar: React.FC = (props: Props) => { const {element} = props; + const [elementSnapshot, setElementSnapshot] = useState(); + const [sectionDefs, sectionKeys] = useMemo(() => { const sectionKeys = []; const sectionDefs = []; @@ -135,7 +143,36 @@ const Sidebar: React.FC = (props: Props) => { return [sectionDefs, sectionKeys]; }, [element]); - const sections: Array = ( + 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 && element?.data && Object.entries(SidebarExtensions).map(([ext, Comp]) => ( @@ -147,18 +184,32 @@ const Sidebar: React.FC = (props: Props) => { selectedNode={element} /> ))) || - [] - ).concat( - sectionDefs.map((def) => ( - - )), - ); + []; + + const sidebarInspector = sectionDefs.map((def) => ( + + )); + + const sidebarPreview = + props.inSnapshotMode && elementSnapshot?.snapshot ? ( + + + + ) : null; useEffect(() => { sectionKeys.map((key) => @@ -169,7 +220,13 @@ const Sidebar: React.FC = (props: Props) => { if (!element || !element.data) { return No data; } - return <>{sections}; + return ( + <> + {sidebarExtensions} + {sidebarInspector} + {sidebarPreview} + + ); }; export default Sidebar; diff --git a/desktop/plugins/public/layout/index.tsx b/desktop/plugins/public/layout/index.tsx index 5e433475c..aa71975fc 100644 --- a/desktop/plugins/public/layout/index.tsx +++ b/desktop/plugins/public/layout/index.tsx @@ -40,6 +40,7 @@ import {getFlipperLib} from 'flipper-plugin'; type State = { init: boolean; inTargetMode: boolean; + inSnapshotMode: boolean; inAXMode: boolean; inAlignmentMode: boolean; selectedElement: ElementID | null | undefined; @@ -201,6 +202,7 @@ export default class LayoutPlugin extends FlipperPlugin< state: State = { init: false, inTargetMode: false, + inSnapshotMode: false, inAXMode: false, inAlignmentMode: false, selectedElement: null, @@ -317,6 +319,10 @@ export default class LayoutPlugin extends FlipperPlugin< } }; + onToggleSnapshotMode = () => { + this.setState((prevState) => ({inSnapshotMode: !prevState.inSnapshotMode})); + }; + onToggleAXMode = () => { this.setState({inAXMode: !this.state.inAXMode}); }; @@ -476,14 +482,26 @@ export default class LayoutPlugin extends FlipperPlugin< {!this.props.isArchivedDevice && ( )} + {!this.props.isArchivedDevice && + this.realClient.query.os === 'iOS' && ( + + )} {this.realClient.query.os === 'Android' && ( diff --git a/iOS/Plugins/FlipperKitLayoutPlugin/FlipperKitLayoutPlugin/FlipperKitLayoutPlugin.mm b/iOS/Plugins/FlipperKitLayoutPlugin/FlipperKitLayoutPlugin/FlipperKitLayoutPlugin.mm index 4f720c6f2..9ff368866 100644 --- a/iOS/Plugins/FlipperKitLayoutPlugin/FlipperKitLayoutPlugin/FlipperKitLayoutPlugin.mm +++ b/iOS/Plugins/FlipperKitLayoutPlugin/FlipperKitLayoutPlugin/FlipperKitLayoutPlugin.mm @@ -155,6 +155,16 @@ NSObject* flattenLayoutEditorMessage(NSObject* field); }, responder); }]; + + [connection receive:@"getSnapshot" + withBlock:^(NSDictionary* params, id responder) { + FlipperPerformBlockOnMainThread( + ^{ + [weakSelf onCallGetSnapshot:params[@"id"] + withResponder:responder]; + }, + responder); + }]; } - (void)didDisconnect { @@ -320,6 +330,46 @@ NSObject* flattenLayoutEditorMessage(NSObject* field) { return; } +- (void)onCallGetSnapshot:(NSString*)objectId + withResponder:(id)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 withResponder:(id)responder { if (_lastHighlightedNode != nil) { diff --git a/iOS/Plugins/FlipperKitPluginUtils/FlipperKitLayoutHelpers/FlipperKitLayoutHelpers/SKNodeDescriptor.h b/iOS/Plugins/FlipperKitPluginUtils/FlipperKitLayoutHelpers/FlipperKitLayoutHelpers/SKNodeDescriptor.h index b0e537e6b..ee8cb4992 100644 --- a/iOS/Plugins/FlipperKitPluginUtils/FlipperKitLayoutHelpers/FlipperKitLayoutHelpers/SKNodeDescriptor.h +++ b/iOS/Plugins/FlipperKitPluginUtils/FlipperKitLayoutHelpers/FlipperKitLayoutHelpers/SKNodeDescriptor.h @@ -105,6 +105,12 @@ typedef void (^SKNodeUpdateData)(id value); */ - (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 one of the children of the node, or finish the hit testing on this diff --git a/iOS/Plugins/FlipperKitPluginUtils/FlipperKitLayoutHelpers/FlipperKitLayoutHelpers/SKNodeDescriptor.mm b/iOS/Plugins/FlipperKitPluginUtils/FlipperKitLayoutHelpers/FlipperKitLayoutHelpers/SKNodeDescriptor.mm index d4ddd034c..6a2967502 100644 --- a/iOS/Plugins/FlipperKitPluginUtils/FlipperKitLayoutHelpers/FlipperKitLayoutHelpers/SKNodeDescriptor.mm +++ b/iOS/Plugins/FlipperKitPluginUtils/FlipperKitLayoutHelpers/FlipperKitLayoutHelpers/SKNodeDescriptor.mm @@ -68,6 +68,10 @@ return @[]; } +- (UIImage*)getSnapshot:(BOOL)includeChildren forNode:(id)node { + return nil; +} + - (void)setHighlighted:(BOOL)highlighted forNode:(id)node { } diff --git a/iOS/Plugins/FlipperKitPluginUtils/FlipperKitLayoutIOSDescriptors/FlipperKitLayoutIOSDescriptors/SKApplicationDescriptor.m b/iOS/Plugins/FlipperKitPluginUtils/FlipperKitLayoutIOSDescriptors/FlipperKitLayoutIOSDescriptors/SKApplicationDescriptor.m index 72c943a4e..aa2cbbeb9 100644 --- a/iOS/Plugins/FlipperKitPluginUtils/FlipperKitLayoutIOSDescriptors/FlipperKitLayoutIOSDescriptors/SKApplicationDescriptor.m +++ b/iOS/Plugins/FlipperKitPluginUtils/FlipperKitLayoutIOSDescriptors/FlipperKitLayoutIOSDescriptors/SKApplicationDescriptor.m @@ -32,6 +32,11 @@ [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 { bool finish = true; for (NSInteger index = [self childCountForNode:node] - 1; index >= 0; diff --git a/iOS/Plugins/FlipperKitPluginUtils/FlipperKitLayoutIOSDescriptors/FlipperKitLayoutIOSDescriptors/SKButtonDescriptor.mm b/iOS/Plugins/FlipperKitPluginUtils/FlipperKitLayoutIOSDescriptors/FlipperKitLayoutIOSDescriptors/SKButtonDescriptor.mm index a6ea08f48..bc22dbdef 100644 --- a/iOS/Plugins/FlipperKitPluginUtils/FlipperKitLayoutIOSDescriptors/FlipperKitLayoutIOSDescriptors/SKButtonDescriptor.mm +++ b/iOS/Plugins/FlipperKitPluginUtils/FlipperKitLayoutIOSDescriptors/FlipperKitLayoutIOSDescriptors/SKButtonDescriptor.mm @@ -87,6 +87,11 @@ return mutations; [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 { [touch finish]; } diff --git a/iOS/Plugins/FlipperKitPluginUtils/FlipperKitLayoutIOSDescriptors/FlipperKitLayoutIOSDescriptors/SKScrollViewDescriptor.m b/iOS/Plugins/FlipperKitPluginUtils/FlipperKitLayoutIOSDescriptors/FlipperKitLayoutIOSDescriptors/SKScrollViewDescriptor.m index e907a7a66..cb27d4f5b 100644 --- a/iOS/Plugins/FlipperKitPluginUtils/FlipperKitLayoutIOSDescriptors/FlipperKitLayoutIOSDescriptors/SKScrollViewDescriptor.m +++ b/iOS/Plugins/FlipperKitPluginUtils/FlipperKitLayoutIOSDescriptors/FlipperKitLayoutIOSDescriptors/SKScrollViewDescriptor.m @@ -83,6 +83,11 @@ } } +- (UIImage*)getSnapshot:(BOOL)includeChildren forNode:(UIScrollView*)node { + SKNodeDescriptor* descriptor = [self descriptorForClass:[UIView class]]; + return [descriptor getSnapshot:includeChildren forNode:node]; +} + @end #endif diff --git a/iOS/Plugins/FlipperKitPluginUtils/FlipperKitLayoutIOSDescriptors/FlipperKitLayoutIOSDescriptors/SKViewControllerDescriptor.m b/iOS/Plugins/FlipperKitPluginUtils/FlipperKitLayoutIOSDescriptors/FlipperKitLayoutIOSDescriptors/SKViewControllerDescriptor.m index 4439770c7..998e4238e 100644 --- a/iOS/Plugins/FlipperKitPluginUtils/FlipperKitLayoutIOSDescriptors/FlipperKitLayoutIOSDescriptors/SKViewControllerDescriptor.m +++ b/iOS/Plugins/FlipperKitPluginUtils/FlipperKitLayoutIOSDescriptors/FlipperKitLayoutIOSDescriptors/SKViewControllerDescriptor.m @@ -36,6 +36,11 @@ [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 { [touch continueWithChildIndex:0 withOffset:(CGPoint){0, 0}]; } diff --git a/iOS/Plugins/FlipperKitPluginUtils/FlipperKitLayoutIOSDescriptors/FlipperKitLayoutIOSDescriptors/SKViewDescriptor.mm b/iOS/Plugins/FlipperKitPluginUtils/FlipperKitLayoutIOSDescriptors/FlipperKitLayoutIOSDescriptors/SKViewDescriptor.mm index 61fcc5a22..944f9fc42 100644 --- a/iOS/Plugins/FlipperKitPluginUtils/FlipperKitLayoutIOSDescriptors/FlipperKitLayoutIOSDescriptors/SKViewDescriptor.mm +++ b/iOS/Plugins/FlipperKitPluginUtils/FlipperKitLayoutIOSDescriptors/FlipperKitLayoutIOSDescriptors/SKViewDescriptor.mm @@ -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 { bool finish = true; for (NSInteger index = [self childCountForNode:node] - 1; index >= 0;