This is an automated email from the ASF dual-hosted git repository.

lukaszlenart pushed a commit to branch feat/struts-diagram-tab
in repository https://gitbox.apache.org/repos/asf/struts-intellij-plugin.git

commit 003ded1d683f2083120f5eaabb0201355a59c794
Author: Lukasz Lenart <[email protected]>
AuthorDate: Sun Apr 5 10:42:31 2026 +0200

    feat: add read-only Diagram tab for struts.xml config visualization
    
    Introduce a lightweight Swing-based Diagram tab alongside the existing
    (disabled-by-default) Graph tab. The new tab renders packages, actions,
    and results in a hierarchical layout with tooltips and click-to-navigate,
    without depending on the deprecated GraphBuilder APIs.
    
    New components:
    - diagram/model: toolkit-neutral DTOs (node, edge, snapshot builder)
    - diagram/presentation: reusable tooltip/navigation helpers
    - diagram/ui: custom Swing renderer with hover and double-click support
    - diagram/fileEditor: PerspectiveFileEditorProvider + editor registration
    
    Made-with: Cursor
---
 CHANGELOG.md                                       |   4 +
 .../fileEditor/Struts2DiagramFileEditor.java       |  91 ++++++++
 .../Struts2DiagramFileEditorProvider.java          |  92 ++++++++
 .../diagram/model/StrutsConfigDiagramModel.java    |  98 ++++++++
 .../struts2/diagram/model/StrutsDiagramEdge.java   |  55 +++++
 .../struts2/diagram/model/StrutsDiagramNode.java   |  72 ++++++
 .../presentation/StrutsDiagramPresentation.java    | 102 +++++++++
 .../diagram/ui/Struts2DiagramComponent.java        | 246 +++++++++++++++++++++
 src/main/resources/META-INF/plugin.xml             |   1 +
 9 files changed, 761 insertions(+)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 73c9120..24e2f36 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,10 @@
 
 ## [Unreleased]
 
+### Added
+
+- Add read-only Diagram tab for struts.xml files with lightweight config 
visualization (packages, actions, results)
+
 ### Changed
 
 - Disable the deprecated Graph editor tab by default; opt in with JVM property 
`-Dcom.intellij.struts2.enableGraphEditor=true`
diff --git 
a/src/main/java/com/intellij/struts2/diagram/fileEditor/Struts2DiagramFileEditor.java
 
b/src/main/java/com/intellij/struts2/diagram/fileEditor/Struts2DiagramFileEditor.java
new file mode 100644
index 0000000..d04fd0a
--- /dev/null
+++ 
b/src/main/java/com/intellij/struts2/diagram/fileEditor/Struts2DiagramFileEditor.java
@@ -0,0 +1,91 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.intellij.struts2.diagram.fileEditor;
+
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.psi.PsiFile;
+import com.intellij.psi.xml.XmlFile;
+import com.intellij.struts2.diagram.model.StrutsConfigDiagramModel;
+import com.intellij.struts2.diagram.ui.Struts2DiagramComponent;
+import com.intellij.util.xml.DomElement;
+import com.intellij.util.xml.ui.PerspectiveFileEditor;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import javax.swing.*;
+
+/**
+ * Read-only file editor that hosts the lightweight Struts config diagram.
+ */
+public class Struts2DiagramFileEditor extends PerspectiveFileEditor {
+
+    private final XmlFile myXmlFile;
+    private Struts2DiagramComponent myComponent;
+
+    public Struts2DiagramFileEditor(final Project project, final VirtualFile 
file) {
+        super(project, file);
+
+        final PsiFile psiFile = getPsiFile();
+        assert psiFile instanceof XmlFile;
+        myXmlFile = (XmlFile) psiFile;
+    }
+
+    @Override
+    @Nullable
+    protected DomElement getSelectedDomElement() {
+        return null;
+    }
+
+    @Override
+    protected void setSelectedDomElement(final DomElement domElement) {
+    }
+
+    @Override
+    @NotNull
+    protected JComponent createCustomComponent() {
+        return getDiagramComponent();
+    }
+
+    @Override
+    @Nullable
+    public JComponent getPreferredFocusedComponent() {
+        return getDiagramComponent();
+    }
+
+    @Override
+    public void commit() {
+    }
+
+    @Override
+    public void reset() {
+        
getDiagramComponent().rebuild(StrutsConfigDiagramModel.build(myXmlFile));
+    }
+
+    @Override
+    @NotNull
+    public String getName() {
+        return "Diagram";
+    }
+
+    private Struts2DiagramComponent getDiagramComponent() {
+        if (myComponent == null) {
+            myComponent = new 
Struts2DiagramComponent(StrutsConfigDiagramModel.build(myXmlFile));
+        }
+        return myComponent;
+    }
+}
diff --git 
a/src/main/java/com/intellij/struts2/diagram/fileEditor/Struts2DiagramFileEditorProvider.java
 
