1+ /*
2+ * Minecraft Development for IntelliJ
3+ *
4+ * https://mcdev.io/
5+ *
6+ * Copyright (C) 2025 minecraft-dev
7+ *
8+ * This program is free software: you can redistribute it and/or modify
9+ * it under the terms of the GNU Lesser General Public License as published
10+ * by the Free Software Foundation, version 3.0 only.
11+ *
12+ * This program is distributed in the hope that it will be useful,
13+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
14+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15+ * GNU General Public License for more details.
16+ *
17+ * You should have received a copy of the GNU Lesser General Public License
18+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
19+ */
20+
21+ package com.demonwav.mcdev.platform.mixin.expression.gui
22+
23+ import com.intellij.openapi.application.EDT
24+ import com.intellij.openapi.editor.colors.EditorColorsManager
25+ import com.intellij.openapi.editor.colors.EditorFontType
26+ import com.intellij.openapi.progress.checkCanceled
27+ import com.intellij.openapi.project.Project
28+ import com.intellij.util.ui.JBUI
29+ import com.intellij.util.ui.UIUtil
30+ import com.mxgraph.layout.hierarchical.mxHierarchicalLayout
31+ import com.mxgraph.model.mxCell
32+ import com.mxgraph.swing.mxGraphComponent
33+ import com.mxgraph.util.mxConstants
34+ import com.mxgraph.util.mxEvent
35+ import com.mxgraph.util.mxRectangle
36+ import com.mxgraph.view.mxGraph
37+ import java.awt.BorderLayout
38+ import java.awt.Color
39+ import java.awt.Dimension
40+ import java.awt.Rectangle
41+ import java.util.*
42+ import javax.swing.JButton
43+ import javax.swing.JLabel
44+ import javax.swing.JPanel
45+ import javax.swing.JTextField
46+ import javax.swing.JToolBar
47+ import javax.swing.event.DocumentEvent
48+ import javax.swing.event.DocumentListener
49+ import kotlinx.coroutines.Dispatchers
50+ import kotlinx.coroutines.withContext
51+ import org.objectweb.asm.tree.ClassNode
52+ import org.objectweb.asm.tree.MethodNode
53+
54+ private const val OUTER_PADDING = 30.0
55+ private const val INTER_GROUP_SPACING = 75
56+ private const val INTRA_GROUP_SPACING = 75
57+ private const val LINE_NUMBER_STYLE = " LINE_NUMBER"
58+ private const val HIGHLIGHT_STYLE = " HIGHLIGHT"
59+
60+ class FlowDiagram (
61+ val method : MethodNode ,
62+ val panel : JPanel ,
63+ val scrollToLine : (Int ) -> Unit ,
64+ ) {
65+ companion object {
66+ suspend fun create (project : Project , clazz : ClassNode , method : MethodNode ): FlowDiagram ? {
67+ val flowGraph = FlowGraph .parse(project, clazz, method) ? : return null
68+ return buildPanel(flowGraph, method)
69+ }
70+ }
71+ }
72+
73+ private suspend fun buildPanel (flowGraph : FlowGraph , method : MethodNode ): FlowDiagram {
74+ val graph = MxFlowGraph ()
75+ setupStyles(graph)
76+ val groupedCells = addGraphContent(graph, flowGraph)
77+ val lineNumberNodes = sortedMapOf<Int , mxCell>()
78+ val calculateBounds = layOutGraph(graph, groupedCells, lineNumberNodes)
79+
80+ val panel: JPanel
81+ val scrollToLine = withContext(Dispatchers .EDT ) {
82+ panel = JPanel (BorderLayout ())
83+ displayGraphComponent(graph, panel, calculateBounds, lineNumberNodes)
84+ }
85+ return FlowDiagram (method, panel, scrollToLine)
86+ }
87+
88+ private fun displayGraphComponent (
89+ graph : mxGraph,
90+ panel : JPanel ,
91+ calculateBounds : () -> Dimension ,
92+ lineNumberNodes : SortedMap <Int , mxCell>
93+ ): (Int ) -> Unit {
94+ val comp = mxGraphComponent(graph)
95+ fun fixBounds () {
96+ comp.graphControl.preferredSize = calculateBounds()
97+ }
98+
99+ graph.view.addListener(mxEvent.SCALE_AND_TRANSLATE ) { _, _ ->
100+ fixBounds()
101+ }
102+ fixBounds()
103+ configureGraphComponent(comp)
104+
105+ val toolbar = createToolbar(comp, ::fixBounds)
106+ panel.add(toolbar, BorderLayout .NORTH )
107+ panel.add(comp, BorderLayout .CENTER )
108+
109+ return { lineNumber ->
110+ lineNumberNodes.tailMap(lineNumber).firstEntry()?.let { (_, node) ->
111+ scrollCellToVisible(comp, node)
112+ }
113+ }
114+ }
115+
116+ private fun scrollCellToVisible (comp : mxGraphComponent, node : mxCell) {
117+ // Scrolls the cell to the top of the screen if possible
118+ val graph = comp.graph
119+ val state = graph.view.getState(node) ? : return
120+ val cellBounds = state.rectangle
121+ val viewRect = comp.viewport.viewRect
122+ val targetRect = Rectangle (
123+ cellBounds.x, cellBounds.y,
124+ 1 , viewRect.height
125+ )
126+ comp.graphControl.scrollRectToVisible(targetRect)
127+ }
128+
129+ private fun createToolbar (comp : mxGraphComponent, fixBounds : () -> Unit ): JToolBar {
130+ val toolbar = JToolBar ()
131+ toolbar.isFloatable = false
132+ val zoomInButton = JButton (" +" )
133+ zoomInButton.toolTipText = " Zoom In"
134+ zoomInButton.addActionListener {
135+ comp.zoomIn()
136+ }
137+ val zoomOutButton = JButton (" −" )
138+ zoomOutButton.toolTipText = " Zoom Out"
139+ zoomOutButton.addActionListener {
140+ comp.zoomOut()
141+ }
142+ toolbar.add(zoomInButton)
143+ toolbar.add(zoomOutButton)
144+ toolbar.addSeparator(Dimension (20 , 0 ))
145+ toolbar.add(JLabel (" Search: " ))
146+ toolbar.add(createSearchField(comp, fixBounds))
147+ return toolbar
148+ }
149+
150+ private fun createSearchField (comp : mxGraphComponent, fixBounds : () -> Unit ): JTextField {
151+ val graph = comp.graph
152+ val searchField = JTextField ()
153+ searchField.document.addDocumentListener(object : DocumentListener {
154+ override fun insertUpdate (e : DocumentEvent ) = updateHighlight()
155+
156+ override fun removeUpdate (e : DocumentEvent ) = updateHighlight()
157+
158+ override fun changedUpdate (e : DocumentEvent ) = updateHighlight()
159+
160+ private fun updateHighlight () {
161+ val searchText = searchField.text.lowercase()
162+ graph.update {
163+ val vertices = graph.getChildVertices(graph.defaultParent)
164+ var scrolled = false
165+
166+ for (cell in vertices) {
167+ cell as mxCell
168+ if (cell.style == LINE_NUMBER_STYLE ) {
169+ continue
170+ }
171+ val texts = listOf (
172+ graph.convertValueToString(cell),
173+ graph.getToolTipForCell(cell),
174+ )
175+
176+ if (searchText.isNotEmpty() && texts.any { searchText in it.lowercase() }) {
177+ graph.setCellStyle(HIGHLIGHT_STYLE , arrayOf(cell))
178+ if (! scrolled) {
179+ comp.scrollCellToVisible(cell, true )
180+ comp.zoomTo(1.2 , true )
181+ graph.selectionCell = cell
182+ scrolled = true
183+ }
184+ } else {
185+ graph.model.setStyle(cell, null )
186+ }
187+ }
188+ }
189+ comp.refresh()
190+ fixBounds()
191+ }
192+ })
193+ return searchField
194+ }
195+
196+ private class MxFlowGraph : mxGraph() {
197+ override fun getToolTipForCell (cell : Any? ): String {
198+ val flow = (cell as ? mxCell)?.value as ? FlowNode ? : return super .getToolTipForCell(cell)
199+ return flow.longText
200+ }
201+
202+ override fun convertValueToString (cell : Any? ): String {
203+ val flow = (cell as ? mxCell)?.value as ? FlowNode ? : return super .convertValueToString(cell)
204+ return flow.shortText
205+ }
206+ }
207+
208+ private suspend fun addGraphContent (
209+ graph : mxGraph,
210+ flowGraph : FlowGraph
211+ ): SortedMap <FlowGroup , List <mxCell>> {
212+ val groupedCells = sortedMapOf<FlowGroup , List <mxCell>>()
213+ graph.update {
214+ fun addFlow (flow : FlowNode , parent : mxCell? , out : (mxCell) -> Unit ) {
215+ val node = graph.insertVertex(null , null , flow, 0.0 , 0.0 , 0.0 , 0.0 ) as mxCell
216+ graph.updateCellSize(node, true )
217+ if (parent != null ) {
218+ out (graph.insertEdge(null , null , null , node, parent) as mxCell)
219+ }
220+ for (input in flow.inputs) {
221+ addFlow(input, node, out )
222+ }
223+ out (node)
224+ }
225+
226+ for (group in flowGraph) {
227+ @Suppress(" UnstableApiUsage" )
228+ checkCanceled()
229+ val cells = mutableListOf< mxCell> ()
230+ addFlow(group.root, null , cells::add)
231+ groupedCells[group] = cells
232+ }
233+ }
234+ return groupedCells
235+ }
236+
237+ private suspend fun layOutGraph (
238+ graph : mxGraph,
239+ groupedCells : SortedMap <FlowGroup , List <mxCell>>,
240+ lineNumberNodes : SortedMap <Int , mxCell>
241+ ): () -> Dimension {
242+ val layout = mxHierarchicalLayout(graph)
243+ var lastBounds = mxRectangle(0.0 , 0.0 , 0.0 , 0.0 )
244+ var maxX = 0.0
245+ var maxY = 0.0
246+ var lastLine: Int? = null
247+ for ((group, list) in groupedCells) {
248+ @Suppress(" UnstableApiUsage" )
249+ checkCanceled()
250+
251+ val (targetLeft, targetTop) = if (group.lineNumber == lastLine) {
252+ (lastBounds.x + lastBounds.width + INTRA_GROUP_SPACING ) to (lastBounds.y)
253+ } else {
254+ val label = graph.insertVertex(
255+ null , null ,
256+ " Line ${group.lineNumber} :" ,
257+ OUTER_PADDING / 2 ,
258+ maxY + INTER_GROUP_SPACING / 2 ,
259+ 0.0 , 0.0 ,
260+ LINE_NUMBER_STYLE
261+ ) as mxCell
262+ lineNumberNodes[group.lineNumber] = label
263+ graph.updateCellSize(label, true )
264+ graph.moveCells(arrayOf(label), 0.0 , - graph.view.getState(label).height / 2 )
265+ (OUTER_PADDING ) to (maxY + INTER_GROUP_SPACING )
266+ }
267+ layout.execute(graph.getDefaultParent(), list)
268+ val cells = list.toTypedArray()
269+ val bounds = graph.view.getBounds(cells)
270+ graph.moveCells(cells, - bounds.x + targetLeft, - bounds.y + targetTop)
271+ lastBounds = mxRectangle(targetLeft, targetTop, bounds.width, bounds.height)
272+ maxX = maxOf(maxX, lastBounds.x + lastBounds.width)
273+ maxY = maxOf(maxY, lastBounds.y + lastBounds.height)
274+ lastLine = group.lineNumber
275+ }
276+
277+ return {
278+ Dimension (
279+ ((maxX + OUTER_PADDING ) * graph.view.scale).toInt(),
280+ ((maxY + OUTER_PADDING ) * graph.view.scale).toInt()
281+ )
282+ }
283+ }
284+
285+ private fun setupStyles (graph : mxGraph) {
286+ val colorScheme = EditorColorsManager .getInstance().globalScheme
287+ graph.stylesheet.defaultVertexStyle.let {
288+ it[mxConstants.STYLE_FONTFAMILY ] = colorScheme.getFont(EditorFontType .PLAIN ).family
289+ it[mxConstants.STYLE_ROUNDED ] = true
290+ it[mxConstants.STYLE_FILLCOLOR ] = JBUI .CurrentTheme .Button .buttonColorStart().hexString
291+ it[mxConstants.STYLE_FONTCOLOR ] = UIUtil .getLabelForeground().hexString
292+ it[mxConstants.STYLE_STROKECOLOR ] = JBUI .CurrentTheme .Button .buttonOutlineColorStart(false ).hexString
293+ it[mxConstants.STYLE_ALIGN ] = mxConstants.ALIGN_CENTER
294+ it[mxConstants.STYLE_VERTICAL_ALIGN ] = mxConstants.ALIGN_TOP
295+ it[mxConstants.STYLE_SHAPE ] = mxConstants.SHAPE_LABEL
296+ it[mxConstants.STYLE_SPACING ] = 5
297+ it[mxConstants.STYLE_SPACING_TOP ] = 3
298+ }
299+
300+ graph.stylesheet.defaultEdgeStyle.let {
301+ it[mxConstants.STYLE_STROKECOLOR ] = UIUtil .getFocusedBorderColor().hexString
302+ }
303+
304+ graph.stylesheet.putCellStyle(
305+ LINE_NUMBER_STYLE ,
306+ mapOf (
307+ mxConstants.STYLE_FONTSIZE to " 16" ,
308+ mxConstants.STYLE_STROKECOLOR to " none" ,
309+ mxConstants.STYLE_FILLCOLOR to " none" ,
310+ )
311+ )
312+ graph.stylesheet.putCellStyle(
313+ HIGHLIGHT_STYLE ,
314+ mapOf (
315+ mxConstants.STYLE_STROKECOLOR to UIUtil .getFocusedBorderColor().hexString,
316+ mxConstants.STYLE_STROKEWIDTH to " 2" ,
317+ )
318+ )
319+ }
320+
321+ private fun configureGraphComponent (comp : mxGraphComponent) {
322+ val graph = comp.graph
323+ graph.isCellsSelectable = false
324+ graph.isCellsEditable = false
325+ comp.isConnectable = false
326+ comp.isPanning = true
327+ comp.setToolTips(true )
328+ comp.viewport.setOpaque(true )
329+ comp.viewport.setBackground(EditorColorsManager .getInstance().globalScheme.defaultBackground)
330+
331+ comp.zoomAndCenter()
332+ comp.graphControl.isDoubleBuffered = false
333+ comp.graphControl.setOpaque(false )
334+ comp.verticalScrollBar.setUnitIncrement(16 )
335+ comp.horizontalScrollBar.setUnitIncrement(16 )
336+ }
337+
338+ private val Color .hexString get() = " #" + rgb.toString(16 )
339+
340+ private inline fun <T > mxGraph.update (routine : () -> T ): T {
341+ model.beginUpdate()
342+ try {
343+ return routine()
344+ } finally {
345+ model.endUpdate()
346+ }
347+ }
0 commit comments