Initial commit 🎉
fbshipit-source-id: b6fc29740c6875d2e78953b8a7123890a67930f2 Co-authored-by: Sebastian McKenzie <sebmck@fb.com> Co-authored-by: John Knox <jknox@fb.com> Co-authored-by: Emil Sjölander <emilsj@fb.com> Co-authored-by: Pritesh Nandgaonkar <prit91@fb.com>
This commit is contained in:
@@ -0,0 +1,696 @@
|
||||
// Copyright 2004-present Facebook. All Rights Reserved.
|
||||
|
||||
package com.facebook.litho.sonar;
|
||||
|
||||
import static com.facebook.litho.annotations.ImportantForAccessibility.IMPORTANT_FOR_ACCESSIBILITY_NO;
|
||||
import static com.facebook.litho.annotations.ImportantForAccessibility.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS;
|
||||
import static com.facebook.sonar.plugins.inspector.InspectorValue.Type.Color;
|
||||
import static com.facebook.sonar.plugins.inspector.InspectorValue.Type.Enum;
|
||||
import static com.facebook.sonar.plugins.inspector.InspectorValue.Type.Number;
|
||||
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.drawable.ColorDrawable;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.support.v4.util.Pair;
|
||||
import android.view.View;
|
||||
import com.facebook.litho.Component;
|
||||
import com.facebook.litho.ComponentContext;
|
||||
import com.facebook.litho.ComponentLifecycle;
|
||||
import com.facebook.litho.DebugComponent;
|
||||
import com.facebook.litho.DebugLayoutNode;
|
||||
import com.facebook.litho.EventHandler;
|
||||
import com.facebook.litho.LithoView;
|
||||
import com.facebook.litho.annotations.Prop;
|
||||
import com.facebook.litho.annotations.State;
|
||||
import com.facebook.litho.reference.Reference;
|
||||
import com.facebook.sonar.core.SonarDynamic;
|
||||
import com.facebook.sonar.core.SonarObject;
|
||||
import com.facebook.sonar.plugins.inspector.HighlightedOverlay;
|
||||
import com.facebook.sonar.plugins.inspector.InspectorValue;
|
||||
import com.facebook.sonar.plugins.inspector.Named;
|
||||
import com.facebook.sonar.plugins.inspector.NodeDescriptor;
|
||||
import com.facebook.sonar.plugins.inspector.Touch;
|
||||
import com.facebook.sonar.plugins.inspector.descriptors.ObjectDescriptor;
|
||||
import com.facebook.sonar.plugins.inspector.descriptors.utils.AccessibilityUtil;
|
||||
import com.facebook.yoga.YogaAlign;
|
||||
import com.facebook.yoga.YogaDirection;
|
||||
import com.facebook.yoga.YogaEdge;
|
||||
import com.facebook.yoga.YogaFlexDirection;
|
||||
import com.facebook.yoga.YogaJustify;
|
||||
import com.facebook.yoga.YogaPositionType;
|
||||
import com.facebook.yoga.YogaValue;
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
public class DebugComponentDescriptor extends NodeDescriptor<DebugComponent> {
|
||||
|
||||
private Map<String, List<Pair<String[], SonarDynamic>>> mOverrides = new HashMap<>();
|
||||
private DebugComponent.Overrider mOverrider =
|
||||
new DebugComponent.Overrider() {
|
||||
@Override
|
||||
public void applyComponentOverrides(String key, Component component) {
|
||||
final List<Pair<String[], SonarDynamic>> overrides = mOverrides.get(key);
|
||||
if (overrides == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (Pair<String[], SonarDynamic> override : overrides) {
|
||||
if (override.first[0].equals("Props")) {
|
||||
applyReflectiveOverride(component, override.first[1], override.second);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void applyStateOverrides(
|
||||
String key, ComponentLifecycle.StateContainer stateContainer) {
|
||||
final List<Pair<String[], SonarDynamic>> overrides = mOverrides.get(key);
|
||||
if (overrides == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (Pair<String[], SonarDynamic> override : overrides) {
|
||||
if (override.first[0].equals("State")) {
|
||||
applyReflectiveOverride(stateContainer, override.first[1], override.second);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void applyLayoutOverrides(String key, DebugLayoutNode node) {
|
||||
final List<Pair<String[], SonarDynamic>> overrides = mOverrides.get(key);
|
||||
if (overrides == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (Pair<String[], SonarDynamic> override : overrides) {
|
||||
if (override.first[0].equals("Layout")) {
|
||||
try {
|
||||
applyLayoutOverride(
|
||||
node,
|
||||
Arrays.copyOfRange(override.first, 1, override.first.length),
|
||||
override.second);
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
} else if (override.first[0].equals("Accessibility")) {
|
||||
applyAccessibilityOverride(node, override.first[1], override.second);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@Override
|
||||
public void init(DebugComponent node) {
|
||||
// We rely on the LithoView being invalidated when a component hierarchy changes.
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId(DebugComponent node) {
|
||||
return node.getGlobalKey();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName(DebugComponent node) throws Exception {
|
||||
NodeDescriptor componentDescriptor = descriptorForClass(node.getComponent().getClass());
|
||||
if (componentDescriptor.getClass() != ObjectDescriptor.class) {
|
||||
return componentDescriptor.getName(node.getComponent());
|
||||
}
|
||||
return node.getComponent().getSimpleName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getChildCount(DebugComponent node) {
|
||||
if (node.getMountedView() != null || node.getMountedDrawable() != null) {
|
||||
return 1;
|
||||
} else {
|
||||
return node.getChildComponents().size();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getChildAt(DebugComponent node, int index) {
|
||||
final View mountedView = node.getMountedView();
|
||||
final Drawable mountedDrawable = node.getMountedDrawable();
|
||||
|
||||
if (mountedView != null) {
|
||||
return mountedView;
|
||||
} else if (mountedDrawable != null) {
|
||||
return mountedDrawable;
|
||||
} else {
|
||||
return node.getChildComponents().get(index);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Named<SonarObject>> getData(DebugComponent node) throws Exception {
|
||||
NodeDescriptor componentDescriptor = descriptorForClass(node.getComponent().getClass());
|
||||
if (componentDescriptor.getClass() != ObjectDescriptor.class) {
|
||||
return componentDescriptor.getData(node.getComponent());
|
||||
}
|
||||
|
||||
final List<Named<SonarObject>> data = new ArrayList<>();
|
||||
|
||||
final SonarObject layoutData = getLayoutData(node);
|
||||
if (layoutData != null) {
|
||||
data.add(new Named<>("Layout", layoutData));
|
||||
}
|
||||
|
||||
final SonarObject propData = getPropData(node);
|
||||
if (propData != null) {
|
||||
data.add(new Named<>("Props", propData));
|
||||
}
|
||||
|
||||
final SonarObject stateData = getStateData(node);
|
||||
if (stateData != null) {
|
||||
data.add(new Named<>("State", stateData));
|
||||
}
|
||||
|
||||
final SonarObject accessibilityData = getAccessibilityData(node);
|
||||
if (accessibilityData != null) {
|
||||
data.add(new Named<>("Accessibility", accessibilityData));
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private static SonarObject getLayoutData(DebugComponent node) {
|
||||
final DebugLayoutNode layout = node.getLayoutNode();
|
||||
if (layout == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final SonarObject.Builder data = new SonarObject.Builder();
|
||||
data.put("background", fromReference(node.getContext(), layout.getBackground()));
|
||||
data.put("foreground", fromDrawable(layout.getForeground()));
|
||||
|
||||
data.put("direction", InspectorValue.mutable(Enum, layout.getLayoutDirection().toString()));
|
||||
data.put("flex-direction", InspectorValue.mutable(Enum, layout.getFlexDirection().toString()));
|
||||
data.put(
|
||||
"justify-content", InspectorValue.mutable(Enum, layout.getJustifyContent().toString()));
|
||||
data.put("align-items", InspectorValue.mutable(Enum, layout.getAlignItems().toString()));
|
||||
data.put("align-self", InspectorValue.mutable(Enum, layout.getAlignSelf().toString()));
|
||||
data.put("align-content", InspectorValue.mutable(Enum, layout.getAlignContent().toString()));
|
||||
data.put("position-type", InspectorValue.mutable(Enum, layout.getPositionType().toString()));
|
||||
|
||||
data.put("flex-grow", fromFloat(layout.getFlexGrow()));
|
||||
data.put("flex-shrink", fromFloat(layout.getFlexShrink()));
|
||||
data.put("flex-basis", fromYogaValue(layout.getFlexBasis()));
|
||||
|
||||
data.put("width", fromYogaValue(layout.getWidth()));
|
||||
data.put("min-width", fromYogaValue(layout.getMinWidth()));
|
||||
data.put("max-width", fromYogaValue(layout.getMaxWidth()));
|
||||
|
||||
data.put("height", fromYogaValue(layout.getHeight()));
|
||||
data.put("min-height", fromYogaValue(layout.getMinHeight()));
|
||||
data.put("max-height", fromYogaValue(layout.getMaxHeight()));
|
||||
|
||||
data.put("aspect-ratio", fromFloat(layout.getAspectRatio()));
|
||||
|
||||
data.put(
|
||||
"margin",
|
||||
new SonarObject.Builder()
|
||||
.put("left", fromYogaValue(layout.getMargin(YogaEdge.LEFT)))
|
||||
.put("top", fromYogaValue(layout.getMargin(YogaEdge.TOP)))
|
||||
.put("right", fromYogaValue(layout.getMargin(YogaEdge.RIGHT)))
|
||||
.put("bottom", fromYogaValue(layout.getMargin(YogaEdge.BOTTOM)))
|
||||
.put("start", fromYogaValue(layout.getMargin(YogaEdge.START)))
|
||||
.put("end", fromYogaValue(layout.getMargin(YogaEdge.END)))
|
||||
.put("horizontal", fromYogaValue(layout.getMargin(YogaEdge.HORIZONTAL)))
|
||||
.put("vertical", fromYogaValue(layout.getMargin(YogaEdge.VERTICAL)))
|
||||
.put("all", fromYogaValue(layout.getMargin(YogaEdge.ALL))));
|
||||
|
||||
data.put(
|
||||
"padding",
|
||||
new SonarObject.Builder()
|
||||
.put("left", fromYogaValue(layout.getPadding(YogaEdge.LEFT)))
|
||||
.put("top", fromYogaValue(layout.getPadding(YogaEdge.TOP)))
|
||||
.put("right", fromYogaValue(layout.getPadding(YogaEdge.RIGHT)))
|
||||
.put("bottom", fromYogaValue(layout.getPadding(YogaEdge.BOTTOM)))
|
||||
.put("start", fromYogaValue(layout.getPadding(YogaEdge.START)))
|
||||
.put("end", fromYogaValue(layout.getPadding(YogaEdge.END)))
|
||||
.put("horizontal", fromYogaValue(layout.getPadding(YogaEdge.HORIZONTAL)))
|
||||
.put("vertical", fromYogaValue(layout.getPadding(YogaEdge.VERTICAL)))
|
||||
.put("all", fromYogaValue(layout.getPadding(YogaEdge.ALL))));
|
||||
|
||||
data.put(
|
||||
"border",
|
||||
new SonarObject.Builder()
|
||||
.put("left", fromFloat(layout.getBorderWidth(YogaEdge.LEFT)))
|
||||
.put("top", fromFloat(layout.getBorderWidth(YogaEdge.TOP)))
|
||||
.put("right", fromFloat(layout.getBorderWidth(YogaEdge.RIGHT)))
|
||||
.put("bottom", fromFloat(layout.getBorderWidth(YogaEdge.BOTTOM)))
|
||||
.put("start", fromFloat(layout.getBorderWidth(YogaEdge.START)))
|
||||
.put("end", fromFloat(layout.getBorderWidth(YogaEdge.END)))
|
||||
.put("horizontal", fromFloat(layout.getBorderWidth(YogaEdge.HORIZONTAL)))
|
||||
.put("vertical", fromFloat(layout.getBorderWidth(YogaEdge.VERTICAL)))
|
||||
.put("all", fromFloat(layout.getBorderWidth(YogaEdge.ALL))));
|
||||
|
||||
data.put(
|
||||
"position",
|
||||
new SonarObject.Builder()
|
||||
.put("left", fromYogaValue(layout.getPosition(YogaEdge.LEFT)))
|
||||
.put("top", fromYogaValue(layout.getPosition(YogaEdge.TOP)))
|
||||
.put("right", fromYogaValue(layout.getPosition(YogaEdge.RIGHT)))
|
||||
.put("bottom", fromYogaValue(layout.getPosition(YogaEdge.BOTTOM)))
|
||||
.put("start", fromYogaValue(layout.getPosition(YogaEdge.START)))
|
||||
.put("end", fromYogaValue(layout.getPosition(YogaEdge.END)))
|
||||
.put("horizontal", fromYogaValue(layout.getPosition(YogaEdge.HORIZONTAL)))
|
||||
.put("vertical", fromYogaValue(layout.getPosition(YogaEdge.VERTICAL)))
|
||||
.put("all", fromYogaValue(layout.getPosition(YogaEdge.ALL))));
|
||||
|
||||
return data.build();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private static SonarObject getPropData(DebugComponent node) {
|
||||
if (node.isInternalComponent()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final Component component = node.getComponent();
|
||||
final SonarObject.Builder props = new SonarObject.Builder();
|
||||
|
||||
boolean hasProps = false;
|
||||
for (Field f : component.getClass().getDeclaredFields()) {
|
||||
try {
|
||||
f.setAccessible(true);
|
||||
|
||||
final Prop annotation = f.getAnnotation(Prop.class);
|
||||
if (annotation != null) {
|
||||
switch (annotation.resType()) {
|
||||
case COLOR:
|
||||
props.put(f.getName(), fromColor((Integer) f.get(component)));
|
||||
break;
|
||||
case DRAWABLE:
|
||||
props.put(f.getName(), fromDrawable((Drawable) f.get(component)));
|
||||
break;
|
||||
default:
|
||||
if (f.get(component) != null
|
||||
&& PropWithDescription.class.isAssignableFrom(f.get(component).getClass())) {
|
||||
final Object description =
|
||||
((PropWithDescription) f.get(component))
|
||||
.getSonarLayoutInspectorPropDescription();
|
||||
// Treat the description as immutable for now, because it's a "translation" of the
|
||||
// actual prop,
|
||||
// mutating them is not going to change the original prop.
|
||||
if (description instanceof Map<?, ?>) {
|
||||
final Map<?, ?> descriptionMap = (Map<?, ?>) description;
|
||||
for (Map.Entry<?, ?> entry : descriptionMap.entrySet()) {
|
||||
props.put(
|
||||
entry.getKey().toString(), InspectorValue.immutable(entry.getValue()));
|
||||
}
|
||||
} else {
|
||||
props.put(f.getName(), InspectorValue.immutable(description));
|
||||
}
|
||||
} else {
|
||||
if (isTypeMutable(f.getType())) {
|
||||
props.put(f.getName(), InspectorValue.mutable(f.get(component)));
|
||||
} else {
|
||||
props.put(f.getName(), InspectorValue.immutable(f.get(component)));
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
hasProps = true;
|
||||
}
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
return hasProps ? props.build() : null;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private static SonarObject getStateData(DebugComponent node) {
|
||||
if (node.isInternalComponent()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final ComponentLifecycle.StateContainer stateContainer = node.getStateContainer();
|
||||
if (stateContainer == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final SonarObject.Builder state = new SonarObject.Builder();
|
||||
|
||||
boolean hasState = false;
|
||||
for (Field f : stateContainer.getClass().getDeclaredFields()) {
|
||||
try {
|
||||
f.setAccessible(true);
|
||||
|
||||
final State annotation = f.getAnnotation(State.class);
|
||||
if (annotation != null) {
|
||||
if (isTypeMutable(f.getType())) {
|
||||
state.put(f.getName(), InspectorValue.mutable(f.get(stateContainer)));
|
||||
} else {
|
||||
state.put(f.getName(), InspectorValue.immutable(f.get(stateContainer)));
|
||||
}
|
||||
hasState = true;
|
||||
}
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
return hasState ? state.build() : null;
|
||||
}
|
||||
|
||||
private static boolean isTypeMutable(Class<?> type) {
|
||||
if (type == int.class || type == Integer.class) {
|
||||
return true;
|
||||
} else if (type == long.class || type == Long.class) {
|
||||
return true;
|
||||
} else if (type == float.class || type == Float.class) {
|
||||
return true;
|
||||
} else if (type == double.class || type == Double.class) {
|
||||
return true;
|
||||
} else if (type == boolean.class || type == Boolean.class) {
|
||||
return true;
|
||||
} else if (type.isAssignableFrom(String.class)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private static SonarObject getAccessibilityData(DebugComponent node) {
|
||||
final DebugLayoutNode layout = node.getLayoutNode();
|
||||
if (layout == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final View hostView = node.getComponentHost();
|
||||
final SonarObject.Builder accessibilityProps = new SonarObject.Builder();
|
||||
|
||||
// This needs to be an empty string to be mutable. See t20470623.
|
||||
final CharSequence contentDescription =
|
||||
layout.getContentDescription() != null ? layout.getContentDescription() : "";
|
||||
accessibilityProps.put("content-description", InspectorValue.mutable(contentDescription));
|
||||
accessibilityProps.put("focusable", InspectorValue.mutable(layout.getFocusable()));
|
||||
accessibilityProps.put(
|
||||
"important-for-accessibility",
|
||||
AccessibilityUtil.sImportantForAccessibilityMapping.get(
|
||||
layout.getImportantForAccessibility()));
|
||||
|
||||
// No host view exists, so this component is inherently not accessible. Add the reason why this
|
||||
// is the case and then return.
|
||||
if (hostView == node.getLithoView() || hostView == null) {
|
||||
final int importantForAccessibility = layout.getImportantForAccessibility();
|
||||
final boolean isAccessibilityEnabled =
|
||||
AccessibilityUtil.isAccessibilityEnabled(node.getContext());
|
||||
String ignoredReason;
|
||||
|
||||
if (!isAccessibilityEnabled) {
|
||||
ignoredReason = "No accessibility service is running.";
|
||||
} else if (importantForAccessibility == IMPORTANT_FOR_ACCESSIBILITY_NO) {
|
||||
ignoredReason = "Component has importantForAccessibility set to NO.";
|
||||
} else if (importantForAccessibility == IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS) {
|
||||
ignoredReason = "Component has importantForAccessibility set to NO_HIDE_DESCENDANTS.";
|
||||
} else {
|
||||
ignoredReason = "Component does not have content, or accessibility handlers.";
|
||||
}
|
||||
|
||||
accessibilityProps.put("talkback-ignored", true);
|
||||
accessibilityProps.put("talkback-ignored-reasons", ignoredReason);
|
||||
|
||||
return accessibilityProps.build();
|
||||
}
|
||||
|
||||
accessibilityProps.put(
|
||||
"node-info", AccessibilityUtil.getAccessibilityNodeInfoProperties(hostView));
|
||||
AccessibilityUtil.addTalkbackProperties(accessibilityProps, hostView);
|
||||
|
||||
return accessibilityProps.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setValue(DebugComponent node, String[] path, SonarDynamic value) {
|
||||
List<Pair<String[], SonarDynamic>> overrides = mOverrides.get(node.getGlobalKey());
|
||||
if (overrides == null) {
|
||||
overrides = new ArrayList<>();
|
||||
mOverrides.put(node.getGlobalKey(), overrides);
|
||||
}
|
||||
overrides.add(new Pair<>(path, value));
|
||||
|
||||
node.setOverrider(mOverrider);
|
||||
node.rerender();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Named<String>> getAttributes(DebugComponent node) {
|
||||
final List<Named<String>> attributes = new ArrayList<>();
|
||||
final String key = node.getKey();
|
||||
final String testKey = node.getTestKey();
|
||||
|
||||
if (key != null && key.trim().length() > 0) {
|
||||
attributes.add(new Named<>("key", key));
|
||||
}
|
||||
|
||||
if (testKey != null && testKey.trim().length() > 0) {
|
||||
attributes.add(new Named<>("testKey", testKey));
|
||||
}
|
||||
|
||||
return attributes;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setHighlighted(DebugComponent node, boolean selected) {
|
||||
final LithoView lithoView = node.getLithoView();
|
||||
if (lithoView == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!selected) {
|
||||
HighlightedOverlay.removeHighlight(lithoView);
|
||||
return;
|
||||
}
|
||||
|
||||
final DebugLayoutNode layout = node.getLayoutNode();
|
||||
final boolean hasNode = layout != null;
|
||||
final Rect margin;
|
||||
if (!node.isRoot()) {
|
||||
margin =
|
||||
new Rect(
|
||||
hasNode ? (int) layout.getResultMargin(YogaEdge.START) : 0,
|
||||
hasNode ? (int) layout.getResultMargin(YogaEdge.TOP) : 0,
|
||||
hasNode ? (int) layout.getResultMargin(YogaEdge.END) : 0,
|
||||
hasNode ? (int) layout.getResultMargin(YogaEdge.BOTTOM) : 0);
|
||||
} else {
|
||||
// Margin not applied if you're at the root
|
||||
margin = new Rect();
|
||||
}
|
||||
|
||||
final Rect padding =
|
||||
new Rect(
|
||||
hasNode ? (int) layout.getResultPadding(YogaEdge.START) : 0,
|
||||
hasNode ? (int) layout.getResultPadding(YogaEdge.TOP) : 0,
|
||||
hasNode ? (int) layout.getResultPadding(YogaEdge.END) : 0,
|
||||
hasNode ? (int) layout.getResultPadding(YogaEdge.BOTTOM) : 0);
|
||||
|
||||
final Rect contentBounds = node.getBoundsInLithoView();
|
||||
HighlightedOverlay.setHighlighted(lithoView, margin, padding, contentBounds);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void hitTest(DebugComponent node, Touch touch) {
|
||||
for (int i = getChildCount(node) - 1; i >= 0; i--) {
|
||||
final Object child = getChildAt(node, i);
|
||||
if (child instanceof DebugComponent) {
|
||||
final DebugComponent componentChild = (DebugComponent) child;
|
||||
final Rect bounds = componentChild.getBounds();
|
||||
|
||||
if (touch.containedIn(bounds.left, bounds.top, bounds.right, bounds.bottom)) {
|
||||
touch.continueWithOffset(i, bounds.left, bounds.top);
|
||||
return;
|
||||
}
|
||||
} else if (child instanceof View || child instanceof Drawable) {
|
||||
// Components can only mount one view or drawable and its bounds are the same as the
|
||||
// hosting component.
|
||||
touch.continueWithOffset(i, 0, 0);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
touch.finish();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDecoration(DebugComponent node) throws Exception {
|
||||
if (node.getComponent() != null) {
|
||||
NodeDescriptor componentDescriptor = descriptorForClass(node.getComponent().getClass());
|
||||
if (componentDescriptor.getClass() != ObjectDescriptor.class) {
|
||||
return componentDescriptor.getDecoration(node.getComponent());
|
||||
}
|
||||
}
|
||||
return "litho";
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean matches(String query, DebugComponent node) throws Exception {
|
||||
NodeDescriptor descriptor = descriptorForClass(Object.class);
|
||||
return descriptor.matches(query, node);
|
||||
}
|
||||
|
||||
private static void applyAccessibilityOverride(
|
||||
DebugLayoutNode node, String key, SonarDynamic value) {
|
||||
switch (key) {
|
||||
case "focusable":
|
||||
node.setFocusable(value.asBoolean());
|
||||
break;
|
||||
case "important-for-accessibility":
|
||||
node.setImportantForAccessibility(
|
||||
AccessibilityUtil.sImportantForAccessibilityMapping.get(value.asString()));
|
||||
break;
|
||||
case "content-description":
|
||||
node.setContentDescription(value.asString());
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private static void applyLayoutOverride(DebugLayoutNode node, String[] path, SonarDynamic value) {
|
||||
switch (path[0]) {
|
||||
case "background":
|
||||
node.setBackgroundColor(value.asInt());
|
||||
break;
|
||||
case "foreground":
|
||||
node.setForegroundColor(value.asInt());
|
||||
break;
|
||||
case "direction":
|
||||
node.setLayoutDirection(YogaDirection.valueOf(value.asString().toUpperCase()));
|
||||
break;
|
||||
case "flex-direction":
|
||||
node.setFlexDirection(YogaFlexDirection.valueOf(value.asString().toUpperCase()));
|
||||
break;
|
||||
case "justify-content":
|
||||
node.setJustifyContent(YogaJustify.valueOf(value.asString().toUpperCase()));
|
||||
break;
|
||||
case "align-items":
|
||||
node.setAlignItems(YogaAlign.valueOf(value.asString().toUpperCase()));
|
||||
break;
|
||||
case "align-self":
|
||||
node.setAlignSelf(YogaAlign.valueOf(value.asString().toUpperCase()));
|
||||
break;
|
||||
case "align-content":
|
||||
node.setAlignContent(YogaAlign.valueOf(value.asString().toUpperCase()));
|
||||
break;
|
||||
case "position-type":
|
||||
node.setPositionType(YogaPositionType.valueOf(value.asString().toUpperCase()));
|
||||
break;
|
||||
case "flex-grow":
|
||||
node.setFlexGrow(value.asFloat());
|
||||
break;
|
||||
case "flex-shrink":
|
||||
node.setFlexShrink(value.asFloat());
|
||||
break;
|
||||
case "flex-basis":
|
||||
node.setFlexBasis(YogaValue.parse(value.asString()));
|
||||
break;
|
||||
case "width":
|
||||
node.setWidth(YogaValue.parse(value.asString()));
|
||||
break;
|
||||
case "min-width":
|
||||
node.setMinWidth(YogaValue.parse(value.asString()));
|
||||
break;
|
||||
case "max-width":
|
||||
node.setMaxWidth(YogaValue.parse(value.asString()));
|
||||
break;
|
||||
case "height":
|
||||
node.setHeight(YogaValue.parse(value.asString()));
|
||||
break;
|
||||
case "min-height":
|
||||
node.setMinHeight(YogaValue.parse(value.asString()));
|
||||
break;
|
||||
case "max-height":
|
||||
node.setMaxHeight(YogaValue.parse(value.asString()));
|
||||
break;
|
||||
case "aspect-ratio":
|
||||
node.setAspectRatio(value.asFloat());
|
||||
break;
|
||||
case "margin":
|
||||
node.setMargin(edgeFromString(path[1]), YogaValue.parse(value.asString()));
|
||||
break;
|
||||
case "padding":
|
||||
node.setPadding(edgeFromString(path[1]), YogaValue.parse(value.asString()));
|
||||
break;
|
||||
case "border":
|
||||
node.setBorderWidth(edgeFromString(path[1]), value.asFloat());
|
||||
break;
|
||||
case "position":
|
||||
node.setPosition(edgeFromString(path[1]), YogaValue.parse(value.asString()));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private static YogaEdge edgeFromString(String s) {
|
||||
return YogaEdge.valueOf(s.toUpperCase());
|
||||
}
|
||||
|
||||
private static void applyReflectiveOverride(Object o, String key, SonarDynamic dynamic) {
|
||||
try {
|
||||
final Field field = o.getClass().getDeclaredField(key);
|
||||
field.setAccessible(true);
|
||||
|
||||
final Class type = field.getType();
|
||||
|
||||
Object value = null;
|
||||
if (type == int.class || type == Integer.class) {
|
||||
value = dynamic.asInt();
|
||||
} else if (type == long.class || type == Long.class) {
|
||||
value = dynamic.asLong();
|
||||
} else if (type == float.class || type == Float.class) {
|
||||
value = dynamic.asFloat();
|
||||
} else if (type == double.class || type == Double.class) {
|
||||
value = dynamic.asDouble();
|
||||
} else if (type == boolean.class || type == Boolean.class) {
|
||||
value = dynamic.asBoolean();
|
||||
} else if (type.isAssignableFrom(String.class)) {
|
||||
value = dynamic.asString();
|
||||
}
|
||||
|
||||
if (value != null) {
|
||||
field.set(o, value);
|
||||
}
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
private static InspectorValue fromDrawable(Drawable d) {
|
||||
if (d instanceof ColorDrawable) {
|
||||
return InspectorValue.mutable(Color, ((ColorDrawable) d).getColor());
|
||||
}
|
||||
return InspectorValue.mutable(Color, 0);
|
||||
}
|
||||
|
||||
private static <T extends Drawable> InspectorValue fromReference(
|
||||
ComponentContext c, Reference<T> r) {
|
||||
if (r == null) {
|
||||
return fromDrawable(null);
|
||||
}
|
||||
|
||||
final T d = Reference.acquire(c, r);
|
||||
final InspectorValue v = fromDrawable(d);
|
||||
Reference.release(c, d, r);
|
||||
return v;
|
||||
}
|
||||
|
||||
private static InspectorValue fromFloat(float f) {
|
||||
if (Float.isNaN(f)) {
|
||||
return InspectorValue.mutable(Enum, "undefined");
|
||||
}
|
||||
return InspectorValue.mutable(Number, f);
|
||||
}
|
||||
|
||||
private static InspectorValue fromYogaValue(YogaValue v) {
|
||||
// TODO add support for Type.Dimension or similar
|
||||
return InspectorValue.mutable(Enum, v.toString());
|
||||
}
|
||||
|
||||
private static InspectorValue fromColor(int color) {
|
||||
return InspectorValue.mutable(Color, color);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
// Copyright 2004-present Facebook. All Rights Reserved.
|
||||
|
||||
package com.facebook.litho.sonar;
|
||||
|
||||
import com.facebook.litho.DebugComponent;
|
||||
import com.facebook.litho.LithoView;
|
||||
import com.facebook.sonar.plugins.inspector.DescriptorMapping;
|
||||
|
||||
public final class LithoSonarDescriptors {
|
||||
|
||||
public static void add(DescriptorMapping descriptorMapping) {
|
||||
descriptorMapping.register(LithoView.class, new LithoViewDescriptor());
|
||||
descriptorMapping.register(DebugComponent.class, new DebugComponentDescriptor());
|
||||
}
|
||||
}
|
||||
111
android/plugins/inspector/litho-sonar/LithoViewDescriptor.java
Normal file
111
android/plugins/inspector/litho-sonar/LithoViewDescriptor.java
Normal file
@@ -0,0 +1,111 @@
|
||||
// Copyright 2004-present Facebook. All Rights Reserved.
|
||||
|
||||
package com.facebook.litho.sonar;
|
||||
|
||||
import android.graphics.Rect;
|
||||
import android.view.ViewGroup;
|
||||
import com.facebook.litho.DebugComponent;
|
||||
import com.facebook.litho.LithoView;
|
||||
import com.facebook.sonar.core.SonarDynamic;
|
||||
import com.facebook.sonar.core.SonarObject;
|
||||
import com.facebook.sonar.plugins.inspector.Named;
|
||||
import com.facebook.sonar.plugins.inspector.NodeDescriptor;
|
||||
import com.facebook.sonar.plugins.inspector.Touch;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class LithoViewDescriptor extends NodeDescriptor<LithoView> {
|
||||
|
||||
@Override
|
||||
public void init(LithoView node) throws Exception {
|
||||
node.setOnDirtyMountListener(
|
||||
new LithoView.OnDirtyMountListener() {
|
||||
@Override
|
||||
public void onDirtyMount(LithoView view) {
|
||||
invalidate(view);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId(LithoView node) throws Exception {
|
||||
final NodeDescriptor descriptor = descriptorForClass(ViewGroup.class);
|
||||
return descriptor.getId(node);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName(LithoView node) throws Exception {
|
||||
final NodeDescriptor descriptor = descriptorForClass(ViewGroup.class);
|
||||
return descriptor.getName(node);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getChildCount(LithoView node) {
|
||||
return DebugComponent.getRootInstance(node) == null ? 0 : 1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getChildAt(LithoView node, int index) {
|
||||
return DebugComponent.getRootInstance(node);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Named<SonarObject>> getData(LithoView node) throws Exception {
|
||||
final List<Named<SonarObject>> props = new ArrayList<>();
|
||||
final NodeDescriptor descriptor = descriptorForClass(ViewGroup.class);
|
||||
final Rect mountedBounds = node.getPreviousMountBounds();
|
||||
|
||||
props.add(
|
||||
0,
|
||||
new Named<>(
|
||||
"LithoView",
|
||||
new SonarObject.Builder()
|
||||
.put(
|
||||
"mountbounds",
|
||||
new SonarObject.Builder()
|
||||
.put("left", mountedBounds.left)
|
||||
.put("top", mountedBounds.top)
|
||||
.put("right", mountedBounds.right)
|
||||
.put("bottom", mountedBounds.bottom))
|
||||
.build()));
|
||||
|
||||
props.addAll(descriptor.getData(node));
|
||||
|
||||
return props;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setValue(LithoView node, String[] path, SonarDynamic value) throws Exception {
|
||||
final NodeDescriptor descriptor = descriptorForClass(ViewGroup.class);
|
||||
descriptor.setValue(node, path, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Named<String>> getAttributes(LithoView node) throws Exception {
|
||||
final NodeDescriptor descriptor = descriptorForClass(ViewGroup.class);
|
||||
return descriptor.getAttributes(node);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setHighlighted(LithoView node, boolean selected) throws Exception {
|
||||
final NodeDescriptor descriptor = descriptorForClass(ViewGroup.class);
|
||||
descriptor.setHighlighted(node, selected);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void hitTest(LithoView node, Touch touch) {
|
||||
touch.continueWithOffset(0, 0, 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDecoration(LithoView node) throws Exception {
|
||||
final NodeDescriptor descriptor = descriptorForClass(ViewGroup.class);
|
||||
return descriptor.getDecoration(node);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean matches(String query, LithoView node) throws Exception {
|
||||
NodeDescriptor descriptor = descriptorForClass(Object.class);
|
||||
return descriptor.matches(query, node);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
// Copyright 2004-present Facebook. All Rights Reserved.
|
||||
|
||||
package com.facebook.litho.sonar;
|
||||
|
||||
public interface PropWithDescription {
|
||||
|
||||
Object getSonarLayoutInspectorPropDescription();
|
||||
}
|
||||
Reference in New Issue
Block a user