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 07b6ced89..ebc7823d0 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 @@ -177,6 +177,9 @@ private void populateJvmMenu(@Nonnull ContextMenu menu, refactor.infoItem("menu.refactor.rename", TAG_EDIT, actions::renameClass); refactor.infoItem("menu.refactor.move", STACKED_MOVE, actions::moveClass); + // Export actions + builder.infoItem("menu.export.class", EXPORT, actions::exportClass); + // TODO: implement operations // - View // - Class hierarchy 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 b8b6ed9a2..51bac7f1b 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 @@ -50,6 +50,7 @@ import software.coley.recaf.ui.pane.editing.text.TextFilePane; import software.coley.recaf.util.*; import software.coley.recaf.util.visitors.*; +import software.coley.recaf.workspace.PathExportingManager; import software.coley.recaf.workspace.model.Workspace; import software.coley.recaf.workspace.model.bundle.*; import software.coley.recaf.workspace.model.resource.WorkspaceResource; @@ -78,6 +79,7 @@ public class Actions implements Service { private final DockingManager dockingManager; private final TextProviderService textService; private final IconProviderService iconService; + private final PathExportingManager pathExportingManager; private final Instance applierProvider; private final Instance jvmPaneProvider; private final Instance androidPaneProvider; @@ -96,6 +98,7 @@ public Actions(@Nonnull ActionsConfig config, @Nonnull DockingManager dockingManager, @Nonnull TextProviderService textService, @Nonnull IconProviderService iconService, + @Nonnull PathExportingManager pathExportingManager, @Nonnull Instance applierProvider, @Nonnull Instance jvmPaneProvider, @Nonnull Instance androidPaneProvider, @@ -111,6 +114,7 @@ public Actions(@Nonnull ActionsConfig config, this.dockingManager = dockingManager; this.textService = textService; this.iconService = iconService; + this.pathExportingManager = pathExportingManager; this.applierProvider = applierProvider; this.jvmPaneProvider = jvmPaneProvider; this.androidPaneProvider = androidPaneProvider; @@ -1454,6 +1458,25 @@ else if (path instanceof ClassMemberPathNode classMemberPathNode) }); } + /** + * Exports a class, prompting the user to select a location to save the class to. + * + * @param workspace + * Containing workspace. + * @param resource + * Containing resource. + * @param bundle + * Containing bundle. + * @param info + * Class to delete. + */ + public void exportClass(@Nonnull Workspace workspace, + @Nonnull WorkspaceResource resource, + @Nonnull JvmClassBundle bundle, + @Nonnull JvmClassInfo info) { + pathExportingManager.export(info); + } + /** * Prompts the user (if configured, otherwise prompt is skipped) to delete the class. * diff --git a/recaf-ui/src/main/java/software/coley/recaf/ui/config/RecentFilesConfig.java b/recaf-ui/src/main/java/software/coley/recaf/ui/config/RecentFilesConfig.java index 6151cce60..872d3aff9 100644 --- a/recaf-ui/src/main/java/software/coley/recaf/ui/config/RecentFilesConfig.java +++ b/recaf-ui/src/main/java/software/coley/recaf/ui/config/RecentFilesConfig.java @@ -40,6 +40,7 @@ public class RecentFilesConfig extends BasicConfigContainer { private final ObservableCollection> recentWorkspaces = new ObservableCollection<>(ArrayList::new); private final ObservableString lastWorkspaceOpenDirectory = new ObservableString(System.getProperty("user.dir")); private final ObservableString lastWorkspaceExportDirectory = new ObservableString(System.getProperty("user.dir")); + private final ObservableString lastClassExportDirectory = new ObservableString(System.getProperty("user.dir")); @Inject public RecentFilesConfig() { @@ -49,6 +50,7 @@ public RecentFilesConfig() { addValue(new BasicCollectionConfigValue<>("recent-workspaces", List.class, WorkspaceModel.class, recentWorkspaces)); addValue(new BasicConfigValue<>("last-workspace-open-path", String.class, lastWorkspaceOpenDirectory)); addValue(new BasicConfigValue<>("last-workspace-export-path", String.class, lastWorkspaceExportDirectory)); + addValue(new BasicConfigValue<>("last-class-export-path", String.class, lastClassExportDirectory)); } /** @@ -126,6 +128,14 @@ public ObservableString getLastWorkspaceExportDirectory() { return lastWorkspaceExportDirectory; } + /** + * @return Last path used to export a class to. + */ + @Nonnull + public ObservableString getLastClassExportDirectory() { + return lastClassExportDirectory; + } + /** * Basic wrapper for workspaces. * diff --git a/recaf-ui/src/main/java/software/coley/recaf/workspace/PathExportingManager.java b/recaf-ui/src/main/java/software/coley/recaf/workspace/PathExportingManager.java index 2396e6e99..8f5c59aa8 100644 --- a/recaf-ui/src/main/java/software/coley/recaf/workspace/PathExportingManager.java +++ b/recaf-ui/src/main/java/software/coley/recaf/workspace/PathExportingManager.java @@ -7,6 +7,7 @@ import org.slf4j.Logger; import software.coley.observables.ObservableString; import software.coley.recaf.analytics.logging.Logging; +import software.coley.recaf.info.JvmClassInfo; import software.coley.recaf.services.workspace.WorkspaceManager; import software.coley.recaf.ui.config.ExportConfig; import software.coley.recaf.ui.config.RecentFilesConfig; @@ -21,6 +22,7 @@ import java.io.File; import java.io.IOException; +import java.nio.file.Files; import java.nio.file.Path; /** @@ -120,4 +122,39 @@ public void export(Workspace workspace) { ); } } + + public void export(JvmClassInfo info) { + ObservableString lastClassExportDir = recentFilesConfig.getLastClassExportDirectory(); + File lastExportDir = lastClassExportDir.unboxingMap(File::new); + + FileChooser chooser = new FileChooser(); + chooser.setInitialDirectory(lastExportDir); + chooser.setTitle(Lang.get("dialog.file.export")); + chooser.getExtensionFilters().add(new FileChooser.ExtensionFilter("Java Class", "*.class")); + + File selectedPath = chooser.showSaveDialog(null); + + if (selectedPath == null) { + return; + } + + Path exportPath = selectedPath.toPath(); + if (!exportPath.endsWith(".class")) { + exportPath = exportPath.resolveSibling(exportPath.getFileName() + ".class"); + } + + lastClassExportDir.setValue(selectedPath.getParent()); + + try { + Files.write(exportPath, info.getBytecode()); + } catch (IOException ex) { + logger.error("Failed to export class to path '{}'", selectedPath, ex); + ErrorDialogs.show( + Lang.getBinding("dialog.error.exportclass.title"), + Lang.getBinding("dialog.error.exportclass.header"), + Lang.getBinding("dialog.error.exportclass.content"), + ex + ); + } + } } diff --git a/recaf-ui/src/main/resources/translations/en_US.lang b/recaf-ui/src/main/resources/translations/en_US.lang index 5a59e5dd5..4d7c051b0 100644 --- a/recaf-ui/src/main/resources/translations/en_US.lang +++ b/recaf-ui/src/main/resources/translations/en_US.lang @@ -49,6 +49,7 @@ menu.edit.noop=Make no-op menu.edit.changeversion=Change class versions menu.edit.changeversion.up=Upgrade menu.edit.changeversion.down=Downgrade +menu.export.class=Export class menu.help=Help menu.help.discord=Discord menu.help.docs=Online user documentation @@ -654,6 +655,7 @@ service.io.recent-workspaces-config.last-workspace-export-path=Last workspace ex service.io.recent-workspaces-config.last-workspace-open-path=Last workspace open path service.io.recent-workspaces-config.max-recent-workspaces=Maximum record of recent paths service.io.recent-workspaces-config.recent-workspaces=Recent workspace +service.io.recent-workspaces-config.last-class-export-path=Last class export path service.io.resource-importer-config=Archive importing service.io.resource-importer-config.zip-strategy=ZIP parsing strategy service.io.resource-importer-config.skip-revisited-cen-to-local-links=Skip duplicate CEN-to-LOC entries with JVM strategy