diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 8c894dda1..733aa42a4 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -44,6 +44,8 @@ gradle-checker-processor = "2.0.2" javafx-plugin = "0.1.0" shadow = "8.1.1" peterabeles-gversion = "1.10.3" +jgraphx = "v4.0.0" +fxgraphics2d = "2.1.4" [libraries] asm-core = { module = "org.ow2.asm:asm", version.ref = "asm" } @@ -129,6 +131,8 @@ treemapfx = { module = "software.coley:treemap-fx", version.ref = "treemapfx" } vineflower = { module = "org.vineflower:vineflower", version.ref = "vineflower" } wordwrap = { module = "com.github.davidmoten:word-wrap", version.ref = "wordwrap" } +jgraphx = { module = "com.github.jgraph:jgraphx", version.ref = "jgraphx" } +fxgraphics2d = { module = "org.jfree:org.jfree.fxgraphics2d", version.ref = "fxgraphics2d" } [bundles] asm = [ diff --git a/recaf-core/src/main/java/software/coley/recaf/services/cfg/ControlFlowGraph.java b/recaf-core/src/main/java/software/coley/recaf/services/cfg/ControlFlowGraph.java new file mode 100644 index 000000000..48684a499 --- /dev/null +++ b/recaf-core/src/main/java/software/coley/recaf/services/cfg/ControlFlowGraph.java @@ -0,0 +1,31 @@ +package software.coley.recaf.services.cfg; + +import org.objectweb.asm.tree.ClassNode; +import org.objectweb.asm.tree.MethodNode; + +import java.util.ArrayList; +import java.util.List; + +public class ControlFlowGraph { + + final List vertices = new ArrayList<>(); + final ClassNode klass; + final MethodNode method; + + public ControlFlowGraph(ClassNode klass, MethodNode method) { + this.klass = klass; + this.method = method; + } + + public List getVertices() { + return vertices; + } + + public ClassNode getKlass() { + return klass; + } + + public MethodNode getMethod() { + return method; + } +} diff --git a/recaf-core/src/main/java/software/coley/recaf/services/cfg/ControlFlowGraphBuilder.java b/recaf-core/src/main/java/software/coley/recaf/services/cfg/ControlFlowGraphBuilder.java new file mode 100644 index 000000000..819ea1873 --- /dev/null +++ b/recaf-core/src/main/java/software/coley/recaf/services/cfg/ControlFlowGraphBuilder.java @@ -0,0 +1,129 @@ +package software.coley.recaf.services.cfg; + +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.tree.*; + +import java.util.HashMap; +import java.util.Map; + +public class ControlFlowGraphBuilder { + + final Map vertexByInsn = new HashMap<>(); + + public ControlFlowGraph build(ClassNode klass, MethodNode method) { + AbstractInsnNode[] insns = method.instructions.toArray(); + if (insns == null || insns.length == 0) { + return null; + } + + ControlFlowGraph graph = new ControlFlowGraph(klass, method); + ControlFlowVertex vertex = new ControlFlowVertex(); + for (AbstractInsnNode insn : insns) { + if (insn.getType() == AbstractInsnNode.LABEL) { + if (insn != insns[0] && !vertex.getInsns().isEmpty()) { + graph.getVertices().add(vertex); + } + + ControlFlowVertex last = vertex; + vertex = new ControlFlowVertex(); + vertex.getInsns().add(insn); + this.vertexByInsn.put(insn, vertex); + + vertex.getInRefs().add(new ControlFlowVertexReference(ControlFlowVertexReferenceKind.LABEL, last, insn)); + last.getOutRefs().add(new ControlFlowVertexReference(ControlFlowVertexReferenceKind.LABEL, vertex, insn)); + + continue; + } + + vertex.getInsns().add(insn); + this.vertexByInsn.put(insn, vertex); + if (this.isTerminalInsn(insn)) { + graph.getVertices().add(vertex); + vertex = new ControlFlowVertex(); + } + } + + for (AbstractInsnNode insn : insns) { + if (!this.isTerminalInsn(insn)) { + continue; + } + ControlFlowVertex v = this.vertexByInsn.get(insn); + if (v == null) { + continue; + } + this.computeRefs(v, insn); + } + + return graph; + } + + boolean isTerminalInsn(AbstractInsnNode insn) { + return insn.getType() == AbstractInsnNode.JUMP_INSN + || (insn.getOpcode() >= Opcodes.IRETURN && insn.getOpcode() <= Opcodes.RETURN); + } + + // FEQ, IFNE, IFLT, IFGE, IFGT, IFLE, IF_ICMPEQ, IF_ICMPNE, IF_ICMPLT, IF_ICMPGE, + // IF_ICMPGT, IF_ICMPLE, IF_ACMPEQ, IF_ACMPNE, GOTO, JSR, IFNULL or IFNONNULL + // TABLESWITCH + void computeRefs(ControlFlowVertex vertex, AbstractInsnNode node) { + if (node.getType() == AbstractInsnNode.JUMP_INSN) { + JumpInsnNode jump = (JumpInsnNode) node; + ControlFlowVertex jumpVertex = this.vertexByInsn.get(jump.label); + ControlFlowVertex nextVertex = node.getNext() == null ? null : this.vertexByInsn.get(node.getNext()); + switch (node.getOpcode()) { + case Opcodes.IFEQ: + case Opcodes.IFNE: + case Opcodes.IFLT: + case Opcodes.IFGE: + case Opcodes.IFGT: + case Opcodes.IFLE: + + case Opcodes.IF_ICMPEQ: + case Opcodes.IF_ICMPNE: + case Opcodes.IF_ICMPLT: + case Opcodes.IF_ICMPGE: + case Opcodes.IF_ICMPGT: + case Opcodes.IF_ICMPLE: + case Opcodes.IF_ACMPEQ: + case Opcodes.IF_ACMPNE: + + case Opcodes.IFNULL: + case Opcodes.IFNONNULL: + if (jumpVertex != null) { + vertex.getOutRefs().add(new ControlFlowVertexReference(ControlFlowVertexReferenceKind.JUMP, jumpVertex, node)); + jumpVertex.getInRefs().add(new ControlFlowVertexReference(ControlFlowVertexReferenceKind.JUMP, vertex, node)); + } + if (nextVertex != null) { + vertex.getOutRefs().add(new ControlFlowVertexReference(ControlFlowVertexReferenceKind.GOTO, nextVertex, node)); + nextVertex.getInRefs().add(new ControlFlowVertexReference(ControlFlowVertexReferenceKind.GOTO, vertex, node)); + } + break; + + case Opcodes.GOTO: + if (jumpVertex != null) { + vertex.getOutRefs().add(new ControlFlowVertexReference(ControlFlowVertexReferenceKind.GOTO, jumpVertex, node)); + jumpVertex.getInRefs().add(new ControlFlowVertexReference(ControlFlowVertexReferenceKind.GOTO, vertex, node)); + } + break; + + } + } else if (node.getType() == AbstractInsnNode.TABLESWITCH_INSN) { + TableSwitchInsnNode tableSwitchInsn = (TableSwitchInsnNode) node; + ControlFlowVertex defaultVertex = this.vertexByInsn.get(tableSwitchInsn.dflt); + if (defaultVertex != null) { + vertex.getOutRefs().add(new ControlFlowVertexReference(ControlFlowVertexReferenceKind.SWITCH_DEFAULT, defaultVertex, node)); + defaultVertex.getOutRefs().add(new ControlFlowVertexReference(ControlFlowVertexReferenceKind.SWITCH_DEFAULT, vertex, node)); + } + + for (LabelNode label : tableSwitchInsn.labels) { + ControlFlowVertex labelVertex = this.vertexByInsn.get(label); + if (labelVertex == null) { + continue; + } + vertex.getOutRefs().add(new ControlFlowVertexReference(ControlFlowVertexReferenceKind.SWITCH, labelVertex, node)); + labelVertex.getOutRefs().add(new ControlFlowVertexReference(ControlFlowVertexReferenceKind.SWITCH, vertex, node)); + } + } + } + +} diff --git a/recaf-core/src/main/java/software/coley/recaf/services/cfg/ControlFlowGraphService.java b/recaf-core/src/main/java/software/coley/recaf/services/cfg/ControlFlowGraphService.java new file mode 100644 index 000000000..25cf78ef8 --- /dev/null +++ b/recaf-core/src/main/java/software/coley/recaf/services/cfg/ControlFlowGraphService.java @@ -0,0 +1,43 @@ +package software.coley.recaf.services.cfg; + +import jakarta.inject.Inject; +import org.objectweb.asm.ClassReader; +import org.objectweb.asm.tree.ClassNode; +import org.objectweb.asm.tree.MethodNode; +import software.coley.recaf.cdi.WorkspaceScoped; +import software.coley.recaf.info.ClassInfo; +import software.coley.recaf.info.JvmClassInfo; +import software.coley.recaf.info.member.MethodMember; +import software.coley.recaf.workspace.model.Workspace; + +@WorkspaceScoped +public class ControlFlowGraphService { + + final Workspace workspace; + + @Inject + public ControlFlowGraphService(Workspace workspace) { + this.workspace = workspace; + } + + public ControlFlowGraph createControlFlow(ClassInfo klass, MethodMember member) { + if (!member.isMethod()) { + return null; + } + JvmClassInfo jvmClassInfo = this.workspace.getPrimaryResource().getJvmClassBundle().get(klass.getName()); + if (jvmClassInfo == null) { + return null; + } + ClassReader reader = jvmClassInfo.getClassReader(); + ClassNode node = new ClassNode(); + reader.accept(node, ClassReader.SKIP_DEBUG); + MethodNode method = node.methods.stream() + .filter(it -> it.name.equals(member.getName()) && it.desc.equals(member.getDescriptor())) + .findFirst() + .orElse(null); + if (method == null) { + return null; + } + return new ControlFlowGraphBuilder().build(node, method); + } +} diff --git a/recaf-core/src/main/java/software/coley/recaf/services/cfg/ControlFlowVertex.java b/recaf-core/src/main/java/software/coley/recaf/services/cfg/ControlFlowVertex.java new file mode 100644 index 000000000..ebf523bc2 --- /dev/null +++ b/recaf-core/src/main/java/software/coley/recaf/services/cfg/ControlFlowVertex.java @@ -0,0 +1,30 @@ +package software.coley.recaf.services.cfg; + +import com.google.common.collect.Iterables; +import org.objectweb.asm.tree.AbstractInsnNode; + +import java.util.ArrayList; +import java.util.List; + +public class ControlFlowVertex { + final List inRefs = new ArrayList<>(); + final List outRefs = new ArrayList<>(); + + final List insns = new ArrayList<>(); + + public List getInRefs() { + return inRefs; + } + + public List getOutRefs() { + return outRefs; + } + + public List getInsns() { + return insns; + } + + public AbstractInsnNode getTerminalNode() { + return Iterables.getLast(this.insns); + } +} diff --git a/recaf-core/src/main/java/software/coley/recaf/services/cfg/ControlFlowVertexReference.java b/recaf-core/src/main/java/software/coley/recaf/services/cfg/ControlFlowVertexReference.java new file mode 100644 index 000000000..c944b88f6 --- /dev/null +++ b/recaf-core/src/main/java/software/coley/recaf/services/cfg/ControlFlowVertexReference.java @@ -0,0 +1,27 @@ +package software.coley.recaf.services.cfg; + +import org.objectweb.asm.tree.AbstractInsnNode; + +public class ControlFlowVertexReference { + final ControlFlowVertexReferenceKind kind; + final ControlFlowVertex vertex; + final AbstractInsnNode insn; + + public ControlFlowVertexReference(ControlFlowVertexReferenceKind kind, ControlFlowVertex vertex, AbstractInsnNode insn) { + this.kind = kind; + this.vertex = vertex; + this.insn = insn; + } + + public ControlFlowVertexReferenceKind getKind() { + return kind; + } + + public ControlFlowVertex getVertex() { + return vertex; + } + + public AbstractInsnNode getInsn() { + return insn; + } +} diff --git a/recaf-core/src/main/java/software/coley/recaf/services/cfg/ControlFlowVertexReferenceKind.java b/recaf-core/src/main/java/software/coley/recaf/services/cfg/ControlFlowVertexReferenceKind.java new file mode 100644 index 000000000..c22f086a6 --- /dev/null +++ b/recaf-core/src/main/java/software/coley/recaf/services/cfg/ControlFlowVertexReferenceKind.java @@ -0,0 +1,9 @@ +package software.coley.recaf.services.cfg; + +public enum ControlFlowVertexReferenceKind { + JUMP, + GOTO, + LABEL, + SWITCH, + SWITCH_DEFAULT +} diff --git a/recaf-ui/build.gradle b/recaf-ui/build.gradle index b114f589c..e35358d01 100644 --- a/recaf-ui/build.gradle +++ b/recaf-ui/build.gradle @@ -24,6 +24,8 @@ dependencies { implementation(libs.reactfx) implementation(libs.richtextfx) implementation(libs.treemapfx) + implementation(libs.jgraphx) + implementation(libs.fxgraphics2d) } application { diff --git a/recaf-ui/src/main/java/software/coley/recaf/services/cell/context/BasicMethodContextMenuProviderFactory.java b/recaf-ui/src/main/java/software/coley/recaf/services/cell/context/BasicMethodContextMenuProviderFactory.java index b76ba331e..e680b27f1 100644 --- a/recaf-ui/src/main/java/software/coley/recaf/services/cell/context/BasicMethodContextMenuProviderFactory.java +++ b/recaf-ui/src/main/java/software/coley/recaf/services/cell/context/BasicMethodContextMenuProviderFactory.java @@ -103,6 +103,7 @@ public ContextMenuProvider getMethodContextMenuProvider(@Nonnull ContextSource s JvmClassBundle jvmBundle = (JvmClassBundle) bundle; JvmClassInfo declaringJvmClass = declaringClass.asJvmClass(); view.item("menu.view.methodcallgraph", FLOW, () -> actions.openMethodCallGraph(workspace, resource, jvmBundle,declaringJvmClass, method)); + view.item("menu.view.methodcfg", FLOW_CONNECTION, () -> actions.openMethodCFG(workspace, resource, jvmBundle,declaringJvmClass, method)); } // TODO: implement additional operations diff --git a/recaf-ui/src/main/java/software/coley/recaf/services/navigation/Actions.java b/recaf-ui/src/main/java/software/coley/recaf/services/navigation/Actions.java index 229202cd3..bac7e635d 100644 --- a/recaf-ui/src/main/java/software/coley/recaf/services/navigation/Actions.java +++ b/recaf-ui/src/main/java/software/coley/recaf/services/navigation/Actions.java @@ -34,6 +34,7 @@ import software.coley.recaf.services.mapping.MappingResults; import software.coley.recaf.services.window.WindowFactory; import software.coley.recaf.ui.control.FontIconView; +import software.coley.recaf.ui.control.cfg.ControlFlowGraphPane; import software.coley.recaf.ui.control.graph.MethodCallGraphsPane; import software.coley.recaf.ui.control.popup.AddMemberPopup; import software.coley.recaf.ui.control.popup.ItemListSelectionPopup; @@ -101,6 +102,7 @@ public class Actions implements Service { private final Instance assemblerPaneProvider; private final Instance documentationPaneProvider; private final Instance callGraphsPaneProvider; + private final Instance cfgPaneProvider; private final Instance stringSearchPaneProvider; private final Instance numberSearchPaneProvider; private final Instance classReferenceSearchPaneProvider; @@ -109,28 +111,28 @@ public class Actions implements Service { @Inject public Actions(@Nonnull ActionsConfig config, - @Nonnull NavigationManager navigationManager, - @Nonnull DockingManager dockingManager, - @Nonnull WindowFactory windowFactory, - @Nonnull TextProviderService textService, - @Nonnull IconProviderService iconService, - @Nonnull PathExportingManager pathExportingManager, - @Nonnull Instance applierProvider, - @Nonnull Instance jvmPaneProvider, - @Nonnull Instance androidPaneProvider, - @Nonnull Instance binaryXmlPaneProvider, - @Nonnull Instance textPaneProvider, - @Nonnull Instance imagePaneProvider, - @Nonnull Instance audioPaneProvider, - @Nonnull Instance videoPaneProvider, - @Nonnull Instance hexPaneProvider, - @Nonnull Instance assemblerPaneProvider, - @Nonnull Instance documentationPaneProvider, - @Nonnull Instance stringSearchPaneProvider, - @Nonnull Instance numberSearchPaneProvider, - @Nonnull Instance callGraphsPaneProvider, - @Nonnull Instance classReferenceSearchPaneProvider, - @Nonnull Instance memberReferenceSearchPaneProvider) { + @Nonnull NavigationManager navigationManager, + @Nonnull DockingManager dockingManager, + @Nonnull WindowFactory windowFactory, + @Nonnull TextProviderService textService, + @Nonnull IconProviderService iconService, + @Nonnull PathExportingManager pathExportingManager, + @Nonnull Instance applierProvider, + @Nonnull Instance jvmPaneProvider, + @Nonnull Instance androidPaneProvider, + @Nonnull Instance binaryXmlPaneProvider, + @Nonnull Instance textPaneProvider, + @Nonnull Instance imagePaneProvider, + @Nonnull Instance audioPaneProvider, + @Nonnull Instance videoPaneProvider, + @Nonnull Instance hexPaneProvider, + @Nonnull Instance assemblerPaneProvider, + @Nonnull Instance documentationPaneProvider, + @Nonnull Instance stringSearchPaneProvider, + @Nonnull Instance numberSearchPaneProvider, + @Nonnull Instance callGraphsPaneProvider, Instance cfgPaneProvider, + @Nonnull Instance classReferenceSearchPaneProvider, + @Nonnull Instance memberReferenceSearchPaneProvider) { this.config = config; this.navigationManager = navigationManager; this.dockingManager = dockingManager; @@ -152,7 +154,8 @@ public Actions(@Nonnull ActionsConfig config, this.stringSearchPaneProvider = stringSearchPaneProvider; this.numberSearchPaneProvider = numberSearchPaneProvider; this.callGraphsPaneProvider = callGraphsPaneProvider; - this.classReferenceSearchPaneProvider = classReferenceSearchPaneProvider; + this.cfgPaneProvider = cfgPaneProvider; + this.classReferenceSearchPaneProvider = classReferenceSearchPaneProvider; this.memberReferenceSearchPaneProvider = memberReferenceSearchPaneProvider; } @@ -1609,6 +1612,25 @@ public Navigable openMethodCallGraph(@Nonnull Workspace workspace, }); } + public Navigable openMethodCFG(@Nonnull Workspace workspace, + @Nonnull WorkspaceResource resource, + @Nonnull JvmClassBundle bundle, + @Nonnull JvmClassInfo declaringClass, + @Nonnull MethodMember method) { + return createContent(() -> { + // Create text/graphic for the tab to create. + String title = Lang.get("menu.view.methodcfg") + ": " + method.getName(); + Node graphic = new FontIconView(CarbonIcons.FLOW); + + // Create content for the tab. + ControlFlowGraphPane content = cfgPaneProvider.get(); + content.onUpdatePath(PathNodes.memberPath(workspace, resource, bundle, declaringClass, method)); + + // Build the tab. + return createTab(dockingManager.getPrimaryRegion(), title, graphic, content); + }); + } + /** * Exports a class, prompting the user to select a location to save the class to. * diff --git a/recaf-ui/src/main/java/software/coley/recaf/ui/control/cfg/ControlFlowGraphPane.java b/recaf-ui/src/main/java/software/coley/recaf/ui/control/cfg/ControlFlowGraphPane.java new file mode 100644 index 000000000..3c3a7dd2e --- /dev/null +++ b/recaf-ui/src/main/java/software/coley/recaf/ui/control/cfg/ControlFlowGraphPane.java @@ -0,0 +1,147 @@ +package software.coley.recaf.ui.control.cfg; + +import com.mxgraph.view.mxGraph; +import jakarta.annotation.Nonnull; +import jakarta.annotation.Nullable; +import jakarta.enterprise.context.Dependent; +import jakarta.inject.Inject; +import javafx.beans.property.ObjectProperty; +import javafx.beans.property.SimpleObjectProperty; +import javafx.geometry.Pos; +import javafx.scene.control.Button; +import javafx.scene.control.Label; +import javafx.scene.layout.*; +import org.kordamp.ikonli.carbonicons.CarbonIcons; +import software.coley.recaf.info.member.ClassMember; +import software.coley.recaf.info.member.MethodMember; +import software.coley.recaf.path.ClassMemberPathNode; +import software.coley.recaf.path.ClassPathNode; +import software.coley.recaf.path.PathNode; +import software.coley.recaf.services.cfg.ControlFlowGraph; +import software.coley.recaf.services.cfg.ControlFlowGraphBuilder; +import software.coley.recaf.services.cfg.ControlFlowGraphService; +import software.coley.recaf.services.navigation.ClassNavigable; +import software.coley.recaf.services.navigation.Navigable; +import software.coley.recaf.services.navigation.UpdatableNavigable; +import software.coley.recaf.ui.control.FontIconView; +import software.coley.recaf.ui.pane.editing.AbstractContentPane; +import software.coley.recaf.util.FxThreadUtil; +import software.coley.recaf.util.Lang; +import software.coley.recaf.util.graph.JavaFXGraphComponent; +import software.coley.recaf.workspace.model.Workspace; + +import java.util.Collection; +import java.util.List; +import java.util.concurrent.CompletableFuture; + +@Dependent +public class ControlFlowGraphPane extends AbstractContentPane> implements ClassNavigable, UpdatableNavigable { + + final ObjectProperty currentMethodInfo = new SimpleObjectProperty<>(); + final ControlFlowGraphView view; + final ControlFlowGraphService service; + ClassPathNode path; + + @Inject + public ControlFlowGraphPane(ControlFlowGraphView view, ControlFlowGraphService service) { + this.view = view; + this.service = service; + getStyleClass().addAll("borderless"); + } + + @Nonnull + @Override + public ClassPathNode getClassPath() { + return path; + } + + @Override + public void requestFocus(@Nonnull ClassMember member) { + + } + + @Override + public void onUpdatePath(@Nonnull PathNode path) { + if (path instanceof ClassMemberPathNode memberPathNode) { + this.path = memberPathNode.getParent(); + ClassMember member = memberPathNode.getValue(); + if (member instanceof MethodMember method) { + currentMethodInfo.setValue(method); + + this.generateDisplay(); + } + } + } + + @Nullable + @Override + public PathNode getPath() { + return path; + } + + @Override + protected void generateDisplay() { + this.setLoading(); + CompletableFuture.supplyAsync(this::createControlFlowGraph) + .thenAccept(graph -> FxThreadUtil.run(() -> this.setGraph(graph))); + } + + mxGraph createControlFlowGraph() { + ControlFlowGraph cfg = this.service.createControlFlow(this.path.getValue(), this.currentMethodInfo.get()); + // TODO: error check + try { + return this.view.createGraph(cfg); + } catch (Throwable throwable) { + throwable.printStackTrace(); + throw throwable; + } + } + + void setLoading() { + Label label = new Label(); + label.textProperty().bind(Lang.getBinding("menu.view.methodcfg.loading")); + HBox controls = new HBox(); + controls.setAlignment(Pos.CENTER); + controls.getChildren().add(label); + this.setCenter(controls); + } + + void setGraph(mxGraph graph) { + JavaFXGraphComponent graphComponent = new JavaFXGraphComponent(); + graphComponent.setGraph(graph); + + HBox controls = new HBox(); + controls.setSpacing(4); + controls.setAlignment(Pos.CENTER_RIGHT); + + Button zoomInButton = new Button(); + zoomInButton.setGraphic(new FontIconView(CarbonIcons.ZOOM_IN)); + + Button zoomOutButton = new Button(); + zoomOutButton.setGraphic(new FontIconView(CarbonIcons.ZOOM_OUT)); + + zoomInButton.setOnMouseClicked(event -> graphComponent.zoomPropertyProperty().set(graphComponent.zoomPropertyProperty().get() + 0.2d)); + zoomOutButton.setOnMouseClicked(event -> graphComponent.zoomPropertyProperty().set(graphComponent.zoomPropertyProperty().get() - 0.2d)); + + controls.getChildren().addAll(zoomInButton, zoomOutButton); + + VBox box = new VBox(); + box.getChildren().add(graphComponent); + graphComponent.setMaxWidth(Double.MAX_VALUE); + box.getChildren().add(controls); + VBox.setVgrow(graphComponent, Priority.ALWAYS); + + this.setCenter(box); + } + + @Nonnull + @Override + public Collection getNavigableChildren() { + return List.of(); + } + + @Override + public void disable() { + + } +} \ No newline at end of file diff --git a/recaf-ui/src/main/java/software/coley/recaf/ui/control/cfg/ControlFlowGraphView.java b/recaf-ui/src/main/java/software/coley/recaf/ui/control/cfg/ControlFlowGraphView.java new file mode 100644 index 000000000..fd7513107 --- /dev/null +++ b/recaf-ui/src/main/java/software/coley/recaf/ui/control/cfg/ControlFlowGraphView.java @@ -0,0 +1,253 @@ +package software.coley.recaf.ui.control.cfg; + +import com.google.common.collect.HashBiMap; +import com.mxgraph.canvas.mxGraphics2DCanvas; +import com.mxgraph.shape.mxRectangleShape; +import com.mxgraph.util.mxConstants; +import com.mxgraph.view.mxCellState; +import com.mxgraph.view.mxGraph; +import com.mxgraph.view.mxStylesheet; +import dev.xdark.blw.asm.AsmBytecodeLibrary; +import dev.xdark.blw.asm.ClassWriterProvider; +import dev.xdark.blw.asm.internal.Util; +import dev.xdark.blw.classfile.ClassBuilder; +import dev.xdark.blw.classfile.Method; +import dev.xdark.blw.classfile.generic.GenericClassBuilder; +import dev.xdark.blw.classfile.generic.GenericMethod; +import dev.xdark.blw.code.CodeElement; +import dev.xdark.blw.code.generic.GenericCode; +import dev.xdark.blw.code.generic.GenericLabel; +import dev.xdark.blw.code.instruction.*; +import dev.xdark.blw.type.MethodType; +import dev.xdark.blw.type.Types; +import dev.xdark.blw.util.Reflectable; +import jakarta.enterprise.context.Dependent; +import jakarta.inject.Inject; +import me.darknet.assembler.compile.analysis.jvm.IndexedStraightforwardSimulation; +import me.darknet.assembler.helper.Variables; +import me.darknet.assembler.printer.InstructionPrinter; +import me.darknet.assembler.printer.PrintContext; +import org.objectweb.asm.ClassWriter; +import org.objectweb.asm.Label; +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.Type; +import org.objectweb.asm.tree.*; +import software.coley.recaf.services.cfg.ControlFlowGraph; +import software.coley.recaf.services.cfg.ControlFlowVertex; +import software.coley.recaf.services.cfg.ControlFlowVertexReference; +import software.coley.recaf.ui.control.cfg.layout.PatchedHierarchicalLayout; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.StringWriter; +import java.util.*; +import java.util.List; + +@Dependent +public class ControlFlowGraphView { + + final ControlFlowHighlighter highlighter; + + @Inject + public ControlFlowGraphView(ControlFlowHighlighter highlighter) { + this.highlighter = highlighter; + } + + public mxGraph createGraph(ControlFlowGraph cfg) { + mxGraph graph = new mxGraph(); + graph.setHtmlLabels(true); + graph.setAutoOrigin(true); + graph.setAutoSizeCells(true); + graph.setAllowLoops(true); + graph.setAllowDanglingEdges(true); + this.setStyles(graph); + + graph.getModel().beginUpdate(); + HashMap cells = new HashMap<>(); + Map views = this.createVerticesViews(cfg); + for (ControlFlowVertex vertex : cfg.getVertices()) { + Object cell = graph.insertVertex(graph.getDefaultParent(), null, views.get(vertex), + 10, 10, 80, 80, this.getCellStyle()); + graph.updateCellSize(cell); + cells.put(vertex, cell); + } + + for (ControlFlowVertex vertex : cfg.getVertices()) { + Object cell = cells.get(vertex); + + for (ControlFlowVertexReference ref : vertex.getOutRefs()) { + Object refCell = cells.get(ref.getVertex()); + if (refCell == null) { + continue; + } + + graph.insertEdge(graph.getDefaultParent(), null, "", cell, refCell, this.getEdgeStyle(vertex, ref)); + } + } + + PatchedHierarchicalLayout layout = new PatchedHierarchicalLayout(graph); + layout.setFineTuning(true); + layout.setIntraCellSpacing(20d); + layout.setInterRankCellSpacing(50d); + layout.setDisableEdgeStyle(true); + layout.setParallelEdgeSpacing(100d); + layout.setUseBoundingBox(true); + layout.execute(graph.getDefaultParent()); + + graph.getModel().endUpdate(); + + return graph; + } + + void setStyles(mxGraph graph) { + Map edgeStyle = graph.getStylesheet().getDefaultEdgeStyle(); + edgeStyle.put(mxConstants.STYLE_ROUNDED, true); + edgeStyle.put(mxConstants.STYLE_ELBOW, mxConstants.ELBOW_VERTICAL); + edgeStyle.put(mxConstants.STYLE_ENDARROW, mxConstants.ARROW_DIAMOND); + edgeStyle.put(mxConstants.STYLE_TARGET_PERIMETER_SPACING, 1d); + edgeStyle.put(mxConstants.STYLE_STROKEWIDTH, 1.25d); + + Map vertexStyle = graph.getStylesheet().getDefaultVertexStyle(); + vertexStyle.put(mxConstants.STYLE_AUTOSIZE, 1); + vertexStyle.put(mxConstants.STYLE_SPACING, "5"); + vertexStyle.put(mxConstants.STYLE_ORTHOGONAL, "true"); + vertexStyle.put(mxConstants.STYLE_ROUNDED, true); + vertexStyle.put(mxConstants.STYLE_ARCSIZE, 5); + vertexStyle.put(mxConstants.STYLE_ALIGN, mxConstants.ALIGN_LEFT); + mxGraphics2DCanvas.putShape(mxConstants.SHAPE_RECTANGLE, new mxRectangleShape() { + @Override + protected int getArcSize(mxCellState state, double w, double h) { + return 10; + } + }); + mxStylesheet stylesheet = new mxStylesheet(); + stylesheet.setDefaultEdgeStyle(edgeStyle); + stylesheet.setDefaultVertexStyle(vertexStyle); + + graph.setStylesheet(stylesheet); + } + + Map createVerticesViews(ControlFlowGraph graph) { + Map views = new HashMap<>(); + Map labelNames = new HashMap<>(); + Map labelIndexes = new HashMap<>(); + + int idx = 1; + for (AbstractInsnNode insn : graph.getMethod().instructions.toArray()) { + if (insn.getType() == AbstractInsnNode.LABEL) { + labelIndexes.put((LabelNode) insn, idx); + labelNames.put(idx, "label" + idx); + idx++; + } + } + + IndexedStraightforwardSimulation simulation = new IndexedStraightforwardSimulation(); + for (ControlFlowVertex vertex : graph.getVertices()) { + List elements = vertex.getInsns() + .stream() + .map(insn -> liftInsn(insn, labelIndexes)) + .filter(Objects::nonNull) + .toList(); + + GenericCode code = new GenericCode(0, 0, elements, List.of(), List.of()); + GenericMethod method = new GenericMethod(0, null, null, null, null, + null, code, null, null, null); + + StringWriter writer = new StringWriter(); + PrintContext> printContext = new PrintContext<>(" ", writer); + + InstructionPrinter printer = new InstructionPrinter(new PrintContext.CodePrint(printContext), + method.code(), new Variables(new TreeMap<>(), List.of()), labelNames); + simulation.execute(printer, method); + + String view = this.highlighter.highlightToHtml("jasm", writer.toString()); + views.put(vertex, view); + } + + + return views; + } + + String getCellStyle() { + return "fillColor=#2f343d;fontColor=#F4FEFF;strokeColor=#343A44"; + } + + String getEdgeStyle(ControlFlowVertex vertex, ControlFlowVertexReference ref) { + return "strokeColor=" + getEdgeColor(vertex, ref); + } + + String getEdgeColor(ControlFlowVertex vertex, ControlFlowVertexReference ref) { + switch (ref.getKind()) { + case JUMP: + return "#009231"; + case LABEL: + case SWITCH_DEFAULT: + return "#e63ce0"; + case GOTO: { + if (vertex.getOutRefs().size() == 1) { + return "#777c85"; + } + return "#ea2027"; + } + case SWITCH: + return "ffc312"; + default: + return "#777c85"; + } + } + + static CodeElement liftInsn(AbstractInsnNode insn, Map labelIndexes) { + if (insn instanceof LdcInsnNode ldc) { + return Util.wrapLdcInsn(ldc.cst); + } else if (insn instanceof MethodInsnNode min) { + return Util.wrapMethodInsn(min.getOpcode(), min.owner, min.name, min.desc, false); + } else if (insn instanceof FieldInsnNode fin) { + return Util.wrapFieldInsn(fin.getOpcode(), fin.owner, fin.name, fin.desc); + } else if (insn instanceof TypeInsnNode tin) { + return Util.wrapTypeInsn(tin.getOpcode(), tin.desc); + } else if (insn instanceof IntInsnNode iin) { + return Util.wrapIntInsn(iin.getOpcode(), iin.operand); + } else if (insn instanceof InsnNode in) { + return Util.wrapInsn(in.getOpcode()); + } else if (insn instanceof InvokeDynamicInsnNode indy) { + return Util.wrapInvokeDynamicInsn(indy.name, indy.desc, indy.bsm, indy.bsmArgs); + } else if (insn instanceof IincInsnNode iin) { + return new VariableIncrementInstruction(iin.var, iin.incr); + } else if (insn instanceof LabelNode labelNode) { + return getLabel(labelNode, labelIndexes); + } else if (insn instanceof LineNumberNode line) { + getLabel(line.start, labelIndexes).setLineNumber(line.line); + } else if (insn instanceof JumpInsnNode jump) { + if (jump.getOpcode() == Opcodes.GOTO) { + return new ImmediateJumpInstruction(Opcodes.GOTO, getLabel(jump.label, labelIndexes)); + } else { + return new ConditionalJumpInstruction(jump.getOpcode(), getLabel(jump.label, labelIndexes)); + } + } else if (insn instanceof LookupSwitchInsnNode lsinsn) { + return new LookupSwitchInstruction( + lsinsn.keys.stream().mapToInt(it -> it).toArray(), + getLabel(lsinsn.dflt, labelIndexes), + lsinsn.labels.stream().map(it -> ControlFlowGraphView.getLabel(it, labelIndexes)).toList() + ); + } else if (insn instanceof TableSwitchInsnNode tsinsn) { + return new TableSwitchInstruction( + tsinsn.min, + getLabel(tsinsn.dflt, labelIndexes), + tsinsn.labels.stream().map(it -> ControlFlowGraphView.getLabel(it, labelIndexes)).toList() + ); + } else if (insn instanceof MultiANewArrayInsnNode manainsn) { + return new AllocateMultiDimArrayInstruction(Types.arrayTypeFromDescriptor(manainsn.desc), manainsn.dims); + } else if (insn instanceof VarInsnNode varinsn) { + return new VarInstruction(varinsn.getOpcode(), varinsn.var); + } + + return null; + } + + static dev.xdark.blw.code.Label getLabel(LabelNode label, Map labelIndexes) { + GenericLabel genericLabel = new GenericLabel(); + genericLabel.setIndex(labelIndexes.getOrDefault(label, -1)); + return genericLabel; + } + +} diff --git a/recaf-ui/src/main/java/software/coley/recaf/ui/control/cfg/ControlFlowHighlighter.java b/recaf-ui/src/main/java/software/coley/recaf/ui/control/cfg/ControlFlowHighlighter.java new file mode 100644 index 000000000..cd289c466 --- /dev/null +++ b/recaf-ui/src/main/java/software/coley/recaf/ui/control/cfg/ControlFlowHighlighter.java @@ -0,0 +1,97 @@ +package software.coley.recaf.ui.control.cfg; + +import com.google.common.collect.Iterables; +import jakarta.inject.Inject; +import javafx.css.*; +import javafx.scene.paint.Color; +import org.fxmisc.richtext.model.StyleSpan; +import org.fxmisc.richtext.model.StyleSpans; +import software.coley.recaf.cdi.WorkspaceScoped; +import software.coley.recaf.ui.control.richtext.syntax.RegexLanguages; +import software.coley.recaf.ui.control.richtext.syntax.RegexSyntaxHighlighter; + +import javax.swing.text.BadLocationException; +import javax.swing.text.html.HTMLDocument; +import java.awt.*; +import java.io.IOException; +import java.net.URL; +import java.util.*; + +@WorkspaceScoped +public class ControlFlowHighlighter { + + final Map styleSheets = new HashMap<>(); + + @Inject + public ControlFlowHighlighter() { + this.loadStyleSheet("jasm", this.getClass().getClassLoader().getResource("syntax/jasm.css")); + } + + void loadStyleSheet(String name, URL url) { + CssParser parser = new CssParser(); + try { + Stylesheet sheet = parser.parse(url); + Map classes = new HashMap<>(); + + for (Rule rule : sheet.getRules()) { + Map properties = new HashMap<>(); + for (Declaration decl : rule.getDeclarations()) { + properties.put(decl.getProperty(), new StyleSheetProperty(decl.getProperty(), decl.getParsedValue())); + } + for (Selector selector : rule.getSelectors()) { + for (Selector ruleSelector : selector.getRule().getSelectors()) { + if (ruleSelector instanceof SimpleSelector simpleSelector) { + for (StyleClass styleClass : simpleSelector.getStyleClassSet()) { + classes.put(styleClass.getStyleClassName(), new StyleSheetClass(properties)); + } + } else if (ruleSelector instanceof CompoundSelector compoundSelector) { + for (SimpleSelector compoundSelectorSelector : compoundSelector.getSelectors()) { + for (StyleClass styleClass : compoundSelectorSelector.getStyleClassSet()) { + classes.put(styleClass.getStyleClassName(), new StyleSheetClass(properties)); + } + } + } + } + } + } + this.styleSheets.put(name, new StyleSheet(classes)); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + public String highlightToHtml(String name, String text) { + RegexSyntaxHighlighter highlighter = new RegexSyntaxHighlighter(RegexLanguages.getJasmLanguage()); + StyleSpans> spans = highlighter.createStyleSpans(text, 0, text.length()); + StringBuilder builder = new StringBuilder(); + int idx = 0; + for (StyleSpan> span : spans) { + String spanText = text.substring(idx, idx + span.getLength()); + String style = Iterables.getFirst(span.getStyle(), null); + StyleSheet styleSheet = style == null ? null : this.styleSheets.get(name); + if (styleSheet == null) { + builder.append(String.format("%s", spanText)); + } else { + String color = Optional.ofNullable(styleSheet.classes().get(style)) + .flatMap(it -> Optional.ofNullable(it.properties().get("-fx-fill"))) + .map(it -> (ParsedValue) it.value()) + .filter(it -> it.getValue() instanceof Color) + .map(it -> toHexString((Color) it.getValue())) + .orElse("#F4FEFF"); + builder.append(String.format("%s", color, spanText)); + } + idx += span.getLength(); + } + return builder.toString(); + } + + record StyleSheet(Map classes) {} + record StyleSheetClass(Map properties) {} + record StyleSheetProperty(String name, Object value) {} + + static String toHexString(Color color) { + return String.format("#%02X%02X%02X", (int) (color.getRed() * 255.0d), (int) (color.getGreen() * 255.0d), + (int) (color.getBlue() * 255.0d)); + } + +} diff --git a/recaf-ui/src/main/java/software/coley/recaf/ui/control/cfg/layout/PatchedCoordinateAssignment.java b/recaf-ui/src/main/java/software/coley/recaf/ui/control/cfg/layout/PatchedCoordinateAssignment.java new file mode 100644 index 000000000..cd76a539c --- /dev/null +++ b/recaf-ui/src/main/java/software/coley/recaf/ui/control/cfg/layout/PatchedCoordinateAssignment.java @@ -0,0 +1,147 @@ +// Copyright GFI 2017 - Data Systemizer +package software.coley.recaf.ui.control.cfg.layout; + +import com.mxgraph.layout.hierarchical.model.mxGraphAbstractHierarchyCell; +import com.mxgraph.layout.hierarchical.model.mxGraphHierarchyEdge; +import com.mxgraph.layout.hierarchical.model.mxGraphHierarchyModel; +import com.mxgraph.layout.hierarchical.model.mxGraphHierarchyNode; +import com.mxgraph.layout.hierarchical.mxHierarchicalLayout; +import com.mxgraph.layout.hierarchical.stage.mxCoordinateAssignment; +import com.mxgraph.model.mxCell; +import com.mxgraph.view.mxGraph; + +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +/** + * Patched hierarchical layout to route directly + * cross-group edges + * + * @author Loison + */ +public class PatchedCoordinateAssignment extends mxCoordinateAssignment { + + /** + * Constructor + * + * @param layout + * @param intraCellSpacing + * @param interRankCellSpacing + * @param orientation + * @param initialX + * @param parallelEdgeSpacing + */ + public PatchedCoordinateAssignment(mxHierarchicalLayout layout, double intraCellSpacing, double interRankCellSpacing, + int orientation, double initialX, double parallelEdgeSpacing) { + super(layout, intraCellSpacing, interRankCellSpacing, orientation, initialX, parallelEdgeSpacing); + } + + /** + * Sets the cell locations in the facade to those + * stored after this layout + * processing step has completed. + * + * @param graph the facade describing the input graph + * @param model an internal model of the hierarchical + * layout + */ + @Override + protected void setCellLocations(mxGraph graph, mxGraphHierarchyModel model) { + rankTopY = new double[model.ranks.size()]; + rankBottomY = new double[model.ranks.size()]; + + for (int i = 0; i < model.ranks.size(); i++) { + rankTopY[i] = Double.MAX_VALUE; + rankBottomY[i] = -Double.MAX_VALUE; + } + + Set parentsChanged = null; + + if (layout.isResizeParent()) { + parentsChanged = new HashSet<>(); + } + + Map edges = model.getEdgeMapper(); + Map vertices = model.getVertexMapper(); + + // Process vertices all first, since they define the + // lower and + // limits of each rank. Between these limits lie the + // channels + // where the edges can be routed across the graph + + for (mxGraphHierarchyNode cell : vertices.values()) { + setVertexLocation(cell); + + if (layout.isResizeParent()) { + parentsChanged.add(graph.getModel().getParent(cell.cell)); + } + } + + if (layout.isResizeParent()) { + adjustParents(parentsChanged); + } + + // MODIF FLO : enum is not visible + + // Post process edge styles. Needs the vertex + // locations set for initial + // values of the top and bottoms of each rank + //if (this.edgeStyle == HierarchicalEdgeStyle.ORTHOGONAL + // || this.edgeStyle == HierarchicalEdgeStyle + // .POLYLINE) + //{ + localEdgeProcessing(model); + //} + + // MODIF FLO : remove jetty and ranks for + // cross-groups edges : they are garbled + + for (mxGraphAbstractHierarchyCell cell : edges.values()) { + mxGraphHierarchyEdge edge = (mxGraphHierarchyEdge) cell; + + // Cross group edge? + boolean isCrossGroupEdge = isCrossGroupEdge(edge); + + if (isCrossGroupEdge) { + + // Clear jettys + this.jettyPositions.remove(edge); + + // Clear min and max ranks + edge.minRank = -1; + edge.maxRank = -1; + + } + + } + // end MODIF FLO + for (mxGraphAbstractHierarchyCell cell : edges.values()) { + setEdgePosition(cell); + } + } + + public boolean isCrossGroupEdge(mxGraphHierarchyEdge edge) { + + // Cross group edge? + boolean isCrossGroupEdge = false; + + for (Object objCell : edge.edges) { + + mxCell edgeCell = (mxCell) objCell; + + // Edge parent same as source parent? + isCrossGroupEdge = isCrossGroupEdge || (!edgeCell.getParent().equals(edgeCell.getSource().getParent())); + // Edge parent same as target parent? + isCrossGroupEdge = isCrossGroupEdge || (!edgeCell.getParent().equals(edgeCell.getTarget().getParent())); + + if (isCrossGroupEdge) { + // Finished + break; + } + + } + return isCrossGroupEdge; + } +} diff --git a/recaf-ui/src/main/java/software/coley/recaf/ui/control/cfg/layout/PatchedHierarchicalLayout.java b/recaf-ui/src/main/java/software/coley/recaf/ui/control/cfg/layout/PatchedHierarchicalLayout.java new file mode 100644 index 000000000..633f15d9b --- /dev/null +++ b/recaf-ui/src/main/java/software/coley/recaf/ui/control/cfg/layout/PatchedHierarchicalLayout.java @@ -0,0 +1,41 @@ +// Copyright GFI 2017 - Data Systemizer +package software.coley.recaf.ui.control.cfg.layout; + +import com.mxgraph.layout.hierarchical.mxHierarchicalLayout; +import com.mxgraph.layout.hierarchical.stage.mxCoordinateAssignment; +import com.mxgraph.view.mxGraph; + +/** + * Patched hierarchical layout to route directly + * cross-group edges + * + * @author Loison + */ +public class PatchedHierarchicalLayout extends mxHierarchicalLayout { + + public PatchedHierarchicalLayout(mxGraph graph) { + super(graph); + } + + public PatchedHierarchicalLayout(mxGraph graph, int orientation) { + super(graph, orientation); + } + + /** + * Executes the placement stage using + * mxCoordinateAssignment. + *

+ * Use a patched mxCoordinateAssignment class + */ + @Override + public double placementStage(double initialX, Object parent) { + mxCoordinateAssignment placementStage = + new PatchedCoordinateAssignment(this, intraCellSpacing, interRankCellSpacing, orientation, initialX, + parallelEdgeSpacing); + placementStage.setFineTuning(fineTuning); + placementStage.execute(parent); + + return placementStage.getLimitX() + interHierarchySpacing; + } + +} diff --git a/recaf-ui/src/main/java/software/coley/recaf/ui/pane/editing/assembler/AssemblerPane.java b/recaf-ui/src/main/java/software/coley/recaf/ui/pane/editing/assembler/AssemblerPane.java index 6b39b8f34..d5e65355d 100644 --- a/recaf-ui/src/main/java/software/coley/recaf/ui/pane/editing/assembler/AssemblerPane.java +++ b/recaf-ui/src/main/java/software/coley/recaf/ui/pane/editing/assembler/AssemblerPane.java @@ -367,6 +367,7 @@ private CompletableFuture>> parseAST() { // The transform failed. lastPartialAst = partialAst; eachChild(AssemblerAstConsumer.class, c -> c.consumeAst(partialAst, AstPhase.CONCRETE_PARTIAL)); + processErrors(errors, ProblemPhase.LINT); }); } diff --git a/recaf-ui/src/main/java/software/coley/recaf/util/graph/JavaFXGraphComponent.java b/recaf-ui/src/main/java/software/coley/recaf/util/graph/JavaFXGraphComponent.java new file mode 100644 index 000000000..d08bc85ae --- /dev/null +++ b/recaf-ui/src/main/java/software/coley/recaf/util/graph/JavaFXGraphComponent.java @@ -0,0 +1,113 @@ +package software.coley.recaf.util.graph; + +import com.mxgraph.canvas.mxGraphics2DCanvas; +import com.mxgraph.util.mxEvent; +import com.mxgraph.util.mxEventObject; +import com.mxgraph.util.mxRectangle; +import com.mxgraph.view.mxGraph; +import javafx.beans.property.*; +import javafx.beans.value.ObservableValue; +import javafx.scene.canvas.Canvas; +import javafx.scene.control.ScrollPane; +import org.jfree.fx.FXGraphics2D; + +import java.awt.*; + +public class JavaFXGraphComponent extends ScrollPane { + + static final double SCROLL_ZOOM = 0.2; + + final ObjectProperty graphProperty = new SimpleObjectProperty(); + final DoubleProperty zoomProperty = new SimpleDoubleProperty(1.0); + + Canvas canvas; + Graphics2D awtGraphics; + mxGraphics2DCanvas mxCanvas; + + public JavaFXGraphComponent() { + this.setPannable(true); + + this.graphProperty.addListener(this::onGraphChanged); + this.zoomProperty.addListener(this::onZoomChanged); + + this.widthProperty().addListener((observable, oldValue, newValue) -> this.repaint()); + this.heightProperty().addListener((observable, oldValue, newValue) -> this.repaint()); + + this.setOnScroll(event -> { + if (event.isControlDown()) { + double zoom = Math.signum(event.getDeltaY()) * SCROLL_ZOOM; + this.zoomProperty.set(this.zoomProperty.get() + zoom); + event.consume(); + } + }); + } + + public void setGraph(mxGraph graph) { + this.graphProperty.set(graph); + } + + void onZoomChanged(ObservableValue observable, Number oldValue, Number newValue) { + double zoom = newValue.doubleValue(); + + mxGraph graph = this.graphProperty.get(); + graph.getView().scaleAndTranslate(zoom, 0, 0); + + this.createCanvas(this.graphProperty.get()); + this.repaint(); + } + + void onGraphChanged(ObservableValue observable, mxGraph oldValue, mxGraph newValue) { + if (oldValue != null) { + oldValue.removeListener(this::onGraphRepaint); + } + + newValue.addListener(mxEvent.REPAINT, this::onGraphRepaint); + this.createCanvas(newValue); + this.repaint(); + } + + void createCanvas(mxGraph graph) { + mxRectangle bounds = graph.getView().getGraphBounds(); + int border = graph.getBorder(); + int margin = 20; + this.canvas = new Canvas(bounds.getX() + bounds.getWidth() + border * 2 + margin, + bounds.getY() + bounds.getHeight() + border * 2 + margin); + this.setContent(this.canvas); + this.awtGraphics = new FXGraphics2D(this.canvas.getGraphicsContext2D()); + + this.mxCanvas = new mxGraphics2DCanvas(this.awtGraphics); + this.mxCanvas.setScale(graph.getView().getScale()); + } + + void onGraphRepaint(Object o, mxEventObject mxEventObject) { + this.repaint(); + } + + void repaint() { + mxGraph graph = this.graphProperty.get(); + if (this.canvas == null) { + this.createCanvas(graph); + return; + } + + final var gc = this.canvas.getGraphicsContext2D(); + gc.clearRect(0, 0, this.canvas.getWidth(), this.canvas.getHeight()); + + Graphics2D g = (Graphics2D) this.awtGraphics.create(); + try { + g.translate(20, 20); + this.mxCanvas.setGraphics(g); + graph.drawGraph(this.mxCanvas); + } finally { + g.dispose(); + } + } + + public ObjectProperty graphPropertyProperty() { + return graphProperty; + } + + public DoubleProperty zoomPropertyProperty() { + return zoomProperty; + } +} diff --git a/recaf-ui/src/main/resources/translations/en_US.lang b/recaf-ui/src/main/resources/translations/en_US.lang index c3781a958..aa3efb36f 100644 --- a/recaf-ui/src/main/resources/translations/en_US.lang +++ b/recaf-ui/src/main/resources/translations/en_US.lang @@ -97,6 +97,8 @@ menu.view.methodcallgraph=Call Graph menu.view.methodcallgraph.calls=Calls menu.view.methodcallgraph.callers=Callers menu.view.methodcallgraph.focus=Focus on Method +menu.view.methodcfg=Control flow +menu.view.methodcfg.loading=Building control flow graph... menu.tab.close=Close menu.tab.closeothers=Close others menu.tab.closeall=Close all