From 9cea98df3abd6ad4b6b61f2408588e73704e4f69 Mon Sep 17 00:00:00 2001 From: MaartenS11 Date: Wed, 4 Feb 2026 17:20:39 +0100 Subject: [PATCH 1/9] Add the ability to scale the graph in the viewport --- .../kotlin/be/ugent/topl/mio/ui/GraphPanel.kt | 32 ++++++++++++++++--- 1 file changed, 28 insertions(+), 4 deletions(-) diff --git a/src/main/kotlin/be/ugent/topl/mio/ui/GraphPanel.kt b/src/main/kotlin/be/ugent/topl/mio/ui/GraphPanel.kt index 5b08916..ab10a0b 100644 --- a/src/main/kotlin/be/ugent/topl/mio/ui/GraphPanel.kt +++ b/src/main/kotlin/be/ugent/topl/mio/ui/GraphPanel.kt @@ -16,17 +16,20 @@ import java.awt.RenderingHints import java.awt.event.MouseEvent import java.awt.event.MouseListener import java.awt.event.MouseMotionListener +import java.awt.event.MouseWheelEvent +import java.awt.event.MouseWheelListener import java.awt.geom.Path2D import javax.swing.JPanel import javax.swing.JScrollPane import javax.swing.UIManager class GraphPanel(private val graph: MultiverseGraph) : JPanel(), - MouseListener, MouseMotionListener { + MouseListener, MouseMotionListener, MouseWheelListener { private var selectionListeners = mutableListOf<() -> Unit>() init { addMouseListener(this) addMouseMotionListener(this) + addMouseWheelListener(this) } private val textColour = UIManager.getDefaults().getColor("RadioButton.foreground") //private val borderColour = Color(125, 125, 125) @@ -51,7 +54,7 @@ class GraphPanel(private val graph: MultiverseGraph) : JPanel(), data class Node(val x: Int, val y: Int, val w: Int, val h: Int, val value: MultiverseNode) override fun getPreferredSize(): Dimension { - return Dimension(renderedWidth, renderedHeight) + return Dimension((renderedWidth * scaleFactor).toInt(), (renderedHeight * scaleFactor).toInt()) } override fun paintComponent(g: Graphics) { @@ -59,6 +62,7 @@ class GraphPanel(private val graph: MultiverseGraph) : JPanel(), val g2 = g as Graphics2D g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON) g2.stroke = BasicStroke(2.0f) + g2.scale(scaleFactor, scaleFactor) drawPaths(g, width - 100, graph.rootNode) } @@ -192,8 +196,10 @@ class GraphPanel(private val graph: MultiverseGraph) : JPanel(), return } + val x = e.x/scaleFactor + val y = e.y/scaleFactor for (node in nodes) { - if (e.x > node.x && e.y > node.y && e.x < node.x + node.w && e.y < node.y + node.h) { + if (x > node.x && y > node.y && x < node.x + node.w && y < node.y + node.h) { selectedNode = node } } @@ -228,11 +234,29 @@ class GraphPanel(private val graph: MultiverseGraph) : JPanel(), cursor = Cursor(Cursor.DEFAULT_CURSOR) var hit = false for (node in nodes) { - if (e.x > node.x && e.y > node.y && e.x < node.x + node.w && e.y < node.y + node.h) { + val x = e.x/scaleFactor + val y = e.y/scaleFactor + if (x > node.x && y > node.y && x < node.x + node.w && y < node.y + node.h) { hit = true break } } cursor = if(hit) Cursor.getPredefinedCursor(Cursor.HAND_CURSOR) else Cursor.getDefaultCursor() } + + private var scaleFactor = 1.0 + + override fun mouseWheelMoved(e: MouseWheelEvent) { + val oldScaleFactor = scaleFactor + val adjustment = e.wheelRotation / 20.0 + scaleFactor += adjustment + scaleFactor = kotlin.math.max(0.1, scaleFactor) + associatedScrollPane?.horizontalScrollBar?.value = ((associatedScrollPane?.horizontalScrollBar?.value!! / oldScaleFactor) * scaleFactor).toInt() + associatedScrollPane?.verticalScrollBar?.value = ((associatedScrollPane?.verticalScrollBar?.value!! / oldScaleFactor) * scaleFactor).toInt() + println("Scale = $scaleFactor") + repaint() + + associatedScrollPane?.verticalScrollBar?.revalidate() + associatedScrollPane?.horizontalScrollBar?.revalidate() + } } \ No newline at end of file From 3d932de840448de32a1d48f513e48615941f4133 Mon Sep 17 00:00:00 2001 From: MaartenS11 Date: Wed, 4 Feb 2026 20:44:48 +0100 Subject: [PATCH 2/9] Remove scrollpane and replace it with translation in graphics2d + minimap --- .../kotlin/be/ugent/topl/mio/ui/GraphPanel.kt | 109 ++++++++++++++++-- .../ugent/topl/mio/ui/InteractiveDebugger.kt | 5 +- 2 files changed, 98 insertions(+), 16 deletions(-) diff --git a/src/main/kotlin/be/ugent/topl/mio/ui/GraphPanel.kt b/src/main/kotlin/be/ugent/topl/mio/ui/GraphPanel.kt index ab10a0b..9e16a7b 100644 --- a/src/main/kotlin/be/ugent/topl/mio/ui/GraphPanel.kt +++ b/src/main/kotlin/be/ugent/topl/mio/ui/GraphPanel.kt @@ -19,9 +19,14 @@ import java.awt.event.MouseMotionListener import java.awt.event.MouseWheelEvent import java.awt.event.MouseWheelListener import java.awt.geom.Path2D +import java.awt.image.BufferedImage +import java.io.File +import javax.imageio.ImageIO import javax.swing.JPanel import javax.swing.JScrollPane import javax.swing.UIManager +import kotlin.math.min +import kotlin.math.roundToInt class GraphPanel(private val graph: MultiverseGraph) : JPanel(), MouseListener, MouseMotionListener, MouseWheelListener { @@ -57,17 +62,76 @@ class GraphPanel(private val graph: MultiverseGraph) : JPanel(), return Dimension((renderedWidth * scaleFactor).toInt(), (renderedHeight * scaleFactor).toInt()) } + var xOffset = 0 + var yOffset = 0 override fun paintComponent(g: Graphics) { super.paintComponent(g) val g2 = g as Graphics2D g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON) g2.stroke = BasicStroke(2.0f) + + drawMiniMap(g2) + g2.scale(scaleFactor, scaleFactor) + g2.translate(-xOffset, -yOffset) + + drawPaths(g, graph.rootNode) + } - drawPaths(g, width - 100, graph.rootNode) + fun drawMiniMap(g: Graphics2D) { + //g.drawString("camera pos = ($xOffset, $yOffset)", 5, 10) + if (scaleFactor != 1.0) { + val zoomStr = "${(scaleFactor * 100).roundToInt()}%" + val zoomStrWidth = getFontMetrics(g.font).stringWidth(zoomStr) + g.drawString(zoomStr, width - zoomStrWidth, 10) + } + g.color = Color(100, 100, 100, 50) + g.fillRect(0, 0, (renderedWidth * 0.01f).roundToInt(), (renderedHeight * 0.01f).roundToInt()) + g.color = Color(255, 150, 150, 150) + val cameraPosX = (xOffset * 0.01f).roundToInt() + val cameraPosY = (yOffset * 0.01f).roundToInt() + val cameraWidth = (width/scaleFactor * 0.01f).roundToInt() + val cameraHeight = (height/scaleFactor * 0.01f).roundToInt() + g.fillRect(cameraPosX, cameraPosY, cameraWidth, cameraHeight) + g.color = Color(255, 150, 150, 255) + g.drawRect(cameraPosX, cameraPosY, cameraWidth, cameraHeight) + /*g.color = Color(200, 100, 100, 255) + g.drawString("C", cameraPosX, cameraPosY + 10)*/ + g.scale(0.01, 0.01) + drawPaths(g, graph.rootNode) + g.scale(100.0, 100.0) } - private fun drawPaths(g: Graphics2D, width: Int, rootNode: MultiverseNode) { + fun saveImage(filename: String) { + println("Full graph size $renderedWidth x $renderedHeight") + //300000 + //val image = BufferedImage(renderedWidth, renderedHeight, BufferedImage.TYPE_INT_RGB) + val imageSize = 30000 + var imageWidth = min(imageSize, renderedWidth) + var imageHeight = min(imageSize, renderedHeight) + if (renderedWidth * renderedHeight < Integer.MAX_VALUE) { + imageWidth = renderedWidth - 450 + imageHeight = renderedHeight + } + val image = BufferedImage(imageWidth, imageHeight, BufferedImage.TYPE_INT_RGB) + val g = image.createGraphics().apply { + setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON) + stroke = BasicStroke(2.0f) + //translate(-(renderedWidth/2 - imageWidth/2),-(renderedHeight/2 - imageHeight/2)) + // Used for chunks: + //translate(-(renderedWidth - imageWidth),-(renderedHeight/2 - imageHeight/2) + imageHeight * 3) + } + g.color = backgroundColour + g.fillRect(0, 0, renderedWidth, renderedHeight) + println("Drawing multiverse tree...") + drawPaths(g, graph.rootNode) + println("Finished drawing, writing to file...") + g.dispose() + ImageIO.write(image, "png", File(filename)) + println("Finished writing") + } + + private fun drawPaths(g: Graphics2D, rootNode: MultiverseNode) { val xStart = g.fontMetrics.stringWidth(rootNode.displayName)/2 val yPadding = 15 renderedHeight = drawGraph(g, rootNode, x = xStart + 5, yPadding).second + yPadding @@ -196,8 +260,8 @@ class GraphPanel(private val graph: MultiverseGraph) : JPanel(), return } - val x = e.x/scaleFactor - val y = e.y/scaleFactor + val x = e.x/scaleFactor + xOffset + val y = e.y/scaleFactor + yOffset for (node in nodes) { if (x > node.x && y > node.y && x < node.x + node.w && y < node.y + node.h) { selectedNode = node @@ -215,6 +279,7 @@ class GraphPanel(private val graph: MultiverseGraph) : JPanel(), } override fun mousePressed(p0: MouseEvent) { + println("Mouse pressed") startPos = p0.point } @@ -226,16 +291,21 @@ class GraphPanel(private val graph: MultiverseGraph) : JPanel(), override fun mouseDragged(e: MouseEvent) { val delta = Point(e.x - startPos.x, e.y - startPos.y) - associatedScrollPane?.horizontalScrollBar?.value -= delta.x - associatedScrollPane?.verticalScrollBar?.value -= delta.y + println("" + e.x + " " + e.y) + /*associatedScrollPane?.horizontalScrollBar?.value -= delta.x + associatedScrollPane?.verticalScrollBar?.value -= delta.y*/ + xOffset -= (delta.x / scaleFactor).toInt() + yOffset -= (delta.y / scaleFactor).toInt() + repaint() + startPos = Point(e.x, e.y) } override fun mouseMoved(e: MouseEvent) { cursor = Cursor(Cursor.DEFAULT_CURSOR) var hit = false for (node in nodes) { - val x = e.x/scaleFactor - val y = e.y/scaleFactor + val x = e.x/scaleFactor + xOffset + val y = e.y/scaleFactor + yOffset if (x > node.x && y > node.y && x < node.x + node.w && y < node.y + node.h) { hit = true break @@ -251,12 +321,27 @@ class GraphPanel(private val graph: MultiverseGraph) : JPanel(), val adjustment = e.wheelRotation / 20.0 scaleFactor += adjustment scaleFactor = kotlin.math.max(0.1, scaleFactor) - associatedScrollPane?.horizontalScrollBar?.value = ((associatedScrollPane?.horizontalScrollBar?.value!! / oldScaleFactor) * scaleFactor).toInt() - associatedScrollPane?.verticalScrollBar?.value = ((associatedScrollPane?.verticalScrollBar?.value!! / oldScaleFactor) * scaleFactor).toInt() + /*val oldW = height * oldScaleFactor + val newW = height * scaleFactor + val delta = (newW - oldW)/scaleFactor + yOffset += (delta/2).toInt()*/ + + val oldH = height/oldScaleFactor + val newH = height/scaleFactor + val deltaH = (newH - oldH) + yOffset -= (deltaH/2).toInt() + + val oldW = width/oldScaleFactor + val newW = width/scaleFactor + val deltaW = (newW - oldW) + xOffset -= (deltaW/2).toInt() + + /*associatedScrollPane?.horizontalScrollBar?.value = ((associatedScrollPane?.horizontalScrollBar?.value!! / oldScaleFactor) * scaleFactor).toInt() + associatedScrollPane?.verticalScrollBar?.value = ((associatedScrollPane?.verticalScrollBar?.value!! / oldScaleFactor) * scaleFactor).toInt()*/ println("Scale = $scaleFactor") repaint() - associatedScrollPane?.verticalScrollBar?.revalidate() - associatedScrollPane?.horizontalScrollBar?.revalidate() + /*associatedScrollPane?.verticalScrollBar?.revalidate() + associatedScrollPane?.horizontalScrollBar?.revalidate()*/ } } \ No newline at end of file diff --git a/src/main/kotlin/be/ugent/topl/mio/ui/InteractiveDebugger.kt b/src/main/kotlin/be/ugent/topl/mio/ui/InteractiveDebugger.kt index af4a03f..34f7be5 100644 --- a/src/main/kotlin/be/ugent/topl/mio/ui/InteractiveDebugger.kt +++ b/src/main/kotlin/be/ugent/topl/mio/ui/InteractiveDebugger.kt @@ -492,10 +492,7 @@ class MultiversePanel(private val multiverseDebugger: MultiverseDebugger, config } init { layout = BorderLayout() - //add(JScrollPane(graphPanel)) - val scrollpane = JScrollPane(graphPanel) - graphPanel.associatedScrollPane = scrollpane - add(JSplitPane(JSplitPane.VERTICAL_SPLIT, scrollpane, mockPanel).apply { + add(JSplitPane(JSplitPane.VERTICAL_SPLIT, graphPanel, mockPanel).apply { resizeWeight = 0.7 }) //add(OverridesPanel(), BorderLayout.EAST) From c690a7881f671fe47aea49f690fa0a0f3fe0b85c Mon Sep 17 00:00:00 2001 From: MaartenS11 Date: Wed, 4 Feb 2026 20:52:21 +0100 Subject: [PATCH 3/9] Invert scrolling + disable mini version of graph for performance reasons We can probably store the mini version in a texture to speed things up? --- src/main/kotlin/be/ugent/topl/mio/ui/GraphPanel.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/kotlin/be/ugent/topl/mio/ui/GraphPanel.kt b/src/main/kotlin/be/ugent/topl/mio/ui/GraphPanel.kt index 9e16a7b..6e0db27 100644 --- a/src/main/kotlin/be/ugent/topl/mio/ui/GraphPanel.kt +++ b/src/main/kotlin/be/ugent/topl/mio/ui/GraphPanel.kt @@ -97,9 +97,9 @@ class GraphPanel(private val graph: MultiverseGraph) : JPanel(), g.drawRect(cameraPosX, cameraPosY, cameraWidth, cameraHeight) /*g.color = Color(200, 100, 100, 255) g.drawString("C", cameraPosX, cameraPosY + 10)*/ - g.scale(0.01, 0.01) + /*g.scale(0.01, 0.01) drawPaths(g, graph.rootNode) - g.scale(100.0, 100.0) + g.scale(100.0, 100.0)*/ } fun saveImage(filename: String) { @@ -319,7 +319,7 @@ class GraphPanel(private val graph: MultiverseGraph) : JPanel(), override fun mouseWheelMoved(e: MouseWheelEvent) { val oldScaleFactor = scaleFactor val adjustment = e.wheelRotation / 20.0 - scaleFactor += adjustment + scaleFactor -= adjustment scaleFactor = kotlin.math.max(0.1, scaleFactor) /*val oldW = height * oldScaleFactor val newW = height * scaleFactor From 384777d40cd192fa35d4e5bfff6a7da017cb1ccb Mon Sep 17 00:00:00 2001 From: MaartenS11 Date: Wed, 4 Feb 2026 22:54:28 +0100 Subject: [PATCH 4/9] Clip camera rectangle to tree rectangle + auto scale minimap Also changed the color from red to blue --- .../kotlin/be/ugent/topl/mio/ui/GraphPanel.kt | 52 +++++++++++++------ 1 file changed, 35 insertions(+), 17 deletions(-) diff --git a/src/main/kotlin/be/ugent/topl/mio/ui/GraphPanel.kt b/src/main/kotlin/be/ugent/topl/mio/ui/GraphPanel.kt index 6e0db27..f50ff5e 100644 --- a/src/main/kotlin/be/ugent/topl/mio/ui/GraphPanel.kt +++ b/src/main/kotlin/be/ugent/topl/mio/ui/GraphPanel.kt @@ -12,6 +12,7 @@ import java.awt.Dimension import java.awt.Graphics import java.awt.Graphics2D import java.awt.Point +import java.awt.Rectangle import java.awt.RenderingHints import java.awt.event.MouseEvent import java.awt.event.MouseListener @@ -45,8 +46,8 @@ class GraphPanel(private val graph: MultiverseGraph) : JPanel(), private val green = if (!FlatLaf.isLafDark()) Color(89, 158, 94) else Color(136, 207, 131) private val d = 20 private val hSpace = 100 - private var renderedHeight = 500 - private var renderedWidth = 2000 + private var renderedHeight = 100 + private var renderedWidth = 100 private val nodes = mutableListOf() var selectedNode: Node? = null private set @@ -70,36 +71,53 @@ class GraphPanel(private val graph: MultiverseGraph) : JPanel(), g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON) g2.stroke = BasicStroke(2.0f) - drawMiniMap(g2) - g2.scale(scaleFactor, scaleFactor) g2.translate(-xOffset, -yOffset) drawPaths(g, graph.rootNode) + + g2.translate(xOffset, yOffset) + g2.scale(1/scaleFactor, 1/scaleFactor) + + drawMiniMap(g2) } fun drawMiniMap(g: Graphics2D) { + //val scale = min(100.0/renderedHeight, width.toDouble()/renderedWidth) + val scale = min(100.0/renderedHeight, (100.0 * width/height)/renderedWidth) //g.drawString("camera pos = ($xOffset, $yOffset)", 5, 10) + + val graphWidth = (renderedWidth * scale).roundToInt() + val offset = width - graphWidth + + // Zoom str if (scaleFactor != 1.0) { val zoomStr = "${(scaleFactor * 100).roundToInt()}%" val zoomStrWidth = getFontMetrics(g.font).stringWidth(zoomStr) - g.drawString(zoomStr, width - zoomStrWidth, 10) + g.color = Color(100, 100, 100, 150) + g.drawString(zoomStr, offset - zoomStrWidth - 5, 15) } + + // Graph rectangle g.color = Color(100, 100, 100, 50) - g.fillRect(0, 0, (renderedWidth * 0.01f).roundToInt(), (renderedHeight * 0.01f).roundToInt()) - g.color = Color(255, 150, 150, 150) - val cameraPosX = (xOffset * 0.01f).roundToInt() - val cameraPosY = (yOffset * 0.01f).roundToInt() - val cameraWidth = (width/scaleFactor * 0.01f).roundToInt() - val cameraHeight = (height/scaleFactor * 0.01f).roundToInt() - g.fillRect(cameraPosX, cameraPosY, cameraWidth, cameraHeight) - g.color = Color(255, 150, 150, 255) - g.drawRect(cameraPosX, cameraPosY, cameraWidth, cameraHeight) + val rectangle = Rectangle(offset, 0, graphWidth, (renderedHeight * scale).roundToInt()) + g.fillRect(rectangle.x, rectangle.y, rectangle.width, rectangle.height) + g.color = Color(150, 150, 255, 150) + val oldClip = g.clip // If we don't do this the component will be able to draw outside of itself. + g.clip = rectangle + val cameraPosX = (xOffset * scale).roundToInt() + val cameraPosY = (yOffset * scale).roundToInt() + val cameraWidth = (width/scaleFactor * scale).roundToInt() + val cameraHeight = (height/scaleFactor * scale).roundToInt() + g.fillRect(offset + cameraPosX, cameraPosY, cameraWidth, cameraHeight) + g.color = Color(150, 150, 255, 255) + g.drawRect(offset + cameraPosX, cameraPosY, cameraWidth, cameraHeight) + g.clip = oldClip /*g.color = Color(200, 100, 100, 255) g.drawString("C", cameraPosX, cameraPosY + 10)*/ - /*g.scale(0.01, 0.01) + /*g.scale(scale, scale) drawPaths(g, graph.rootNode) - g.scale(100.0, 100.0)*/ + g.scale(1/scale, 1/scale)*/ } fun saveImage(filename: String) { @@ -145,7 +163,7 @@ class GraphPanel(private val graph: MultiverseGraph) : JPanel(), val result = drawGraph(g, child, x + l, y + currentHeight) currentHeight += result.second newPoints.add(result.first) - renderedWidth = Integer.max(renderedWidth, x + node.edgeLength + 500) + renderedWidth = Integer.max(renderedWidth, x + node.edgeLength) } currentHeight = Integer.max(40, currentHeight) From e73975ab4c2ea19989f89d36fadc16984bb1855b Mon Sep 17 00:00:00 2001 From: MaartenS11 Date: Wed, 4 Feb 2026 23:22:41 +0100 Subject: [PATCH 5/9] Wrap graphpanel in regular panel with border --- .../kotlin/be/ugent/topl/mio/ui/GraphPanel.kt | 16 ++-------------- .../be/ugent/topl/mio/ui/InteractiveDebugger.kt | 6 +++++- 2 files changed, 7 insertions(+), 15 deletions(-) diff --git a/src/main/kotlin/be/ugent/topl/mio/ui/GraphPanel.kt b/src/main/kotlin/be/ugent/topl/mio/ui/GraphPanel.kt index f50ff5e..761002d 100644 --- a/src/main/kotlin/be/ugent/topl/mio/ui/GraphPanel.kt +++ b/src/main/kotlin/be/ugent/topl/mio/ui/GraphPanel.kt @@ -5,20 +5,8 @@ import be.ugent.topl.mio.debugger.MultiverseGraph import be.ugent.topl.mio.debugger.MultiverseNode import be.ugent.topl.mio.debugger.PrimitiveNode import com.formdev.flatlaf.FlatLaf -import java.awt.BasicStroke -import java.awt.Color -import java.awt.Cursor -import java.awt.Dimension -import java.awt.Graphics -import java.awt.Graphics2D -import java.awt.Point -import java.awt.Rectangle -import java.awt.RenderingHints -import java.awt.event.MouseEvent -import java.awt.event.MouseListener -import java.awt.event.MouseMotionListener -import java.awt.event.MouseWheelEvent -import java.awt.event.MouseWheelListener +import java.awt.* +import java.awt.event.* import java.awt.geom.Path2D import java.awt.image.BufferedImage import java.io.File diff --git a/src/main/kotlin/be/ugent/topl/mio/ui/InteractiveDebugger.kt b/src/main/kotlin/be/ugent/topl/mio/ui/InteractiveDebugger.kt index 34f7be5..56ecce9 100644 --- a/src/main/kotlin/be/ugent/topl/mio/ui/InteractiveDebugger.kt +++ b/src/main/kotlin/be/ugent/topl/mio/ui/InteractiveDebugger.kt @@ -492,7 +492,11 @@ class MultiversePanel(private val multiverseDebugger: MultiverseDebugger, config } init { layout = BorderLayout() - add(JSplitPane(JSplitPane.VERTICAL_SPLIT, graphPanel, mockPanel).apply { + val treeFrame = JPanel(BorderLayout()).apply { + border = UIManager.getDefaults().getBorder("ScrollPane.border") + add(graphPanel) + } + add(JSplitPane(JSplitPane.VERTICAL_SPLIT, treeFrame, mockPanel).apply { resizeWeight = 0.7 }) //add(OverridesPanel(), BorderLayout.EAST) From 0022af5de969c68a52882b311d12aadfd1fd857d Mon Sep 17 00:00:00 2001 From: MaartenS11 Date: Fri, 6 Feb 2026 10:21:48 +0100 Subject: [PATCH 6/9] Use a scrollbar-esque design instead of a minimap since it works better for more horizontal trees + polish save tree --- .../kotlin/be/ugent/topl/mio/ui/GraphPanel.kt | 100 +++++++++++++++--- 1 file changed, 85 insertions(+), 15 deletions(-) diff --git a/src/main/kotlin/be/ugent/topl/mio/ui/GraphPanel.kt b/src/main/kotlin/be/ugent/topl/mio/ui/GraphPanel.kt index 761002d..371c05e 100644 --- a/src/main/kotlin/be/ugent/topl/mio/ui/GraphPanel.kt +++ b/src/main/kotlin/be/ugent/topl/mio/ui/GraphPanel.kt @@ -11,9 +11,9 @@ import java.awt.geom.Path2D import java.awt.image.BufferedImage import java.io.File import javax.imageio.ImageIO -import javax.swing.JPanel -import javax.swing.JScrollPane -import javax.swing.UIManager +import javax.swing.* +import javax.swing.filechooser.FileNameExtensionFilter +import kotlin.math.max import kotlin.math.min import kotlin.math.roundToInt @@ -39,6 +39,8 @@ class GraphPanel(private val graph: MultiverseGraph) : JPanel(), private val nodes = mutableListOf() var selectedNode: Node? = null private set + private var currentNode: Node? = null + private var lastCompleted: Node? = null // Panning private var startPos = Point(0, 0) @@ -62,6 +64,7 @@ class GraphPanel(private val graph: MultiverseGraph) : JPanel(), g2.scale(scaleFactor, scaleFactor) g2.translate(-xOffset, -yOffset) + renderedWidth = 0 drawPaths(g, graph.rootNode) g2.translate(xOffset, yOffset) @@ -83,11 +86,39 @@ class GraphPanel(private val graph: MultiverseGraph) : JPanel(), val zoomStr = "${(scaleFactor * 100).roundToInt()}%" val zoomStrWidth = getFontMetrics(g.font).stringWidth(zoomStr) g.color = Color(100, 100, 100, 150) - g.drawString(zoomStr, offset - zoomStrWidth - 5, 15) + g.drawString(zoomStr, width - zoomStrWidth - 5, 10 + 15) } - // Graph rectangle + val barHeight = UIManager.getInt("ScrollBar.width") + val tOffset = ((xOffset.toDouble()/renderedWidth) * width).toInt() + val w = (width/scaleFactor)*width/renderedWidth g.color = Color(100, 100, 100, 50) + g.color = UIManager.getColor("ScrollBar.track") + g.fillRect(0, 0, width,barHeight) + g.color = Color(150, 150, 255, 150) + g.color = UIManager.getColor("ScrollBar.thumb") + g.fillRect(tOffset, 0, max(w.toInt(), 5),barHeight) + + selectedNode?.let { node -> + val xPos = (node.x.toDouble()/renderedWidth) * width + g.color = secondaryColour + g.fillRect(xPos.toInt(), 0, 3, barHeight) + } + + currentNode?.let { node -> + val xPos = (node.x.toDouble()/renderedWidth) * width + g.color = Color.black + g.fillRect(xPos.toInt(), 0, 3, barHeight) + } + + lastCompleted?.let { node -> + val xPos = (node.x.toDouble()/renderedWidth) * width + g.color = green + g.fillRect(xPos.toInt(), 0, 3, barHeight) + } + + // Graph rectangle + /*g.color = Color(100, 100, 100, 50) val rectangle = Rectangle(offset, 0, graphWidth, (renderedHeight * scale).roundToInt()) g.fillRect(rectangle.x, rectangle.y, rectangle.width, rectangle.height) g.color = Color(150, 150, 255, 150) @@ -100,7 +131,7 @@ class GraphPanel(private val graph: MultiverseGraph) : JPanel(), g.fillRect(offset + cameraPosX, cameraPosY, cameraWidth, cameraHeight) g.color = Color(150, 150, 255, 255) g.drawRect(offset + cameraPosX, cameraPosY, cameraWidth, cameraHeight) - g.clip = oldClip + g.clip = oldClip*/ /*g.color = Color(200, 100, 100, 255) g.drawString("C", cameraPosX, cameraPosY + 10)*/ /*g.scale(scale, scale) @@ -110,22 +141,17 @@ class GraphPanel(private val graph: MultiverseGraph) : JPanel(), fun saveImage(filename: String) { println("Full graph size $renderedWidth x $renderedHeight") - //300000 - //val image = BufferedImage(renderedWidth, renderedHeight, BufferedImage.TYPE_INT_RGB) val imageSize = 30000 var imageWidth = min(imageSize, renderedWidth) var imageHeight = min(imageSize, renderedHeight) if (renderedWidth * renderedHeight < Integer.MAX_VALUE) { - imageWidth = renderedWidth - 450 + imageWidth = renderedWidth imageHeight = renderedHeight } val image = BufferedImage(imageWidth, imageHeight, BufferedImage.TYPE_INT_RGB) val g = image.createGraphics().apply { setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON) stroke = BasicStroke(2.0f) - //translate(-(renderedWidth/2 - imageWidth/2),-(renderedHeight/2 - imageHeight/2)) - // Used for chunks: - //translate(-(renderedWidth - imageWidth),-(renderedHeight/2 - imageHeight/2) + imageHeight * 3) } g.color = backgroundColour g.fillRect(0, 0, renderedWidth, renderedHeight) @@ -151,7 +177,7 @@ class GraphPanel(private val graph: MultiverseGraph) : JPanel(), val result = drawGraph(g, child, x + l, y + currentHeight) currentHeight += result.second newPoints.add(result.first) - renderedWidth = Integer.max(renderedWidth, x + node.edgeLength) + renderedWidth = Integer.max(renderedWidth, x + node.edgeLength + d + 5) } currentHeight = Integer.max(40, currentHeight) @@ -179,11 +205,16 @@ class GraphPanel(private val graph: MultiverseGraph) : JPanel(), g.color = primaryColour } else if (selectedNodes.contains(node)) { g.color = secondaryColour - if (completedPath.contains(node)) + if (completedPath.contains(node)) { g.color = green + if (node == completedPath.last()) { + lastCompleted = Node(point.x, point.y, d, d, node) + } + } g.fillOval(point.x, point.y, d, d) g.color = primaryColour } else if (node === graph.currentNode) { + currentNode = Node(point.x, point.y, d, d, graph.currentNode) g.color = secondaryColour g.fillOval(point.x, point.y, d, d) g.color = primaryColour @@ -256,12 +287,40 @@ class GraphPanel(private val graph: MultiverseGraph) : JPanel(), selectedPath = null selectedNodes.clear() selectedNode = null + lastCompleted = null } var selectedNodes = mutableSetOf() var selectedPath: Pair, List>? = null var completedPath = mutableSetOf() override fun mouseClicked(e: MouseEvent) { + if (e.button == MouseEvent.BUTTON3) { + JPopupMenu().apply { + val saveItem = JMenuItem("Save as image").apply { + addActionListener { + val chooser = JFileChooser() + chooser.fileFilter = FileNameExtensionFilter("png", "png") + if (chooser.showSaveDialog(this) == JFileChooser.APPROVE_OPTION) { + var filename = chooser.selectedFile.absolutePath + if (!filename.endsWith(".png")) { + filename += ".png" + } + saveImage(filename) + } + } + } + add(saveItem) + + }.show(this, e.x, e.y) + return + } + + if (e.y < 10) { + xOffset = ((e.x.toDouble()/width) * renderedWidth).roundToInt() - width/2 + repaint() + return + } + if (!allowSelection) { return } @@ -289,13 +348,24 @@ class GraphPanel(private val graph: MultiverseGraph) : JPanel(), startPos = p0.point } - override fun mouseReleased(p0: MouseEvent) {} + override fun mouseReleased(e: MouseEvent) { + if (e.button != MouseEvent.BUTTON1) { + mouseClicked(e) + e.consume() + return + } + } override fun mouseEntered(p0: MouseEvent) {} override fun mouseExited(p0: MouseEvent) {} override fun mouseDragged(e: MouseEvent) { + if (e.button != MouseEvent.BUTTON1) { + e.consume() + return + } + val delta = Point(e.x - startPos.x, e.y - startPos.y) println("" + e.x + " " + e.y) /*associatedScrollPane?.horizontalScrollBar?.value -= delta.x From 18baa3ed4724004f62d95fd7deb90124c8aee26e Mon Sep 17 00:00:00 2001 From: MaartenS11 Date: Fri, 6 Feb 2026 11:23:45 +0100 Subject: [PATCH 7/9] Allow dragging the sliderbar --- .../kotlin/be/ugent/topl/mio/ui/GraphPanel.kt | 42 +++++++++++++++---- 1 file changed, 33 insertions(+), 9 deletions(-) diff --git a/src/main/kotlin/be/ugent/topl/mio/ui/GraphPanel.kt b/src/main/kotlin/be/ugent/topl/mio/ui/GraphPanel.kt index 371c05e..0374fe7 100644 --- a/src/main/kotlin/be/ugent/topl/mio/ui/GraphPanel.kt +++ b/src/main/kotlin/be/ugent/topl/mio/ui/GraphPanel.kt @@ -31,6 +31,7 @@ class GraphPanel(private val graph: MultiverseGraph) : JPanel(), private val primaryColour = UIManager.getDefaults().getColor("Panel.foreground") private val backgroundColour = UIManager.getDefaults().getColor("CheckBox.icon.background") private val secondaryColour = UIManager.getDefaults().getColor("Button.default.background") //javax.swing.UIManager.getDefaults().getColor("Button.default.focusColor") + val barHeight = UIManager.getInt("ScrollBar.width") private val green = if (!FlatLaf.isLafDark()) Color(89, 158, 94) else Color(136, 207, 131) private val d = 20 private val hSpace = 100 @@ -89,15 +90,10 @@ class GraphPanel(private val graph: MultiverseGraph) : JPanel(), g.drawString(zoomStr, width - zoomStrWidth - 5, 10 + 15) } - val barHeight = UIManager.getInt("ScrollBar.width") - val tOffset = ((xOffset.toDouble()/renderedWidth) * width).toInt() - val w = (width/scaleFactor)*width/renderedWidth - g.color = Color(100, 100, 100, 50) g.color = UIManager.getColor("ScrollBar.track") g.fillRect(0, 0, width,barHeight) - g.color = Color(150, 150, 255, 150) - g.color = UIManager.getColor("ScrollBar.thumb") - g.fillRect(tOffset, 0, max(w.toInt(), 5),barHeight) + g.color = if(draggingScrollBar) UIManager.getColor("ScrollBar.pressedThumbColor") else UIManager.getColor("ScrollBar.thumb") + g.fillRect(scrollBarPosition(), 0, scrollBarWidth(), barHeight) selectedNode?.let { node -> val xPos = (node.x.toDouble()/renderedWidth) * width @@ -139,6 +135,14 @@ class GraphPanel(private val graph: MultiverseGraph) : JPanel(), g.scale(1/scale, 1/scale)*/ } + private fun scrollBarPosition(): Int { + return ((xOffset.toDouble()/renderedWidth) * width).toInt() + } + + private fun scrollBarWidth(): Int { + return max(((width/scaleFactor)*width/renderedWidth).toInt(), 5) + } + fun saveImage(filename: String) { println("Full graph size $renderedWidth x $renderedHeight") val imageSize = 30000 @@ -315,7 +319,7 @@ class GraphPanel(private val graph: MultiverseGraph) : JPanel(), return } - if (e.y < 10) { + if (e.y < barHeight) { xOffset = ((e.x.toDouble()/width) * renderedWidth).roundToInt() - width/2 repaint() return @@ -348,7 +352,13 @@ class GraphPanel(private val graph: MultiverseGraph) : JPanel(), startPos = p0.point } + private var draggingScrollBar = false override fun mouseReleased(e: MouseEvent) { + println("Mouse released") + if (draggingScrollBar) { + draggingScrollBar = false + repaint() + } if (e.button != MouseEvent.BUTTON1) { mouseClicked(e) e.consume() @@ -361,19 +371,33 @@ class GraphPanel(private val graph: MultiverseGraph) : JPanel(), override fun mouseExited(p0: MouseEvent) {} override fun mouseDragged(e: MouseEvent) { + println("Mouse dragged") if (e.button != MouseEvent.BUTTON1) { e.consume() return } val delta = Point(e.x - startPos.x, e.y - startPos.y) + startPos = Point(e.x, e.y) + + val pos = scrollBarPosition() + val w = scrollBarWidth() + if (e.y < barHeight || draggingScrollBar) { + if ((e.x >= pos && e.x < pos + w) || draggingScrollBar) { + xOffset += ((delta.x.toDouble()/width) * renderedWidth).roundToInt() + draggingScrollBar = true + repaint() + } + e.consume() + return + } + println("" + e.x + " " + e.y) /*associatedScrollPane?.horizontalScrollBar?.value -= delta.x associatedScrollPane?.verticalScrollBar?.value -= delta.y*/ xOffset -= (delta.x / scaleFactor).toInt() yOffset -= (delta.y / scaleFactor).toInt() repaint() - startPos = Point(e.x, e.y) } override fun mouseMoved(e: MouseEvent) { From 6d4bedd1c7babfc5e592177d518612f5c9f85d50 Mon Sep 17 00:00:00 2001 From: MaartenS11 Date: Fri, 6 Feb 2026 14:19:16 +0100 Subject: [PATCH 8/9] Add hover to the sliderbar --- .../kotlin/be/ugent/topl/mio/ui/GraphPanel.kt | 47 +++++++++++++++---- 1 file changed, 38 insertions(+), 9 deletions(-) diff --git a/src/main/kotlin/be/ugent/topl/mio/ui/GraphPanel.kt b/src/main/kotlin/be/ugent/topl/mio/ui/GraphPanel.kt index 0374fe7..975d4cc 100644 --- a/src/main/kotlin/be/ugent/topl/mio/ui/GraphPanel.kt +++ b/src/main/kotlin/be/ugent/topl/mio/ui/GraphPanel.kt @@ -92,7 +92,11 @@ class GraphPanel(private val graph: MultiverseGraph) : JPanel(), g.color = UIManager.getColor("ScrollBar.track") g.fillRect(0, 0, width,barHeight) - g.color = if(draggingScrollBar) UIManager.getColor("ScrollBar.pressedThumbColor") else UIManager.getColor("ScrollBar.thumb") + g.color = when (draggingScrollBar) { + MouseState.None -> UIManager.getColor("ScrollBar.thumb") + MouseState.Hover -> UIManager.getColor("ScrollBar.hoverThumbColor") + MouseState.Pressed -> UIManager.getColor("ScrollBar.pressedThumbColor") + } g.fillRect(scrollBarPosition(), 0, scrollBarWidth(), barHeight) selectedNode?.let { node -> @@ -352,11 +356,16 @@ class GraphPanel(private val graph: MultiverseGraph) : JPanel(), startPos = p0.point } - private var draggingScrollBar = false + enum class MouseState { + None, + Hover, + Pressed + } + private var draggingScrollBar = MouseState.None override fun mouseReleased(e: MouseEvent) { println("Mouse released") - if (draggingScrollBar) { - draggingScrollBar = false + if (draggingScrollBar == MouseState.Pressed) { + draggingScrollBar = MouseState.None repaint() } if (e.button != MouseEvent.BUTTON1) { @@ -366,9 +375,14 @@ class GraphPanel(private val graph: MultiverseGraph) : JPanel(), } } - override fun mouseEntered(p0: MouseEvent) {} + override fun mouseEntered(e: MouseEvent) {} - override fun mouseExited(p0: MouseEvent) {} + override fun mouseExited(e: MouseEvent) { + if (draggingScrollBar == MouseState.Hover) { + draggingScrollBar = MouseState.None + repaint() + } + } override fun mouseDragged(e: MouseEvent) { println("Mouse dragged") @@ -382,10 +396,11 @@ class GraphPanel(private val graph: MultiverseGraph) : JPanel(), val pos = scrollBarPosition() val w = scrollBarWidth() - if (e.y < barHeight || draggingScrollBar) { - if ((e.x >= pos && e.x < pos + w) || draggingScrollBar) { + val dragging = draggingScrollBar == MouseState.Pressed + if (e.y < barHeight || dragging) { + if ((e.x >= pos && e.x < pos + w) || dragging) { xOffset += ((delta.x.toDouble()/width) * renderedWidth).roundToInt() - draggingScrollBar = true + draggingScrollBar = MouseState.Pressed repaint() } e.consume() @@ -401,6 +416,20 @@ class GraphPanel(private val graph: MultiverseGraph) : JPanel(), } override fun mouseMoved(e: MouseEvent) { + // Scrollbar hover + val pos = scrollBarPosition() + val w = scrollBarWidth() + if (e.y < barHeight && e.x >= pos && e.x < pos + w) { + draggingScrollBar = MouseState.Hover + repaint() + e.consume() + return + } else if (draggingScrollBar == MouseState.Hover) { + draggingScrollBar = MouseState.None + repaint() + } + + // Use hand cursor for clicking on nodes. cursor = Cursor(Cursor.DEFAULT_CURSOR) var hit = false for (node in nodes) { From cdd608463ada89f36948761cdc486abf7d177a0c Mon Sep 17 00:00:00 2001 From: MaartenS11 Date: Fri, 6 Feb 2026 14:24:37 +0100 Subject: [PATCH 9/9] Minor cleanup --- .../kotlin/be/ugent/topl/mio/ui/GraphPanel.kt | 44 +------------------ 1 file changed, 1 insertion(+), 43 deletions(-) diff --git a/src/main/kotlin/be/ugent/topl/mio/ui/GraphPanel.kt b/src/main/kotlin/be/ugent/topl/mio/ui/GraphPanel.kt index 975d4cc..0a54b56 100644 --- a/src/main/kotlin/be/ugent/topl/mio/ui/GraphPanel.kt +++ b/src/main/kotlin/be/ugent/topl/mio/ui/GraphPanel.kt @@ -26,7 +26,6 @@ class GraphPanel(private val graph: MultiverseGraph) : JPanel(), addMouseWheelListener(this) } private val textColour = UIManager.getDefaults().getColor("RadioButton.foreground") - //private val borderColour = Color(125, 125, 125) private val borderColour = UIManager.getDefaults().getColor("CheckBox.icon.borderColor") private val primaryColour = UIManager.getDefaults().getColor("Panel.foreground") private val backgroundColour = UIManager.getDefaults().getColor("CheckBox.icon.background") @@ -45,7 +44,6 @@ class GraphPanel(private val graph: MultiverseGraph) : JPanel(), // Panning private var startPos = Point(0, 0) - var associatedScrollPane: JScrollPane? = null var allowSelection = true data class Node(val x: Int, val y: Int, val w: Int, val h: Int, val value: MultiverseNode) @@ -75,13 +73,6 @@ class GraphPanel(private val graph: MultiverseGraph) : JPanel(), } fun drawMiniMap(g: Graphics2D) { - //val scale = min(100.0/renderedHeight, width.toDouble()/renderedWidth) - val scale = min(100.0/renderedHeight, (100.0 * width/height)/renderedWidth) - //g.drawString("camera pos = ($xOffset, $yOffset)", 5, 10) - - val graphWidth = (renderedWidth * scale).roundToInt() - val offset = width - graphWidth - // Zoom str if (scaleFactor != 1.0) { val zoomStr = "${(scaleFactor * 100).roundToInt()}%" @@ -116,27 +107,6 @@ class GraphPanel(private val graph: MultiverseGraph) : JPanel(), g.color = green g.fillRect(xPos.toInt(), 0, 3, barHeight) } - - // Graph rectangle - /*g.color = Color(100, 100, 100, 50) - val rectangle = Rectangle(offset, 0, graphWidth, (renderedHeight * scale).roundToInt()) - g.fillRect(rectangle.x, rectangle.y, rectangle.width, rectangle.height) - g.color = Color(150, 150, 255, 150) - val oldClip = g.clip // If we don't do this the component will be able to draw outside of itself. - g.clip = rectangle - val cameraPosX = (xOffset * scale).roundToInt() - val cameraPosY = (yOffset * scale).roundToInt() - val cameraWidth = (width/scaleFactor * scale).roundToInt() - val cameraHeight = (height/scaleFactor * scale).roundToInt() - g.fillRect(offset + cameraPosX, cameraPosY, cameraWidth, cameraHeight) - g.color = Color(150, 150, 255, 255) - g.drawRect(offset + cameraPosX, cameraPosY, cameraWidth, cameraHeight) - g.clip = oldClip*/ - /*g.color = Color(200, 100, 100, 255) - g.drawString("C", cameraPosX, cameraPosY + 10)*/ - /*g.scale(scale, scale) - drawPaths(g, graph.rootNode) - g.scale(1/scale, 1/scale)*/ } private fun scrollBarPosition(): Int { @@ -408,8 +378,6 @@ class GraphPanel(private val graph: MultiverseGraph) : JPanel(), } println("" + e.x + " " + e.y) - /*associatedScrollPane?.horizontalScrollBar?.value -= delta.x - associatedScrollPane?.verticalScrollBar?.value -= delta.y*/ xOffset -= (delta.x / scaleFactor).toInt() yOffset -= (delta.y / scaleFactor).toInt() repaint() @@ -449,11 +417,7 @@ class GraphPanel(private val graph: MultiverseGraph) : JPanel(), val oldScaleFactor = scaleFactor val adjustment = e.wheelRotation / 20.0 scaleFactor -= adjustment - scaleFactor = kotlin.math.max(0.1, scaleFactor) - /*val oldW = height * oldScaleFactor - val newW = height * scaleFactor - val delta = (newW - oldW)/scaleFactor - yOffset += (delta/2).toInt()*/ + scaleFactor = max(0.1, scaleFactor) val oldH = height/oldScaleFactor val newH = height/scaleFactor @@ -465,12 +429,6 @@ class GraphPanel(private val graph: MultiverseGraph) : JPanel(), val deltaW = (newW - oldW) xOffset -= (deltaW/2).toInt() - /*associatedScrollPane?.horizontalScrollBar?.value = ((associatedScrollPane?.horizontalScrollBar?.value!! / oldScaleFactor) * scaleFactor).toInt() - associatedScrollPane?.verticalScrollBar?.value = ((associatedScrollPane?.verticalScrollBar?.value!! / oldScaleFactor) * scaleFactor).toInt()*/ - println("Scale = $scaleFactor") repaint() - - /*associatedScrollPane?.verticalScrollBar?.revalidate() - associatedScrollPane?.horizontalScrollBar?.revalidate()*/ } } \ No newline at end of file