b/src/main/java/com/intellij/struts2/diagram/fileEditor/Struts2DiagramFileEditorProvider.java
new file mode 100644
index 0000000..44bd789
--- /dev/null
+++ 
b/src/main/java/com/intellij/struts2/diagram/fileEditor/Struts2DiagramFileEditorProvider.java
@@ -0,0 +1,92 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.intellij.struts2.diagram.fileEditor;
+
+import com.intellij.openapi.module.Module;
+import com.intellij.openapi.module.ModuleUtilCore;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.psi.PsiFile;
+import com.intellij.psi.PsiManager;
+import com.intellij.psi.jsp.JspFile;
+import com.intellij.psi.xml.XmlFile;
+import com.intellij.struts2.dom.struts.model.StrutsManager;
+import com.intellij.struts2.facet.ui.StrutsFileSet;
+import com.intellij.util.xml.ui.PerspectiveFileEditor;
+import com.intellij.util.xml.ui.PerspectiveFileEditorProvider;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.Set;
+
+/**
+ * Provides the read-only "Diagram" tab for struts.xml files registered in a 
Struts file set.
+ * Uses the same eligibility rules as the legacy Graph tab but does not depend 
on
+ * deprecated {@code GraphBuilder} APIs.
+ */
+public class Struts2DiagramFileEditorProvider extends 
PerspectiveFileEditorProvider {
+
+    @Override
+    public boolean accept(@NotNull final Project project, @NotNull final 
VirtualFile file) {
+        if (!file.isValid()) {
+            return false;
+        }
+
+        final PsiFile psiFile = PsiManager.getInstance(project).findFile(file);
+
+        if (!(psiFile instanceof XmlFile)) {
+            return false;
+        }
+
+        if (psiFile instanceof JspFile) {
+            return false;
+        }
+
+        if (!StrutsManager.getInstance(project).isStruts2ConfigFile((XmlFile) 
psiFile)) {
+            return false;
+        }
+
+        final Module module = ModuleUtilCore.findModuleForFile(file, project);
+        if (module == null) {
+            return false;
+        }
+
+        final Set<StrutsFileSet> fileSets = 
StrutsManager.getInstance(project).getAllConfigFileSets(module);
+        for (final StrutsFileSet fileSet : fileSets) {
+            if (fileSet.hasFile(file)) {
+                return true;
+            }
+        }
+
+        return false;
+    }
+
+    @Override
+    @NotNull
+    public PerspectiveFileEditor createEditor(@NotNull final Project project, 
@NotNull final VirtualFile file) {
+        return new Struts2DiagramFileEditor(project, file);
+    }
+
+    @Override
+    public boolean isDumbAware() {
+        return false;
+    }
+
+    @Override
+    public double getWeight() {
+        return 0;
+    }
+}
diff --git 
a/src/main/java/com/intellij/struts2/diagram/model/StrutsConfigDiagramModel.java
 
