diff --git a/recaf-core/src/main/java/software/coley/recaf/analytics/logging/Logging.java b/recaf-core/src/main/java/software/coley/recaf/analytics/logging/Logging.java index bea75d145..ed70b26c5 100644 --- a/recaf-core/src/main/java/software/coley/recaf/analytics/logging/Logging.java +++ b/recaf-core/src/main/java/software/coley/recaf/analytics/logging/Logging.java @@ -50,7 +50,7 @@ public static DebuggingLogger get(Class cls) { * @param consumer * New log message consumer. */ - public static void addLogConsumer(LogConsumer consumer) { + public static void addLogConsumer(@Nonnull LogConsumer consumer) { logConsumers.add(consumer); } @@ -58,7 +58,7 @@ public static void addLogConsumer(LogConsumer consumer) { * @param consumer * Log message consumer to remove. */ - public static void removeLogConsumer(LogConsumer consumer) { + public static void removeLogConsumer(@Nonnull LogConsumer consumer) { logConsumers.remove(consumer); } diff --git a/recaf-ui/src/main/java/software/coley/recaf/services/info/summary/builtin/AntiDecompilationSummarizer.java b/recaf-ui/src/main/java/software/coley/recaf/services/info/summary/builtin/AntiDecompilationSummarizer.java index fe21057ab..f0141def7 100644 --- a/recaf-ui/src/main/java/software/coley/recaf/services/info/summary/builtin/AntiDecompilationSummarizer.java +++ b/recaf-ui/src/main/java/software/coley/recaf/services/info/summary/builtin/AntiDecompilationSummarizer.java @@ -423,6 +423,12 @@ public boolean summarize(@Nonnull Workspace workspace, Stage window = windowFactory.createAnonymousStage(scene, getBinding("mapgen"), 800, 400); window.show(); window.requestFocus(); + + // Because our service is application scoped, the injected mapping generator panes won't + // be automatically destroyed until all of Recaf is closed. Thus, for optimal GC usage we + // need to manually invoke the destruction of our injected mapping generator panes. + // We can do this when the stage is closed. + window.setOnHidden(e -> generatorPaneProvider.destroy(mappingGeneratorPane)); }); }, service).exceptionally(t -> { logger.error("Failed to open mapping viewer", t); 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 61d32041f..9b3b294ed 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 @@ -1923,7 +1923,7 @@ private T openSearchPane(@Nonnull String titleId, region = dockingManager.newRegion(); DockingTab tab = region.createTab(getBinding(titleId), content); tab.setGraphic(new FontIconView(icon)); - RecafScene scene = new RecafScene((region)); + RecafScene scene = new RecafScene(region); Stage window = windowFactory.createAnonymousStage(scene, getBinding("menu.search"), 800, 400); window.show(); window.requestFocus(); diff --git a/recaf-ui/src/main/java/software/coley/recaf/services/window/WindowFactory.java b/recaf-ui/src/main/java/software/coley/recaf/services/window/WindowFactory.java index d3c94704a..102d08b8a 100644 --- a/recaf-ui/src/main/java/software/coley/recaf/services/window/WindowFactory.java +++ b/recaf-ui/src/main/java/software/coley/recaf/services/window/WindowFactory.java @@ -8,7 +8,6 @@ import javafx.stage.Stage; import software.coley.recaf.services.Service; import software.coley.recaf.ui.window.RecafStage; -import software.coley.recaf.util.Icons; /** * Creates new windows. diff --git a/recaf-ui/src/main/java/software/coley/recaf/services/window/WindowManager.java b/recaf-ui/src/main/java/software/coley/recaf/services/window/WindowManager.java index bbc458c34..82322ee2d 100644 --- a/recaf-ui/src/main/java/software/coley/recaf/services/window/WindowManager.java +++ b/recaf-ui/src/main/java/software/coley/recaf/services/window/WindowManager.java @@ -5,7 +5,6 @@ import jakarta.enterprise.context.ApplicationScoped; import jakarta.enterprise.inject.Instance; import jakarta.inject.Inject; -import javafx.event.EventHandler; import javafx.stage.Stage; import javafx.stage.WindowEvent; import org.slf4j.Logger; @@ -27,6 +26,7 @@ public class WindowManager implements Service { private static final Logger logger = Logging.get(WindowManager.class); public static final String SERVICE_ID = "window-manager"; + private static final String ANON_PREFIX = "anon-"; // Built-in window keys public static final String WIN_MAIN = "main"; public static final String WIN_REMOTE_VMS = "remote-vms"; @@ -41,7 +41,7 @@ public class WindowManager implements Service { private final Map windowMappings = new HashMap<>(); @Inject - public WindowManager(WindowManagerConfig config, Instance stages) { + public WindowManager(@Nonnull WindowManagerConfig config, @Nonnull Instance stages) { this.config = config; // Register identifiable stages. @@ -58,7 +58,7 @@ public WindowManager(WindowManagerConfig config, Instance sta * Stage to register. */ public void registerAnonymous(@Nonnull Stage stage) { - register(UUID.randomUUID().toString(), stage); + register(ANON_PREFIX + UUID.randomUUID(), stage); } /** @@ -84,24 +84,24 @@ public void register(@Nonnull String id, @Nonnull Stage stage) { if (windowMappings.containsKey(id)) throw new IllegalStateException("The stage ID was already registered: " + id); - EventHandler baseOnShown = stage.getOnShown(); - EventHandler baseOnHidden = stage.getOnHidden(); - - // Wrap original handlers to keep existing behavior. // Record when windows are 'active' based on visibility. - EventHandler onShown = e -> { + // We're using event filters so users can still do things like 'stage.setOnShown(...)' and not interfere with + // our window tracking logic in here + stage.addEventFilter(WindowEvent.WINDOW_SHOWN, e -> { logger.trace("Stage showing: {}", id); activeWindows.add(stage); - if (baseOnShown != null) baseOnShown.handle(e); - - }; - EventHandler onHidden = e -> { + }); + stage.addEventFilter(WindowEvent.WINDOW_HIDDEN, e -> { logger.trace("Stage hiding: {}", id); activeWindows.remove(stage); - if (baseOnHidden != null) baseOnHidden.handle(e); - }; - stage.setOnShown(onShown); - stage.setOnHidden(onHidden); + + // Anonymous stages can be pruned from the id->stage map. + // They are not meant to be persistent. But, we register them anyway for our duplicate register check above. + if (id.startsWith(ANON_PREFIX)) { + logger.trace("Stage pruned: {} ({})", id, stage.getTitle()); + windowMappings.remove(id); + } + }); // If state is already visible, add it right away. if (stage.isShowing()) activeWindows.add(stage); @@ -119,7 +119,7 @@ public void register(@Nonnull String id, @Nonnull Stage stage) { * @return Active windows. */ @Nonnull - public ObservableList getActiveWindows() { + public Collection getActiveWindows() { return activeWindows; } diff --git a/recaf-ui/src/main/java/software/coley/recaf/ui/control/richtext/Editor.java b/recaf-ui/src/main/java/software/coley/recaf/ui/control/richtext/Editor.java index 437ea33a8..9002c5803 100644 --- a/recaf-ui/src/main/java/software/coley/recaf/ui/control/richtext/Editor.java +++ b/recaf-ui/src/main/java/software/coley/recaf/ui/control/richtext/Editor.java @@ -197,6 +197,7 @@ public CompletableFuture restyleAtPosition(int position, int length) { * * @return The {@link StackPane} present in the {@link #getCenter() center} of the editor. */ + @Nonnull public StackPane getPrimaryStack() { return stackPane; } @@ -216,6 +217,7 @@ public void redrawParagraphGraphics() { /** * @return Current style spans for the entire document. */ + @Nonnull public StyleSpans> getStyleSpans() { return codeArea.getStyleSpans(0, getTextLength()); } diff --git a/recaf-ui/src/main/java/software/coley/recaf/ui/pane/MappingGeneratorPane.java b/recaf-ui/src/main/java/software/coley/recaf/ui/pane/MappingGeneratorPane.java index be1df89e1..8722bb2f0 100644 --- a/recaf-ui/src/main/java/software/coley/recaf/ui/pane/MappingGeneratorPane.java +++ b/recaf-ui/src/main/java/software/coley/recaf/ui/pane/MappingGeneratorPane.java @@ -6,6 +6,7 @@ import atlantafx.base.theme.Styles; import jakarta.annotation.Nonnull; import jakarta.annotation.Nullable; +import jakarta.annotation.PreDestroy; import jakarta.enterprise.context.Dependent; import jakarta.enterprise.inject.Instance; import jakarta.inject.Inject; @@ -85,18 +86,18 @@ public class MappingGeneratorPane extends StackPane { private final InheritanceGraph graph; private final ModalPane modal = new ModalPane(); private final MappingApplier mappingApplier; - private final Node previewGroup; + private final Pane previewGroup; @Inject public MappingGeneratorPane(@Nonnull Workspace workspace, - @Nonnull NameGeneratorProviders nameGeneratorProviders, - @Nonnull StringPredicateProvider stringPredicateProvider, - @Nonnull MappingGenerator mappingGenerator, - @Nonnull ConfigComponentManager componentManager, - @Nonnull InheritanceGraph graph, - @Nonnull AggregateMappingManager aggregateMappingManager, - @Nonnull MappingApplier mappingApplier, - @Nonnull Instance searchBarProvider) { + @Nonnull NameGeneratorProviders nameGeneratorProviders, + @Nonnull StringPredicateProvider stringPredicateProvider, + @Nonnull MappingGenerator mappingGenerator, + @Nonnull ConfigComponentManager componentManager, + @Nonnull InheritanceGraph graph, + @Nonnull AggregateMappingManager aggregateMappingManager, + @Nonnull MappingApplier mappingApplier, + @Nonnull Instance searchBarProvider) { this.workspace = workspace; this.nameGeneratorProviders = nameGeneratorProviders; @@ -126,6 +127,16 @@ public MappingGeneratorPane(@Nonnull Workspace workspace, getChildren().addAll(modal, horizontalWrapper); } + @PreDestroy + private void destroy() { + // We want to clear out a number of things here to assist in proper GC cleanup. + // - Mappings (which can hold a workspace/graph reference) + // - UI components (which can hold large virtualized text models for the mapping text representation) + mappingsToApply.setValue(null); + previewGroup.getChildren().clear(); + getChildren().clear(); + } + public void addConfiguredFilter(@Nonnull FilterWithConfigNode filterConfig) { filters.getItems().add(filterConfig); } @@ -212,6 +223,9 @@ private void apply() { if (mappings != null) { MappingResults results = mappingApplier.applyToPrimaryResource(mappings); results.apply(); + + // Clear property now that the mappings have been applied + mappingsToApply.set(null); } } @@ -293,18 +307,7 @@ public String fromString(String s) { @Nonnull private Node createFilterDisplay(@Nonnull AggregateMappingManager aggregateMappingManager) { // List to house current filters. - filters.setCellFactory(param -> new ListCell<>() { - @Override - protected void updateItem(FilterWithConfigNode item, boolean empty) { - super.updateItem(item, empty); - if (empty || item == null) { - textProperty().unbind(); - setText(null); - } else { - textProperty().bind(item.display().map(display -> (getIndex() + 1) + ". " + display)); - } - } - }); + filters.setCellFactory(param -> new ConfiguredFilterCell()); filters.getItems().add(new ExcludeExistingMapped(aggregateMappingManager.getAggregatedMappings())); ReadOnlyObjectProperty> selectedItem = filters.getSelectionModel().selectedItemProperty(); BooleanBinding hasItemSelection = selectedItem.isNull(); @@ -370,9 +373,9 @@ protected void updateItem(FilterWithConfigNode item, boolean empty) { @Nonnull private ActionMenuItem typeSetAction(@Nonnull ObjectProperty>> nodeSupplier, - @Nonnull StringProperty parentText, - @Nonnull String translationKey, - @Nonnull Supplier> supplier) { + @Nonnull StringProperty parentText, + @Nonnull String translationKey, + @Nonnull Supplier> supplier) { StringBinding nameBinding = Lang.getBinding(translationKey); return new ActionMenuItem(nameBinding, () -> { @@ -781,6 +784,23 @@ protected void fillConfigurator(@Nonnull BiConsumer sink) { } } + /** + * List cell to render {@link FilterWithConfigNode}. + */ + private static class ConfiguredFilterCell extends ListCell> { + @Override + protected void updateItem(FilterWithConfigNode item, boolean empty) { + super.updateItem(item, empty); + StringProperty property = textProperty(); + if (empty || item == null) { + property.unbind(); + setText(null); + } else { + property.bind(item.display().map(display -> (getIndex() + 1) + ". " + display)); + } + } + } + /** * Base to create a filter with configuration node. *