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"/>