b/src/main/java/com/intellij/struts2/diagram/model/StrutsConfigDiagramModel.java
new file mode 100644
index 0000000..a814327
--- /dev/null
+++ 
b/src/main/java/com/intellij/struts2/diagram/model/StrutsConfigDiagramModel.java
@@ -0,0 +1,98 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.intellij.struts2.diagram.model;
+
+import com.intellij.icons.AllIcons;
+import com.intellij.openapi.paths.PathReference;
+import com.intellij.psi.xml.XmlFile;
+import com.intellij.struts2.Struts2Icons;
+import com.intellij.struts2.dom.struts.action.Action;
+import com.intellij.struts2.dom.struts.action.Result;
+import com.intellij.struts2.dom.struts.model.StrutsManager;
+import com.intellij.struts2.dom.struts.model.StrutsModel;
+import com.intellij.struts2.dom.struts.strutspackage.StrutsPackage;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import javax.swing.*;
+import java.util.*;
+
+/**
+ * Builds a toolkit-neutral snapshot of the Struts configuration for diagram 
rendering.
+ * Walks {@link StrutsModel} to produce package, action, and result nodes with 
directed
+ * edges (package->action, action->result).
+ */
+public final class StrutsConfigDiagramModel {
+
+    private static final String UNKNOWN = "???";
+
+    private final List<StrutsDiagramNode> nodes = new ArrayList<>();
+    private final List<StrutsDiagramEdge> edges = new ArrayList<>();
+
+    private StrutsConfigDiagramModel() {}
+
+    public @NotNull List<StrutsDiagramNode> getNodes() { return 
Collections.unmodifiableList(nodes); }
+    public @NotNull List<StrutsDiagramEdge> getEdges() { return 
Collections.unmodifiableList(edges); }
+
+    /**
+     * Build a snapshot from the Struts model associated with the given XML 
file.
+     *
+     * @return populated model, or {@code null} if no Struts model is 
available.
+     */
+    public static @Nullable StrutsConfigDiagramModel build(@NotNull XmlFile 
xmlFile) {
+        StrutsModel strutsModel = 
StrutsManager.getInstance(xmlFile.getProject()).getModelByFile(xmlFile);
+        if (strutsModel == null) return null;
+
+        StrutsConfigDiagramModel model = new StrutsConfigDiagramModel();
+        for (StrutsPackage strutsPackage : strutsModel.getStrutsPackages()) {
+            String pkgName = 
Objects.toString(strutsPackage.getName().getStringValue(), UNKNOWN);
+            StrutsDiagramNode pkgNode = new StrutsDiagramNode(
+                    StrutsDiagramNode.Kind.PACKAGE, pkgName, strutsPackage, 
AllIcons.Nodes.Package);
+            model.nodes.add(pkgNode);
+
+            for (Action action : strutsPackage.getActions()) {
+                String actionName = 
Objects.toString(action.getName().getStringValue(), UNKNOWN);
+                StrutsDiagramNode actionNode = new StrutsDiagramNode(
+                        StrutsDiagramNode.Kind.ACTION, actionName, action, 
Struts2Icons.Action);
+                model.nodes.add(actionNode);
+                model.edges.add(new StrutsDiagramEdge(pkgNode, actionNode, 
""));
+
+                for (Result result : action.getResults()) {
+                    PathReference pathRef = result.getValue();
+                    String path = pathRef != null ? pathRef.getPath() : 
UNKNOWN;
+                    Icon resultIcon = resolveResultIcon(result);
+                    StrutsDiagramNode resultNode = new StrutsDiagramNode(
+                            StrutsDiagramNode.Kind.RESULT, path, result, 
resultIcon);
+                    model.nodes.add(resultNode);
+
+                    String resultName = result.getName().getStringValue();
+                    model.edges.add(new StrutsDiagramEdge(actionNode, 
resultNode,
+                            resultName != null ? resultName : 
Result.DEFAULT_NAME));
+                }
+            }
+        }
+        return model;
+    }
+
+    private static @NotNull Icon resolveResultIcon(@NotNull Result result) {
+        if (!result.isValid()) return AllIcons.FileTypes.Unknown;
+        PathReference ref = result.getValue();
+        if (ref == null || ref.resolve() == null) return 
AllIcons.FileTypes.Unknown;
+        Icon icon = ref.getIcon();
+        return icon != null ? icon : AllIcons.FileTypes.Unknown;
+    }
+}
diff --git 
a/src/main/java/com/intellij/struts2/diagram/model/StrutsDiagramEdge.java 
b/src/main/java/com/intellij/struts2/diagram/model/StrutsDiagramEdge.java
new file mode 100644
index 0000000..6460c1b
--- /dev/null
+++ b/src/main/java/com/intellij/struts2/diagram/model/StrutsDiagramEdge.java
@@ -0,0 +1,55 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.intellij.struts2.diagram.model;
+
+import org.jetbrains.annotations.NotNull;
+
+import java.util.Objects;
+
+/**
+ * Toolkit-neutral directed edge between two {@link StrutsDiagramNode}s.
+ */
+public final class StrutsDiagramEdge {
+
+    private final @NotNull StrutsDiagramNode source;
+    private final @NotNull StrutsDiagramNode target;
+    private final @NotNull String label;
+
+    public StrutsDiagramEdge(@NotNull StrutsDiagramNode source,
+                             @NotNull StrutsDiagramNode target,
+                             @NotNull String label) {
+        this.source = source;
+        this.target = target;
+        this.label = label;
+    }
+
+    public @NotNull StrutsDiagramNode getSource() { return source; }
+    public @NotNull StrutsDiagramNode getTarget() { return target; }
+    public @NotNull String getLabel() { return label; }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (!(o instanceof StrutsDiagramEdge that)) return false;
+        return source.equals(that.source) && target.equals(that.target);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(source, target);
+    }
+}
diff --git 
a/src/main/java/com/intellij/struts2/diagram/model/StrutsDiagramNode.java 
b/src/main/java/com/intellij/struts2/diagram/model/StrutsDiagramNode.java
new file mode 100644
index 0000000..709e861
--- /dev/null
+++ b/src/main/java/com/intellij/struts2/diagram/model/StrutsDiagramNode.java
@@ -0,0 +1,72 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.intellij.struts2.diagram.model;
+
+import com.intellij.util.xml.DomElement;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import javax.swing.*;
+import java.util.Objects;
+
+/**
+ * Toolkit-neutral node representing a Struts config element (package, action, 
or result).
+ * Intentionally free of any {@code com.intellij.openapi.graph} dependencies 
so it can
+ * serve as the data layer for both the current lightweight Swing renderer and 
a future
+ * {@code com.intellij.diagram.Provider} migration.
+ */
+public final class StrutsDiagramNode {
+
+    public enum Kind { PACKAGE, ACTION, RESULT }
+
+    private final @NotNull Kind kind;
+    private final @NotNull String name;
+    private final @NotNull DomElement domElement;
+    private final @Nullable Icon icon;
+
+    public StrutsDiagramNode(@NotNull Kind kind,
+                             @NotNull String name,
+                             @NotNull DomElement domElement,
+                             @Nullable Icon icon) {
+        this.kind = kind;
+        this.name = name;
+        this.domElement = domElement;
+        this.icon = icon;
+    }
+
+    public @NotNull Kind getKind() { return kind; }
+    public @NotNull String getName() { return name; }
+    public @NotNull DomElement getDomElement() { return domElement; }
+    public @Nullable Icon getIcon() { return icon; }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (!(o instanceof StrutsDiagramNode that)) return false;
+        return kind == that.kind && name.equals(that.name) && 
domElement.equals(that.domElement);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(kind, name, domElement);
+    }
+
+    @Override
+    public String toString() {
+        return kind + ":" + name;
+    }
+}
diff --git 
a/src/main/java/com/intellij/struts2/diagram/presentation/StrutsDiagramPresentation.java
 
