Skip to content

Commit c4858db

Browse files
committed
New: MixinExtras Flow Graph
1 parent d192c92 commit c4858db

File tree

9 files changed

+841
-2
lines changed

9 files changed

+841
-2
lines changed

build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ dependencies {
9090
exclude(group = "org.ow2.asm", module = "asm-debug-all")
9191
}
9292
testLibs(libs.mixinExtras.common)
93+
implementation(libs.jgraphx)
9394

9495
implementation(libs.mappingIo)
9596
implementation(libs.bundles.asm)

gradle/libs.versions.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,8 @@ changelog-plugin = { module = "org.jetbrains.changelog:org.jetbrains.changelog.g
3434
coroutines-swing = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-swing", version.ref = "coroutines" }
3535

3636
mappingIo = "net.fabricmc:mapping-io:0.2.1"
37-
mixinExtras-expressions = "io.github.llamalad7:mixinextras-expressions:0.0.4"
37+
mixinExtras-expressions = "io.github.llamalad7:mixinextras-expressions:0.0.5"
38+
jgraphx = "com.github.vlsi.mxgraph:jgraphx:4.2.2"
3839

3940
# GrammarKit
4041
jflex-lib = "org.jetbrains.idea:jflex:1.7.0-b7f882a"
Lines changed: 347 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,347 @@
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.SortedMap
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() = "#%06X".format(rgb)
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

Comments
 (0)