IJPL-158395 Provide UI Inspector for Compose-based UIs

GitOrigin-RevId: cbc1657aed4fd464a2ff8cc740104c8a9af5f285
This commit is contained in:
Alexander Lobas
2024-10-25 19:13:09 +00:00
committed by intellij-monorepo-bot
parent d588e586de
commit 8133c2d2cc
9 changed files with 261 additions and 12 deletions
@@ -30,6 +30,7 @@ fun JBComposePanel(
}
return ComposePanel(ComposeAnalytics()).apply {
setContent {
ComposeUiInspector(this)
SwingBridgeTheme {
CompositionLocalProvider(LocalComponent provides this@apply) {
ComponentDataProviderBridge(this@apply) {
@@ -0,0 +1,80 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.platform.compose
import com.intellij.internal.inspector.PropertyBean
import com.intellij.internal.inspector.UiInspectorCustomComponentChildProvider
import com.intellij.internal.inspector.UiInspectorCustomComponentProvider
import com.intellij.util.ui.UIUtil
import org.jetbrains.skiko.ExperimentalSkikoApi
import org.jetbrains.skiko.swing.SkiaSwingLayer
import java.awt.Rectangle
import javax.accessibility.AccessibleComponent
import javax.accessibility.AccessibleContext
import javax.swing.JComponent
@Suppress("FunctionName")
@OptIn(ExperimentalSkikoApi::class)
internal fun ComposeUiInspector(composePanel: JComponent) {
val skiaComponent = UIUtil.findComponentOfType<SkiaSwingLayer>(composePanel, SkiaSwingLayer::class.java)
if (skiaComponent != null && skiaComponent.getClientProperty(UiInspectorCustomComponentProvider.KEY) == null) {
skiaComponent.putClientProperty(UiInspectorCustomComponentProvider.KEY, ComposeUiInspectorProvider(skiaComponent))
}
}
private class ComposeUiInspectorProvider(private val composePanel: JComponent) : UiInspectorCustomComponentProvider {
override fun getChildren(): List<UiInspectorCustomComponentChildProvider> {
val context = composePanel.accessibleContext ?: return emptyList()
val count = context.accessibleChildrenCount
val composeChildren = mutableListOf<UiInspectorCustomComponentChildProvider>()
for (i in 0..count - 1) {
val component = context.getAccessibleChild(i).accessibleContext
if (component != null) {
composeChildren.add(InspectorObject(component))
}
}
return composeChildren
}
}
private val PROPERTIES = listOf<String>("isEnabled", "isVisible", "isShowing", "getLocationOnScreen", "getLocation",
"getBounds", "getSize", "isFocusTraversable", "requestFocus")
private class InspectorObject(private val component: AccessibleContext) : UiInspectorCustomComponentChildProvider {
override fun getTreeName(): String {
val name = component.accessibleName ?: component.accessibleDescription
if (name.isNullOrBlank()) {
return component.toString()
}
return name
}
override fun getChildren(): List<UiInspectorCustomComponentChildProvider> {
val count = component.accessibleChildrenCount
val composeChildren = mutableListOf<UiInspectorCustomComponentChildProvider>()
for (i in 0..count - 1) {
val childComponent = component.getAccessibleChild(i).accessibleContext
if (childComponent != null) {
composeChildren.add(InspectorObject(childComponent))
}
}
return composeChildren
}
override fun getObjectForProperties() = component
override fun getPropertiesMethodList() = PROPERTIES
override fun getUiInspectorContext() = emptyList<PropertyBean>()
override fun getHighlightingBounds(): Rectangle? {
if (component is AccessibleComponent) {
return component.bounds
}
return null
}
}
@@ -1326,6 +1326,19 @@ c:com.intellij.internal.inspector.PropertyBean
- <init>(java.lang.String,java.lang.Object,Z):V
com.intellij.internal.inspector.UiInspectorContextProvider
- a:getUiInspectorContext():java.util.List
com.intellij.internal.inspector.UiInspectorCustomComponentChildProvider
- com.intellij.internal.inspector.UiInspectorContextProvider
- a:getChildren():java.util.List
- a:getHighlightingBounds():java.awt.Rectangle
- a:getObjectForProperties():java.lang.Object
- a:getPropertiesMethodList():java.util.List
- a:getTreeName():java.lang.String
com.intellij.internal.inspector.UiInspectorCustomComponentProvider
- sf:Companion:com.intellij.internal.inspector.UiInspectorCustomComponentProvider$Companion
- sf:KEY:java.lang.String
- a:getChildren():java.util.List
f:com.intellij.internal.inspector.UiInspectorCustomComponentProvider$Companion
- sf:KEY:java.lang.String
com.intellij.internal.inspector.UiInspectorListRendererContextProvider
- a:getUiInspectorContext(javax.swing.JList,java.lang.Object,I):java.util.List
com.intellij.internal.inspector.UiInspectorPreciseContextProvider
@@ -0,0 +1,24 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.internal.inspector
import java.awt.Rectangle
interface UiInspectorCustomComponentProvider {
companion object {
const val KEY: String = "UiInspectorCustomComponentProvider"
}
fun getChildren(): List<UiInspectorCustomComponentChildProvider>
}
interface UiInspectorCustomComponentChildProvider : UiInspectorContextProvider {
fun getTreeName(): String
fun getChildren(): List<UiInspectorCustomComponentChildProvider>
fun getObjectForProperties(): Any?
fun getPropertiesMethodList(): List<String>
fun getHighlightingBounds(): Rectangle?
}
@@ -93,6 +93,12 @@ public final class ComponentPropertiesCollector {
return collector.myProperties;
}
public static @NotNull List<PropertyBean> collect(@NotNull Object propertiesHolder, @NotNull List<@NotNull String> methodList) {
ComponentPropertiesCollector collector = new ComponentPropertiesCollector();
collector.addProperties("", propertiesHolder, methodList);
return collector.myProperties;
}
private final List<PropertyBean> myProperties = new ArrayList<>();
private ComponentPropertiesCollector() { }
@@ -81,6 +81,8 @@ public final class UiInspectorAction extends UiMouseAction implements LightEditC
}
public static final class UiInspector implements Disposable {
private MouseEvent myMouseEvent;
UiInspector(@Nullable Project project) {
if (project != null) {
Disposer.register(project, this);
@@ -97,7 +99,7 @@ public final class UiInspectorAction extends UiMouseAction implements LightEditC
}
public void showInspector(@Nullable Project project, @NotNull Component c) {
InspectorWindow window = new InspectorWindow(project, c, this);
InspectorWindow window = new InspectorWindow(project, c, this, myMouseEvent);
Disposer.register(this, window);
if (DimensionService.getInstance().getSize(InspectorWindow.getDimensionServiceKey(), null) == null) {
window.pack();
@@ -107,6 +109,7 @@ public final class UiInspectorAction extends UiMouseAction implements LightEditC
}
private void processMouseEvent(Project project, MouseEvent me) {
myMouseEvent = me;
me.consume();
Component component = me.getComponent();
@@ -3,12 +3,12 @@ package com.intellij.internal.inspector.components;
import com.intellij.icons.AllIcons;
import com.intellij.ide.impl.DataManagerImpl;
import com.intellij.internal.inspector.PropertyBean;
import com.intellij.internal.inspector.UiInspectorAction;
import com.intellij.internal.inspector.UiInspectorUtil;
import com.intellij.internal.inspector.*;
import com.intellij.internal.inspector.accessibilityAudit.AccessibilityAuditManager;
import com.intellij.internal.inspector.accessibilityAudit.Severity;
import com.intellij.internal.inspector.accessibilityAudit.UiInspectorAccessibilityInspection;
import com.intellij.openapi.actionSystem.DataProvider;
import com.intellij.openapi.actionSystem.UiDataProvider;
import com.intellij.internal.inspector.accessibilityAudit.*;
import com.intellij.openapi.ui.DialogPanel;
import com.intellij.openapi.util.IconLoader;
import com.intellij.openapi.util.Pair;
@@ -35,6 +35,7 @@ import javax.swing.event.TreeSelectionEvent;
import javax.swing.event.TreeSelectionListener;
import javax.swing.tree.*;
import java.awt.*;
import java.awt.event.MouseEvent;
import java.lang.reflect.Field;
import java.lang.reflect.InaccessibleObjectException;
import java.util.List;
@@ -96,6 +97,47 @@ public abstract class HierarchyTree extends JTree implements TreeSelectionListen
selectPath(component, false);
}
public void selectPath(@NotNull Component component, @Nullable MouseEvent event) {
if (event == null ||
!(component instanceof JComponent) ||
((JComponent)component).getClientProperty(UiInspectorCustomComponentProvider.KEY) == null) {
selectPath(component);
return;
}
int count = getRowCount();
for (int i = 0; i < count; i++) {
TreePath row = getPathForRow(i);
Object last = row.getLastPathComponent();
if (last instanceof ComponentNode node && node.myComponent == component) {
TreePath path = findNode(node, SwingUtilities.convertPoint(event.getComponent(), event.getPoint(), component));
setSelectionPath(path == null ? row : path);
scrollPathToVisible(getSelectionPath());
return;
}
}
}
private static @Nullable TreePath findNode(@NotNull ComponentNode node, @NotNull Point location) {
int count = node.getChildCount();
for (int i = 0; i < count; i++) {
ComponentNode childNode = (ComponentNode)node.getChildAt(i);
TreePath result = findNode(childNode, location);
if (result != null) {
return result;
}
UiInspectorCustomComponentChildProvider childProvider = (UiInspectorCustomComponentChildProvider)childNode.getUserObject();
Rectangle bounds = childProvider.getHighlightingBounds();
if (bounds != null && bounds.contains(location)) {
return new TreePath(childNode.getPath());
}
}
return null;
}
public void selectPath(@NotNull Component component, boolean isAccessibleTree) {
int count = getRowCount();
for (int i = 0; i < count; i++) {
@@ -120,6 +162,18 @@ public abstract class HierarchyTree extends JTree implements TreeSelectionListen
return;
}
List<UiInspectorCustomComponentChildProvider> providers = ContainerUtil.mapNotNull(paths, path -> {
Object component = path.getLastPathComponent();
if (component instanceof ComponentNode node && node.getUserObject() instanceof UiInspectorCustomComponentChildProvider provider) {
return provider;
}
return null;
});
if (!providers.isEmpty()) {
onCustomComponentChanged(providers.get(0));
return;
}
List<List<PropertyBean>> clickInfos = ContainerUtil.mapNotNull(paths, path -> {
Object node = path.getLastPathComponent();
if (node instanceof ComponentNode) {
@@ -149,6 +203,8 @@ public abstract class HierarchyTree extends JTree implements TreeSelectionListen
public abstract void onComponentsChanged(List<? extends Component> components);
public abstract void onCustomComponentChanged(UiInspectorCustomComponentChildProvider provider);
public static final class ComponentNode extends DefaultMutableTreeNode {
private final Component myComponent;
private final Accessible myAccessible;
@@ -170,6 +226,21 @@ public abstract class HierarchyTree extends JTree implements TreeSelectionListen
return createComponentNode(component, false);
}
public static ComponentNode createComponentNode(@NotNull UiInspectorCustomComponentChildProvider provider) {
ComponentNode node = new ComponentNode(null, null, provider.getTreeName(), false);
node.setUserObject(provider);
List<TreeNode> result = new ArrayList<>();
for (var child : provider.getChildren()) {
result.add(createComponentNode(child));
}
TreeUtil.addChildrenTo(node, result);
return node;
}
public static ComponentNode createComponentNode(@NotNull Component component, boolean isAccessibleComponent) {
String name = component.getClass().getName();
Accessible accessible = ObjectUtils.tryCast(component, Accessible.class);
@@ -265,6 +336,14 @@ public abstract class HierarchyTree extends JTree implements TreeSelectionListen
result.add(createComponentNode(child));
}
}
if (parent instanceof JComponent jComponent) {
Object clientProperty = jComponent.getClientProperty(UiInspectorCustomComponentProvider.KEY);
if (clientProperty instanceof UiInspectorCustomComponentProvider provider) {
for (var child : provider.getChildren()) {
result.add(createComponentNode(child));
}
}
}
return result;
}
@@ -295,7 +374,8 @@ public abstract class HierarchyTree extends JTree implements TreeSelectionListen
accessibilityAuditIcons.clear();
if (value instanceof ComponentNode componentNode) {
isRenderer = componentNode.getUserObject() instanceof List<?>;
isRenderer = componentNode.getUserObject() instanceof List<?> ||
componentNode.getUserObject() instanceof UiInspectorCustomComponentChildProvider;
Component component = componentNode.getComponent();
if (component != null && !selected) {
@@ -86,6 +86,13 @@ final class InspectorTable extends JBSplitter implements UiDataProvider, Disposa
init(component);
}
InspectorTable(final @NotNull UiInspectorCustomComponentChildProvider provider, @Nullable Project project) {
super(true, 0.75f);
myProject = project;
myModel = new MyModel(provider);
init(null);
}
private void init(@Nullable Component component) {
setSplitterProportionKey("UiInspector.table.splitter.proportion");
@@ -221,6 +228,17 @@ final class InspectorTable extends JBSplitter implements UiDataProvider, Disposa
myProperties.addAll(ComponentPropertiesCollector.collect(c));
}
MyModel(@NotNull UiInspectorCustomComponentChildProvider provider) {
myComponent = null;
Object propertiesHolder = provider.getObjectForProperties();
if (propertiesHolder != null) {
myProperties.addAll(ComponentPropertiesCollector.collect(propertiesHolder, provider.getPropertiesMethodList()));
}
myProperties.addAll(provider.getUiInspectorContext());
}
@Override
public @Nullable Object getValueAt(int row, int column) {
final PropertyBean bean = myProperties.get(row);
@@ -8,6 +8,7 @@ import com.intellij.idea.ActionsBundle;
import com.intellij.internal.InternalActionsBundle;
import com.intellij.internal.inspector.PropertyBean;
import com.intellij.internal.inspector.UiInspectorAction;
import com.intellij.internal.inspector.UiInspectorCustomComponentChildProvider;
import com.intellij.internal.inspector.UiInspectorImpl;
import com.intellij.openapi.Disposable;
import com.intellij.openapi.actionSystem.*;
@@ -45,11 +46,9 @@ import javax.accessibility.AccessibleContext;
import javax.swing.*;
import javax.swing.tree.TreePath;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.KeyEvent;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.awt.event.*;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.function.Consumer;
import java.util.function.Supplier;
@@ -73,7 +72,8 @@ public final class InspectorWindow extends JDialog implements Disposable {
public InspectorWindow(@Nullable Project project,
@NotNull Component component,
UiInspectorAction.UiInspector inspector) throws HeadlessException {
@NotNull UiInspectorAction.UiInspector inspector,
@Nullable MouseEvent event) throws HeadlessException {
super(findWindow(component));
myProject = project;
myInspector = inspector;
@@ -107,6 +107,12 @@ public final class InspectorWindow extends JDialog implements Disposable {
switchClickInfo(info);
updateHighlighting();
}
@Override
public void onCustomComponentChanged(UiInspectorCustomComponentChildProvider provider) {
switchCustomComponentInfo(provider);
updateHighlighting();
}
};
Splitter splitPane = new JBSplitter(false, "UiInspector.splitter.proportion", 0.5f);
splitPane.setSecondComponent(myWrapperPanel);
@@ -170,7 +176,7 @@ public final class InspectorWindow extends JDialog implements Disposable {
add(topPanel, BorderLayout.NORTH);
TreeUtil.expandAll(myHierarchyTree);
myHierarchyTree.selectPath(component);
myHierarchyTree.selectPath(component, event);
addWindowListener(new WindowAdapter() {
@Override
@@ -234,6 +240,24 @@ public final class InspectorWindow extends JDialog implements Disposable {
myWrapperPanel.setContent(myInspectorTable);
}
private void switchCustomComponentInfo(@NotNull UiInspectorCustomComponentChildProvider provider) {
Rectangle bounds = provider.getHighlightingBounds();
if (bounds == null) {
myInfo = null;
}
else {
myInfo = Collections.singletonList(new PropertyBean(UiInspectorAction.RENDERER_BOUNDS, bounds));
}
myComponents.clear();
setTitle(provider.getTreeName());
Disposer.dispose(myInspectorTable);
myInspectorTable = new InspectorTable(provider, myProject);
myWrapperPanel.setContent(myInspectorTable);
}
@Override
public void dispose() {
DimensionService.getInstance().setSize(getDimensionServiceKey(), getSize(), null);