diff --git a/recaf-ui/src/main/java/software/coley/recaf/services/cell/context/BasicClassContextMenuProviderFactory.java b/recaf-ui/src/main/java/software/coley/recaf/services/cell/context/BasicClassContextMenuProviderFactory.java index d17cf8396..f0a1613d8 100644 --- a/recaf-ui/src/main/java/software/coley/recaf/services/cell/context/BasicClassContextMenuProviderFactory.java +++ b/recaf-ui/src/main/java/software/coley/recaf/services/cell/context/BasicClassContextMenuProviderFactory.java @@ -150,6 +150,7 @@ private void populateJvmMenu(@Nonnull ContextMenu menu, )); edit.infoItem("menu.edit.add.field", ADD_ALT, actions::addClassField); edit.infoItem("menu.edit.add.method", ADD_ALT, actions::addClassMethod); + edit.infoItem("menu.edit.override.method", HEALTH_CROSS, actions::overrideClassMethod); edit.infoItem("menu.edit.remove.field", CLOSE, actions::deleteClassFields).disableWhen(info.getFields().isEmpty()); edit.infoItem("menu.edit.remove.method", CLOSE, actions::deleteClassMethods).disableWhen(info.getMethods().isEmpty()); edit.infoItem("menu.edit.remove.annotation", CLOSE, actions::deleteClassAnnotations).disableWhen(info.getAnnotations().isEmpty()); 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 fafbac3fd..78cd45b40 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 @@ -27,8 +27,10 @@ import software.coley.recaf.info.member.MethodMember; import software.coley.recaf.path.*; import software.coley.recaf.services.Service; +import software.coley.recaf.services.cell.CellConfigurationService; import software.coley.recaf.services.cell.icon.IconProviderService; import software.coley.recaf.services.cell.text.TextProviderService; +import software.coley.recaf.services.inheritance.InheritanceGraph; import software.coley.recaf.services.mapping.IntermediateMappings; import software.coley.recaf.services.mapping.MappingApplier; import software.coley.recaf.services.mapping.MappingResults; @@ -39,6 +41,7 @@ import software.coley.recaf.ui.control.popup.ItemListSelectionPopup; import software.coley.recaf.ui.control.popup.ItemTreeSelectionPopup; import software.coley.recaf.ui.control.popup.NamePopup; +import software.coley.recaf.ui.control.popup.OverrideMethodPopup; import software.coley.recaf.ui.docking.DockingManager; import software.coley.recaf.ui.docking.DockingRegion; import software.coley.recaf.ui.docking.DockingTab; @@ -88,7 +91,9 @@ public class Actions implements Service { private final WindowFactory windowFactory; private final TextProviderService textService; private final IconProviderService iconService; + private final CellConfigurationService cellConfigurationService; private final PathExportingManager pathExportingManager; + private final Instance graphProvider; private final Instance applierProvider; private final Instance jvmPaneProvider; private final Instance androidPaneProvider; @@ -114,7 +119,9 @@ public Actions(@Nonnull ActionsConfig config, @Nonnull WindowFactory windowFactory, @Nonnull TextProviderService textService, @Nonnull IconProviderService iconService, + @Nonnull CellConfigurationService cellConfigurationService, @Nonnull PathExportingManager pathExportingManager, + @Nonnull Instance graphProvider, @Nonnull Instance applierProvider, @Nonnull Instance jvmPaneProvider, @Nonnull Instance androidPaneProvider, @@ -137,7 +144,9 @@ public Actions(@Nonnull ActionsConfig config, this.windowFactory = windowFactory; this.textService = textService; this.iconService = iconService; + this.cellConfigurationService = cellConfigurationService; this.pathExportingManager = pathExportingManager; + this.graphProvider= graphProvider; this.applierProvider = applierProvider; this.jvmPaneProvider = jvmPaneProvider; this.androidPaneProvider = androidPaneProvider; @@ -1992,6 +2001,42 @@ public void addClassMethod(@Nonnull Workspace workspace, }).forMethod(info).show(); } + /** + * Prompts the user for method declaration info, to add it to the given class. + * + * @param workspace + * Containing workspace. + * @param resource + * Containing resource. + * @param bundle + * Containing bundle. + * @param info + * Class to update. + */ + public void overrideClassMethod(@Nonnull Workspace workspace, + @Nonnull WorkspaceResource resource, + @Nonnull JvmClassBundle bundle, + @Nonnull JvmClassInfo info) { + InheritanceGraph graph = graphProvider.get(); + if (graph == null) + return; + new OverrideMethodPopup(this, cellConfigurationService, graph, workspace, info, (methodOwner, method) -> { + ClassWriter writer = new ClassWriter(0); + info.getClassReader().accept(new MemberStubAddingVisitor(writer, method), 0); + JvmClassInfo updatedInfo = info.toJvmClassBuilder() + .adaptFrom(writer.toByteArray()) + .build(); + bundle.put(updatedInfo); + + // Open the assembler with the new method + try { + openAssembler(PathNodes.memberPath(workspace, resource, bundle, updatedInfo, method)); + } catch (IncompletePathException e) { + logger.error("Failed to open assembler for new method", e); + } + }).show(); + } + /** * Makes the given methods no-op. * diff --git a/recaf-ui/src/main/java/software/coley/recaf/ui/control/popup/OverrideMethodPopup.java b/recaf-ui/src/main/java/software/coley/recaf/ui/control/popup/OverrideMethodPopup.java new file mode 100644 index 000000000..a8c4996e8 --- /dev/null +++ b/recaf-ui/src/main/java/software/coley/recaf/ui/control/popup/OverrideMethodPopup.java @@ -0,0 +1,97 @@ +package software.coley.recaf.ui.control.popup; + +import atlantafx.base.controls.Spacer; +import jakarta.annotation.Nonnull; +import javafx.geometry.Insets; +import javafx.geometry.Pos; +import javafx.scene.control.Button; +import javafx.scene.control.TreeItem; +import javafx.scene.layout.HBox; +import javafx.scene.layout.VBox; +import javafx.scene.paint.Color; +import software.coley.recaf.info.ClassInfo; +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.path.PathNodes; +import software.coley.recaf.path.WorkspacePathNode; +import software.coley.recaf.services.cell.CellConfigurationService; +import software.coley.recaf.services.inheritance.InheritanceGraph; +import software.coley.recaf.services.inheritance.InheritanceVertex; +import software.coley.recaf.services.navigation.Actions; +import software.coley.recaf.ui.control.ActionButton; +import software.coley.recaf.ui.control.FontIconView; +import software.coley.recaf.ui.control.PathNodeTree; +import software.coley.recaf.ui.window.RecafScene; +import software.coley.recaf.ui.window.RecafStage; +import software.coley.recaf.util.Lang; +import software.coley.recaf.workspace.model.Workspace; + +import java.util.function.BiConsumer; + +import static org.kordamp.ikonli.carbonicons.CarbonIcons.CHECKMARK; +import static org.kordamp.ikonli.carbonicons.CarbonIcons.CLOSE; + +/** + * Popup to show a selection of method to override from parent classes. + * + * @author Matt Coley + */ +public class OverrideMethodPopup extends RecafStage { + private final PathNodeTree tree; + + public OverrideMethodPopup(@Nonnull Actions actions, @Nonnull CellConfigurationService configurationService, + @Nonnull InheritanceGraph graph, @Nonnull Workspace workspace, + @Nonnull ClassInfo targetClass, @Nonnull BiConsumer memberConsumer) { + WorkspacePathNode rootPath = PathNodes.workspacePath(workspace); + TreeItem> rootItem = new TreeItem<>(rootPath); + tree = new PathNodeTree(configurationService, actions); + tree.setShowRoot(false); + tree.setRoot(rootItem); + tree.getStyleClass().add("border-muted"); + + // Setup tree contents + InheritanceVertex vertex = graph.getVertex(targetClass.getName()); + if (vertex != null) { + vertex.allParents().forEach(parent -> { + ClassPathNode parentPath = workspace.findClass(parent.getName()); + if (parentPath == null) + return; + TreeItem> parentItem = new TreeItem<>(parentPath); + rootItem.getChildren().add(parentItem); + for (MethodMember method : parent.getValue().getMethods()) { + if (method.hasPrivateModifier() || method.hasFinalModifier() || method.hasNativeModifier()) + continue; + parentItem.getChildren().add(new TreeItem<>(parentPath.child(method))); + } + parentItem.setExpanded(true); + }); + } + + Button acceptButton = new ActionButton(new FontIconView(CHECKMARK, Color.LAWNGREEN), () -> accept(memberConsumer)); + Button cancelButton = new ActionButton(new FontIconView(CLOSE, Color.RED), this::hide); + acceptButton.disableProperty().bind(tree.getSelectionModel().selectedItemProperty().map(i -> !(i.getValue() instanceof ClassMemberPathNode))); + HBox buttons = new HBox(acceptButton, new Spacer(), cancelButton); + VBox layout = new VBox(tree, buttons); + layout.setSpacing(10); + layout.setAlignment(Pos.TOP_CENTER); + layout.setPadding(new Insets(10)); + setMinWidth(500); + setMinHeight(230); + setMaxHeight(460); + titleProperty().bind(Lang.getBinding("dialog.title.override-method")); + setScene(new RecafScene(layout, 500, 350)); + } + + private void accept(@Nonnull BiConsumer memberConsumer) { + TreeItem> selectedItem = tree.getSelectionModel().getSelectedItem(); + if (selectedItem != null && selectedItem.getValue() instanceof ClassMemberPathNode memberPath) { + ClassInfo owner = memberPath.getValueOfType(ClassInfo.class); + MethodMember method = (MethodMember) memberPath.getValue(); + if (owner != null) + memberConsumer.accept(owner, method); + } + hide(); + } +} diff --git a/recaf-ui/src/main/resources/translations/en_US.lang b/recaf-ui/src/main/resources/translations/en_US.lang index e8ffe10ca..2c4711f05 100644 --- a/recaf-ui/src/main/resources/translations/en_US.lang +++ b/recaf-ui/src/main/resources/translations/en_US.lang @@ -36,6 +36,7 @@ menu.edit=Edit menu.edit.add.field=Add field menu.edit.add.method=Add method menu.edit.add.annotation=Add annotation +menu.edit.override.method=Override method menu.edit.remove.field=Remove fields menu.edit.remove.method=Remove methods menu.edit.remove.annotation=Remove annotations @@ -244,6 +245,7 @@ dialog.header.move-package=Move the package into to a new parent package. ## Add members dialog.title.add-field=Add field dialog.title.add-method=Add method +dialog.title.override-method=Override method dialog.input.name=Name dialog.input.desc=Descriptor dialog.warn.illegal-name=Illegal name