diff --git a/platform/compose/src/com/intellij/platform/compose/JBComposePanel.kt b/platform/compose/src/com/intellij/platform/compose/JBComposePanel.kt index c9b584fb2e59..f14de804d3f2 100644 --- a/platform/compose/src/com/intellij/platform/compose/JBComposePanel.kt +++ b/platform/compose/src/com/intellij/platform/compose/JBComposePanel.kt @@ -30,6 +30,7 @@ fun JBComposePanel( } return ComposePanel(ComposeAnalytics()).apply { setContent { + ComposeUiInspector(this) SwingBridgeTheme { CompositionLocalProvider(LocalComponent provides this@apply) { ComponentDataProviderBridge(this@apply) { diff --git a/platform/compose/src/com/intellij/platform/compose/UiInspectorInfo.kt b/platform/compose/src/com/intellij/platform/compose/UiInspectorInfo.kt new file mode 100644 index 000000000000..63f2d6b4811d --- /dev/null +++ b/platform/compose/src/com/intellij/platform/compose/UiInspectorInfo.kt @@ -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(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 { + val context = composePanel.accessibleContext ?: return emptyList() + val count = context.accessibleChildrenCount + val composeChildren = mutableListOf() + + 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("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 { + val count = component.accessibleChildrenCount + val composeChildren = mutableListOf() + + 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() + + override fun getHighlightingBounds(): Rectangle? { + if (component is AccessibleComponent) { + return component.bounds + } + return null + } +} \ No newline at end of file diff --git a/platform/platform-api/api-dump-unreviewed.txt b/platform/platform-api/api-dump-unreviewed.txt index 92f2d64c0339..795b7edb2c66 100644 --- a/platform/platform-api/api-dump-unreviewed.txt +++ b/platform/platform-api/api-dump-unreviewed.txt @@ -1326,6 +1326,19 @@ c:com.intellij.internal.inspector.PropertyBean - (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 diff --git a/platform/platform-api/src/com/intellij/internal/inspector/UiInspectorCustomProvider.kt b/platform/platform-api/src/com/intellij/internal/inspector/UiInspectorCustomProvider.kt new file mode 100644 index 000000000000..61efddcf8d53 --- /dev/null +++ b/platform/platform-api/src/com/intellij/internal/inspector/UiInspectorCustomProvider.kt @@ -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 +} + +interface UiInspectorCustomComponentChildProvider : UiInspectorContextProvider { + fun getTreeName(): String + + fun getChildren(): List + + fun getObjectForProperties(): Any? + + fun getPropertiesMethodList(): List + + fun getHighlightingBounds(): Rectangle? +} \ No newline at end of file diff --git a/platform/platform-impl/ui-inspector/src/com/intellij/internal/inspector/ComponentPropertiesCollector.java b/platform/platform-impl/ui-inspector/src/com/intellij/internal/inspector/ComponentPropertiesCollector.java index 6d2cd1d656b1..5b65f0dc1f9b 100644 --- a/platform/platform-impl/ui-inspector/src/com/intellij/internal/inspector/ComponentPropertiesCollector.java +++ b/platform/platform-impl/ui-inspector/src/com/intellij/internal/inspector/ComponentPropertiesCollector.java @@ -93,6 +93,12 @@ public final class ComponentPropertiesCollector { return collector.myProperties; } + public static @NotNull List collect(@NotNull Object propertiesHolder, @NotNull List<@NotNull String> methodList) { + ComponentPropertiesCollector collector = new ComponentPropertiesCollector(); + collector.addProperties("", propertiesHolder, methodList); + return collector.myProperties; + } + private final List myProperties = new ArrayList<>(); private ComponentPropertiesCollector() { } diff --git a/platform/platform-impl/ui-inspector/src/com/intellij/internal/inspector/UiInspectorAction.java b/platform/platform-impl/ui-inspector/src/com/intellij/internal/inspector/UiInspectorAction.java index f19ddd96a73b..f8074147bcf3 100644 --- a/platform/platform-impl/ui-inspector/src/com/intellij/internal/inspector/UiInspectorAction.java +++ b/platform/platform-impl/ui-inspector/src/com/intellij/internal/inspector/UiInspectorAction.java @@ -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(); diff --git a/platform/platform-impl/ui-inspector/src/com/intellij/internal/inspector/components/HierarchyTree.java b/platform/platform-impl/ui-inspector/src/com/intellij/internal/inspector/components/HierarchyTree.java index 5f3b2eea9c6c..b9d092911688 100644 --- a/platform/platform-impl/ui-inspector/src/com/intellij/internal/inspector/components/HierarchyTree.java +++ b/platform/platform-impl/ui-inspector/src/com/intellij/internal/inspector/components/HierarchyTree.java @@ -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 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> 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 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 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) { diff --git a/platform/platform-impl/ui-inspector/src/com/intellij/internal/inspector/components/InspectorTable.java b/platform/platform-impl/ui-inspector/src/com/intellij/internal/inspector/components/InspectorTable.java index 63ee4632e568..4e379f39dd5d 100644 --- a/platform/platform-impl/ui-inspector/src/com/intellij/internal/inspector/components/InspectorTable.java +++ b/platform/platform-impl/ui-inspector/src/com/intellij/internal/inspector/components/InspectorTable.java @@ -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); diff --git a/platform/platform-impl/ui-inspector/src/com/intellij/internal/inspector/components/InspectorWindow.java b/platform/platform-impl/ui-inspector/src/com/intellij/internal/inspector/components/InspectorWindow.java index defe7ac2afc6..b4bb7e34acbc 100644 --- a/platform/platform-impl/ui-inspector/src/com/intellij/internal/inspector/components/InspectorWindow.java +++ b/platform/platform-impl/ui-inspector/src/com/intellij/internal/inspector/components/InspectorWindow.java @@ -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);