b/src/main/java/com/intellij/struts2/diagram/presentation/StrutsDiagramPresentation.java
new file mode 100644
index 0000000..ff13618
--- /dev/null
+++ 
b/src/main/java/com/intellij/struts2/diagram/presentation/StrutsDiagramPresentation.java
@@ -0,0 +1,102 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.intellij.struts2.diagram.presentation;
+
+import com.intellij.openapi.paths.PathReference;
+import com.intellij.openapi.util.text.StringUtil;
+import com.intellij.pom.Navigatable;
+import com.intellij.psi.PsiClass;
+import com.intellij.psi.xml.XmlElement;
+import com.intellij.struts2.diagram.model.StrutsDiagramNode;
+import com.intellij.struts2.dom.struts.action.Action;
+import com.intellij.struts2.dom.struts.action.Result;
+import com.intellij.struts2.dom.struts.strutspackage.ResultType;
+import com.intellij.struts2.dom.struts.strutspackage.StrutsPackage;
+import com.intellij.util.OpenSourceUtil;
+import com.intellij.util.xml.DomElement;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * Toolkit-neutral presentation helpers for Struts diagram nodes: tooltips, 
navigation,
+ * and labels. Intentionally free of any {@code com.intellij.openapi.graph} 
dependencies
+ * so it can serve both the current lightweight Swing renderer and a future
+ * {@code com.intellij.diagram.Provider} migration.
+ */
+public final class StrutsDiagramPresentation {
+
+    private StrutsDiagramPresentation() {}
+
+    public static @Nullable String getTooltipHtml(@NotNull StrutsDiagramNode 
node) {
+        DomElement element = node.getDomElement();
+
+        if (element instanceof StrutsPackage pkg) {
+            return new HtmlTableBuilder()
+                    .addLine("Package", pkg.getName().getStringValue())
+                    .addLine("Namespace", pkg.searchNamespace())
+                    .addLine("Extends", pkg.getExtends().getStringValue())
+                    .build();
+        }
+
+        if (element instanceof Action action) {
+            StrutsPackage strutsPackage = action.getStrutsPackage();
+            PsiClass actionClass = action.searchActionClass();
+            return new HtmlTableBuilder()
+                    .addLine("Action", action.getName().getStringValue())
+                    .addLine("Class", actionClass != null ? 
actionClass.getQualifiedName() : null)
+                    .addLine("Method", action.getMethod().getStringValue())
+                    .addLine("Package", 
strutsPackage.getName().getStringValue())
+                    .addLine("Namespace", strutsPackage.searchNamespace())
+                    .build();
+        }
+
+        if (element instanceof Result result) {
+            PathReference ref = result.getValue();
+            String displayPath = ref != null ? ref.getPath() : "???";
+            ResultType resultType = result.getEffectiveResultType();
+            String resultTypeValue = resultType != null ? 
resultType.getName().getStringValue() : "???";
+            return new HtmlTableBuilder()
+                    .addLine("Path", displayPath)
+                    .addLine("Type", resultTypeValue)
+                    .build();
+        }
+
+        return null;
+    }
+
+    public static void navigateToElement(@NotNull StrutsDiagramNode node) {
+        XmlElement xmlElement = node.getDomElement().getXmlElement();
+        if (xmlElement instanceof Navigatable) {
+            OpenSourceUtil.navigate((Navigatable) xmlElement);
+        }
+    }
+
+    private static final class HtmlTableBuilder {
+        private final StringBuilder sb = new StringBuilder("<html><table>");
+
+        HtmlTableBuilder addLine(@NotNull String label, @Nullable String 
content) {
+            
sb.append("<tr><td><strong>").append(label).append(":</strong></td>")
+                    .append("<td>").append(StringUtil.isNotEmpty(content) ? 
content : "-").append("</td></tr>");
+            return this;
+        }
+
+        String build() {
+            sb.append("</table></html>");
+            return sb.toString();
+        }
+    }
+}
diff --git 
a/src/main/java/com/intellij/struts2/diagram/ui/Struts2DiagramComponent.java 
b/src/main/java/com/intellij/struts2/diagram/ui/Struts2DiagramComponent.java
new file mode 100644
index 0000000..3df4eab
--- /dev/null
+++ b/src/main/java/com/intellij/struts2/diagram/ui/Struts2DiagramComponent.java
@@ -0,0 +1,246 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.intellij.struts2.diagram.ui;
+
+import com.intellij.struts2.diagram.model.StrutsConfigDiagramModel;
+import com.intellij.struts2.diagram.model.StrutsDiagramEdge;
+import com.intellij.struts2.diagram.model.StrutsDiagramNode;
+import com.intellij.struts2.diagram.presentation.StrutsDiagramPresentation;
+import com.intellij.ui.JBColor;
+import com.intellij.util.ui.JBUI;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import javax.swing.*;
+import java.awt.*;
+import java.awt.event.MouseAdapter;
+import java.awt.event.MouseEvent;
+import java.awt.geom.Path2D;
+import java.util.List;
+import java.util.*;
+
+/**
+ * Lightweight read-only Swing panel that renders a Struts config diagram with 
a
+ * deterministic hierarchical layout: packages at left, actions in center, 
results at right.
+ * Supports hover tooltips and click-to-navigate.
+ */
+public final class Struts2DiagramComponent extends JPanel {
+
+    private static final int NODE_WIDTH = 160;
+    private static final int NODE_HEIGHT = 30;
+    private static final int H_GAP = 80;
+    private static final int V_GAP = 16;
+    private static final int PADDING = 24;
+    private static final int ICON_TEXT_GAP = 4;
+    private static final int ARC = 8;
+
+    private final Map<StrutsDiagramNode, Rectangle> nodeBounds = new 
LinkedHashMap<>();
+    private final List<StrutsDiagramEdge> edges = new ArrayList<>();
+    private @Nullable StrutsDiagramNode hoveredNode;
+
+    public Struts2DiagramComponent(@Nullable StrutsConfigDiagramModel model) {
+        setBackground(JBColor.background());
+        if (model != null) {
+            layoutModel(model);
+        }
+
+        MouseAdapter mouseHandler = new MouseAdapter() {
+            @Override
+            public void mouseMoved(MouseEvent e) {
+                StrutsDiagramNode hit = hitTest(e.getPoint());
+                if (!Objects.equals(hit, hoveredNode)) {
+                    hoveredNode = hit;
+                    setToolTipText(hit != null ? 
StrutsDiagramPresentation.getTooltipHtml(hit) : null);
+                    repaint();
+                }
+            }
+
+            @Override
+            public void mouseClicked(MouseEvent e) {
+                StrutsDiagramNode hit = hitTest(e.getPoint());
+                if (hit != null && e.getClickCount() == 2) {
+                    StrutsDiagramPresentation.navigateToElement(hit);
+                }
+            }
+        };
+        addMouseListener(mouseHandler);
+        addMouseMotionListener(mouseHandler);
+    }
+
+    public void rebuild(@Nullable StrutsConfigDiagramModel model) {
+        nodeBounds.clear();
+        edges.clear();
+        if (model != null) {
+            layoutModel(model);
+        }
+        revalidate();
+        repaint();
+    }
+
+    private void layoutModel(@NotNull StrutsConfigDiagramModel model) {
+        edges.addAll(model.getEdges());
+
+        List<StrutsDiagramNode> packages = new ArrayList<>();
+        List<StrutsDiagramNode> actions = new ArrayList<>();
+        List<StrutsDiagramNode> results = new ArrayList<>();
+
+        for (StrutsDiagramNode node : model.getNodes()) {
+            switch (node.getKind()) {
+                case PACKAGE -> packages.add(node);
+                case ACTION -> actions.add(node);
+                case RESULT -> results.add(node);
+            }
+        }
+
+        int colX = PADDING;
+        int maxY = placeColumn(packages, colX, PADDING);
+        colX += NODE_WIDTH + H_GAP;
+        maxY = Math.max(maxY, placeColumn(actions, colX, PADDING));
+        colX += NODE_WIDTH + H_GAP;
+        maxY = Math.max(maxY, placeColumn(results, colX, PADDING));
+
+        int totalWidth = colX + NODE_WIDTH + PADDING;
+        int totalHeight = maxY + PADDING;
+        setPreferredSize(new Dimension(totalWidth, totalHeight));
+    }
+
+    private int placeColumn(List<StrutsDiagramNode> nodes, int x, int startY) {
+        int y = startY;
+        for (StrutsDiagramNode node : nodes) {
+            nodeBounds.put(node, new Rectangle(x, y, NODE_WIDTH, NODE_HEIGHT));
+            y += NODE_HEIGHT + V_GAP;
+        }
+        return y;
+    }
+
+    private @Nullable StrutsDiagramNode hitTest(Point p) {
+        for (Map.Entry<StrutsDiagramNode, Rectangle> entry : 
nodeBounds.entrySet()) {
+            if (entry.getValue().contains(p)) {
+                return entry.getKey();
+            }
+        }
+        return null;
+    }
+
+    @Override
+    protected void paintComponent(Graphics g) {
+        super.paintComponent(g);
+        Graphics2D g2 = (Graphics2D) g.create();
+        try {
+            g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, 
RenderingHints.VALUE_ANTIALIAS_ON);
+            g2.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, 
RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
+
+            paintEdges(g2);
+            paintNodes(g2);
+        } finally {
+            g2.dispose();
+        }
+    }
+
+    private void paintEdges(Graphics2D g2) {
+        g2.setStroke(new BasicStroke(1.2f));
+        for (StrutsDiagramEdge edge : edges) {
+            Rectangle srcRect = nodeBounds.get(edge.getSource());
+            Rectangle tgtRect = nodeBounds.get(edge.getTarget());
+            if (srcRect == null || tgtRect == null) continue;
+
+            int x1 = srcRect.x + srcRect.width;
+            int y1 = srcRect.y + srcRect.height / 2;
+            int x2 = tgtRect.x;
+            int y2 = tgtRect.y + tgtRect.height / 2;
+
+            g2.setColor(JBColor.namedColor("Diagram.edgeColor", JBColor.GRAY));
+            int midX = (x1 + x2) / 2;
+            Path2D path = new Path2D.Float();
+            path.moveTo(x1, y1);
+            path.curveTo(midX, y1, midX, y2, x2, y2);
+            g2.draw(path);
+
+            drawArrowHead(g2, midX, y2, x2, y2);
+
+            String label = edge.getLabel();
+            if (!label.isEmpty()) {
+                g2.setFont(JBUI.Fonts.smallFont());
+                g2.setColor(JBColor.namedColor("Diagram.edgeLabelColor", 
JBColor.DARK_GRAY));
+                FontMetrics fm = g2.getFontMetrics();
+                int labelX = midX - fm.stringWidth(label) / 2;
+                int labelY = (y1 + y2) / 2 - 3;
+                g2.drawString(label, labelX, labelY);
+            }
+        }
+    }
+
+    private static void drawArrowHead(Graphics2D g2, int fromX, int fromY, int 
toX, int toY) {
+        double angle = Math.atan2(toY - fromY, toX - fromX);
+        int arrowLen = 8;
+        int x1 = (int) (toX - arrowLen * Math.cos(angle - Math.PI / 6));
+        int y1 = (int) (toY - arrowLen * Math.sin(angle - Math.PI / 6));
+        int x2 = (int) (toX - arrowLen * Math.cos(angle + Math.PI / 6));
+        int y2 = (int) (toY - arrowLen * Math.sin(angle + Math.PI / 6));
+        g2.fillPolygon(new int[]{toX, x1, x2}, new int[]{toY, y1, y2}, 3);
+    }
+
+    private void paintNodes(Graphics2D g2) {
+        Font nodeFont = JBUI.Fonts.label();
+        g2.setFont(nodeFont);
+        FontMetrics fm = g2.getFontMetrics();
+
+        for (Map.Entry<StrutsDiagramNode, Rectangle> entry : 
nodeBounds.entrySet()) {
+            StrutsDiagramNode node = entry.getKey();
+            Rectangle r = entry.getValue();
+            boolean hovered = Objects.equals(node, hoveredNode);
+
+            Color fill = switch (node.getKind()) {
+                case PACKAGE -> JBColor.namedColor("Diagram.packageNodeFill",
+                        new JBColor(new Color(0xE8F0FE), new Color(0x2B3A4C)));
+                case ACTION -> JBColor.namedColor("Diagram.actionNodeFill",
+                        new JBColor(new Color(0xE6F4EA), new Color(0x1E3A2C)));
+                case RESULT -> JBColor.namedColor("Diagram.resultNodeFill",
+                        new JBColor(new Color(0xFFF3E0), new Color(0x3A2E1E)));
+            };
+            Color border = hovered
+                    ? JBColor.namedColor("Diagram.nodeHoverBorder", 
JBColor.BLUE)
+                    : JBColor.namedColor("Diagram.nodeBorder", JBColor.GRAY);
+
+            g2.setColor(fill);
+            g2.fillRoundRect(r.x, r.y, r.width, r.height, ARC, ARC);
+            g2.setColor(border);
+            g2.setStroke(new BasicStroke(hovered ? 2f : 1f));
+            g2.drawRoundRect(r.x, r.y, r.width, r.height, ARC, ARC);
+
+            Icon icon = node.getIcon();
+            int textX = r.x + 6;
+            if (icon != null) {
+                int iconY = r.y + (r.height - icon.getIconHeight()) / 2;
+                icon.paintIcon(this, g2, textX, iconY);
+                textX += icon.getIconWidth() + ICON_TEXT_GAP;
+            }
+
+            g2.setColor(JBColor.foreground());
+            String label = node.getName();
+            int availableWidth = r.x + r.width - textX - 4;
+            if (fm.stringWidth(label) > availableWidth) {
+                while (label.length() > 1 && fm.stringWidth(label + "...") > 
availableWidth) {
+                    label = label.substring(0, label.length() - 1);
+                }
+                label += "...";
+            }
+            int textY = r.y + (r.height + fm.getAscent() - fm.getDescent()) / 
2;
+            g2.drawString(label, textX, textY);
+        }
+    }
+}
diff --git a/src/main/resources/META-INF/plugin.xml 
b/src/main/resources/META-INF/plugin.xml
index 0c55621..2b4fcb9 100644
--- a/src/main/resources/META-INF/plugin.xml
+++ b/src/main/resources/META-INF/plugin.xml
@@ -168,6 +168,7 @@
                 
implementation="com.intellij.struts2.structure.StrutsStructureViewBuilderProvider"/>
 
         <fileEditorProvider 
implementation="com.intellij.struts2.graph.fileEditor.Struts2GraphFileEditorProvider"/>
+        <fileEditorProvider 
implementation="com.intellij.struts2.diagram.fileEditor.Struts2DiagramFileEditorProvider"/>
 
         <struts2.resultContributor
                 
implementation="com.intellij.struts2.dom.struts.impl.path.DispatchPathResultContributor"/>


Reply via email to