mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-09-27 10:03:11 +07:00
IJPL-158395 Provide UI Inspector for Compose-based UIs
GitOrigin-RevId: cbc1657aed4fd464a2ff8cc740104c8a9af5f285
This commit is contained in:
committed by
intellij-monorepo-bot
parent
d588e586de
commit
8133c2d2cc
@@ -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
|
||||
|
||||
+24
@@ -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?
|
||||
}
|
||||
+6
@@ -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() { }
|
||||
|
||||
+4
-1
@@ -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();
|
||||
|
||||
|
||||
+85
-5
@@ -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) {
|
||||
|
||||
+18
@@ -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);
|
||||
|
||||
+30
-6
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user