From 103043f8effb66916c574da58a82661b6bde204d Mon Sep 17 00:00:00 2001 From: Col-E Date: Wed, 27 Sep 2023 06:33:43 -0400 Subject: [PATCH 01/19] Make path loading manager error handling looser typed --- .../software/coley/recaf/util/ErrorDialogs.java | 16 ++++++++-------- .../recaf/workspace/PathLoadingManager.java | 6 +++--- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/recaf-ui/src/main/java/software/coley/recaf/util/ErrorDialogs.java b/recaf-ui/src/main/java/software/coley/recaf/util/ErrorDialogs.java index 01217490a..0cfa3a648 100644 --- a/recaf-ui/src/main/java/software/coley/recaf/util/ErrorDialogs.java +++ b/recaf-ui/src/main/java/software/coley/recaf/util/ErrorDialogs.java @@ -17,13 +17,13 @@ public class ErrorDialogs { * Header text. * @param content * Content text. - * @param ex + * @param t * The error to display. */ - public static void show(String title, String header, String content, Exception ex) { + public static void show(String title, String header, String content, Throwable t) { FxThreadUtil.run(() -> { Alert alert = alert(title, header, content); - configure(alert, ex); + configure(alert, t); }); } @@ -34,19 +34,19 @@ public static void show(String title, String header, String content, Exception e * Header text. * @param content * Content text. - * @param ex + * @param t * The error to display. */ - public static void show(StringBinding title, StringBinding header, StringBinding content, Exception ex) { + public static void show(StringBinding title, StringBinding header, StringBinding content, Throwable t) { FxThreadUtil.run(() -> { Alert alert = alert(title, header, content); - configure(alert, ex); + configure(alert, t); }); } - private static void configure(Alert alert, Exception ex) { + private static void configure(Alert alert, Throwable t) { // Create expandable Exception. - String exceptionText = StringUtil.traceToString(ex); + String exceptionText = StringUtil.traceToString(t); TextArea textArea = new TextArea(exceptionText); textArea.setEditable(false); textArea.setWrapText(true); diff --git a/recaf-ui/src/main/java/software/coley/recaf/workspace/PathLoadingManager.java b/recaf-ui/src/main/java/software/coley/recaf/workspace/PathLoadingManager.java index 334cac2d0..1ed2910a6 100644 --- a/recaf-ui/src/main/java/software/coley/recaf/workspace/PathLoadingManager.java +++ b/recaf-ui/src/main/java/software/coley/recaf/workspace/PathLoadingManager.java @@ -60,7 +60,7 @@ public void removePreLoadListener(WorkspacePreLoadListener listener) { * Error handling for invalid input. */ public void asyncNewWorkspace(@Nonnull Path primaryPath, @Nonnull List supportingPaths, - @Nonnull Consumer errorHandling) { + @Nonnull Consumer errorHandling) { // Invoke listeners, new content is being loaded. for (WorkspacePreLoadListener listener : preLoadListeners) listener.onPreLoad(primaryPath, supportingPaths); @@ -78,8 +78,8 @@ public void asyncNewWorkspace(@Nonnull Path primaryPath, @Nonnull List sup // Wrap into workspace and assign it Workspace workspace = new BasicWorkspace(primaryResource, supportingResources); workspaceManager.setCurrent(workspace); - } catch (IOException ex) { - errorHandling.accept(ex); + } catch (Throwable t) { + errorHandling.accept(t); } }); } From 8cb5f1bd99cf4dbdf3acc401078d60aeb142bad5 Mon Sep 17 00:00:00 2001 From: Col-E Date: Wed, 27 Sep 2023 07:28:10 -0400 Subject: [PATCH 02/19] Validate navigation manager resources are cleared after closing a workspace --- .../navigation/NavigationManager.java | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/recaf-ui/src/main/java/software/coley/recaf/services/navigation/NavigationManager.java b/recaf-ui/src/main/java/software/coley/recaf/services/navigation/NavigationManager.java index 39b899e0e..cf32e494f 100644 --- a/recaf-ui/src/main/java/software/coley/recaf/services/navigation/NavigationManager.java +++ b/recaf-ui/src/main/java/software/coley/recaf/services/navigation/NavigationManager.java @@ -8,6 +8,7 @@ import javafx.beans.value.ChangeListener; import javafx.beans.value.ObservableValue; import javafx.scene.Node; +import javafx.scene.control.Tab; import org.slf4j.Logger; import software.coley.recaf.analytics.logging.Logging; import software.coley.recaf.cdi.EagerInitialization; @@ -83,6 +84,9 @@ public NavigationManager(@Nonnull NavigationManagerConfig config, return; } + // The tab is closed, remove its spy lookup. + tabToSpy.remove(tab); + // Remove content from navigation tracking. spy.remove(tab.getContent()); @@ -98,6 +102,28 @@ public NavigationManager(@Nonnull NavigationManagerConfig config, if (dockingTab != null) dockingTab.close(); } + + // Validate tabs were closed, except un-closable ones. + // All the closable ones should have been done so above. + if (!tabToSpy.isEmpty()) { + if (tabToSpy.keySet().stream().anyMatch(Tab::isClosable)) + logger.warn("Closable tab reference was not cleared after workspace closure in navigation manager"); + tabToSpy.clear(); + } + + // Validate all child references have been removed. + if (!children.isEmpty()) { + logger.warn("Navigation manager children list was not empty after workspace closure"); + children.clear(); + } + if (!childrenToTab.isEmpty()) { + logger.warn("Navigation manager children-to-tab map was not empty after workspace closure"); + childrenToTab.clear(); + } + + // Remove the path reference to the old workspace. + forwarding.workspacePath = null; + path = null; }); // Track current workspace so that we are navigable ourselves. From 911b897a4bfd8ef8dda8c2a18f2241ce2f12db80 Mon Sep 17 00:00:00 2001 From: Col-E Date: Wed, 27 Sep 2023 08:22:30 -0400 Subject: [PATCH 03/19] instanceof cleanup --- ...toRegisterWorkspaceListenersInterceptor.java | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/recaf-core/src/main/java/software/coley/recaf/cdi/AutoRegisterWorkspaceListenersInterceptor.java b/recaf-core/src/main/java/software/coley/recaf/cdi/AutoRegisterWorkspaceListenersInterceptor.java index 80e686751..844ae468e 100644 --- a/recaf-core/src/main/java/software/coley/recaf/cdi/AutoRegisterWorkspaceListenersInterceptor.java +++ b/recaf-core/src/main/java/software/coley/recaf/cdi/AutoRegisterWorkspaceListenersInterceptor.java @@ -63,10 +63,10 @@ public Object intercept(InvocationContext context) throws Exception { if (isApplicationScoped) { // Application scoped beans may want to listen to workspaces opening and closing // for, well, the duration of the application. - if (value instanceof WorkspaceOpenListener) - workspaceManager.addWorkspaceOpenListener((WorkspaceOpenListener) value); - if (value instanceof WorkspaceCloseListener) - workspaceManager.addWorkspaceCloseListener((WorkspaceCloseListener) value); + if (value instanceof WorkspaceOpenListener openListener) + workspaceManager.addWorkspaceOpenListener(openListener); + if (value instanceof WorkspaceCloseListener closeListener) + workspaceManager.addWorkspaceCloseListener(closeListener); } else { // The bean is likely dependent scoped, or linked to the lifespan of the current workspace. // This it only really makes sense to have the close listener be supported. @@ -75,16 +75,15 @@ public Object intercept(InvocationContext context) throws Exception { if (value instanceof WorkspaceOpenListener) logger.warn("The class '{}' implements '{}' but is not @ApplicationScoped", valueType.getName(), WorkspaceOpenListener.class.getSimpleName()); - if (value instanceof WorkspaceCloseListener) - workspaceManager.addWorkspaceCloseListener( - new AutoUnregisteringCloseListener((WorkspaceCloseListener) value)); + if (value instanceof WorkspaceCloseListener closeListener) + workspaceManager.addWorkspaceCloseListener(new AutoUnregisteringCloseListener(closeListener)); } // If a current workspace exists (at the time of creation of the instance), add the modification listener too. // We don't need to worry about clearing these because workspaces remove their own listeners on closing. Workspace current = workspaceManager.getCurrent(); - if (current != null && value instanceof WorkspaceModificationListener) - current.addWorkspaceModificationListener((WorkspaceModificationListener) value); + if (current != null && value instanceof WorkspaceModificationListener modificationListener) + current.addWorkspaceModificationListener(modificationListener); return context.proceed(); } From 5ebe45bac539e15e72df12b6136c9db3261e8268 Mon Sep 17 00:00:00 2001 From: Col-E Date: Wed, 27 Sep 2023 09:02:22 -0400 Subject: [PATCH 04/19] Fix memory leak where workspace references got stuck in the UI in the background - Tweak docking closure logic a bit - Make WorkspaceRootPane effectively a singleton, and handle its docking regions more carefully - Clean up references in mapping progress panel --- .../coley/recaf/RecafApplication.java | 15 ++-- .../navigation/NavigationManager.java | 15 +--- .../recaf/ui/docking/DockingManager.java | 7 +- .../coley/recaf/ui/docking/DockingRegion.java | 56 ++++++++++--- .../coley/recaf/ui/docking/DockingTab.java | 36 +++++++- .../recaf/ui/pane/MappingProgressPane.java | 17 ++++ .../recaf/ui/pane/WorkspaceRootPane.java | 82 +++++++++++++++---- 7 files changed, 174 insertions(+), 54 deletions(-) diff --git a/recaf-ui/src/main/java/software/coley/recaf/RecafApplication.java b/recaf-ui/src/main/java/software/coley/recaf/RecafApplication.java index 0f83c7878..53560347c 100644 --- a/recaf-ui/src/main/java/software/coley/recaf/RecafApplication.java +++ b/recaf-ui/src/main/java/software/coley/recaf/RecafApplication.java @@ -2,7 +2,6 @@ import jakarta.annotation.Nonnull; import javafx.application.Application; -import javafx.geometry.Insets; import javafx.geometry.Orientation; import javafx.scene.Node; import javafx.scene.Scene; @@ -38,6 +37,8 @@ public class RecafApplication extends Application implements WorkspaceOpenListener, WorkspaceCloseListener { private final Recaf recaf = Bootstrap.get(); private final BorderPane root = new BorderPane(); + private WorkspaceRootPane workspaceRootPane; + private WelcomePane welcomePane; @Override public void start(Stage stage) { @@ -48,6 +49,8 @@ public void start(Stage stage) { MainMenu menu = recaf.get(MainMenu.class); WelcomePane pane = recaf.get(WelcomePane.class); Node logging = createLoggingWrapper(); + workspaceRootPane = recaf.get(WorkspaceRootPane.class); + welcomePane = recaf.get(WelcomePane.class); // Layout SplitPane splitPane = new SplitPane(root, logging); @@ -93,17 +96,11 @@ private Node createLoggingWrapper() { @Override public void onWorkspaceClosed(@Nonnull Workspace workspace) { - FxThreadUtil.run(() -> { - WelcomePane pane = recaf.get(WelcomePane.class); - root.setCenter(pane); - }); + FxThreadUtil.run(() -> root.setCenter(welcomePane)); } @Override public void onWorkspaceOpened(@Nonnull Workspace workspace) { - FxThreadUtil.run(() -> { - WorkspaceRootPane pane = recaf.get(WorkspaceRootPane.class); - root.setCenter(pane); - }); + FxThreadUtil.run(() -> root.setCenter(workspaceRootPane)); } } diff --git a/recaf-ui/src/main/java/software/coley/recaf/services/navigation/NavigationManager.java b/recaf-ui/src/main/java/software/coley/recaf/services/navigation/NavigationManager.java index cf32e494f..f7a7aef35 100644 --- a/recaf-ui/src/main/java/software/coley/recaf/services/navigation/NavigationManager.java +++ b/recaf-ui/src/main/java/software/coley/recaf/services/navigation/NavigationManager.java @@ -22,6 +22,7 @@ import software.coley.recaf.services.mapping.MappingResults; import software.coley.recaf.ui.docking.DockingManager; import software.coley.recaf.ui.docking.DockingTab; +import software.coley.recaf.util.FxThreadUtil; import software.coley.recaf.workspace.WorkspaceManager; import software.coley.recaf.workspace.WorkspaceModificationListener; import software.coley.recaf.workspace.model.Workspace; @@ -78,15 +79,13 @@ public NavigationManager(@Nonnull NavigationManagerConfig config, spy.changed(contentProperty, null, contentProperty.getValue()); }); dockingManager.addTabClosureListener(((parent, tab) -> { - NavigableSpy spy = tabToSpy.get(tab); + // The tab is closed, remove its spy lookup. + NavigableSpy spy = tabToSpy.remove(tab); if (spy == null) { logger.warn("Tab {} was closed, but had no associated content spy instance", tab.getText()); return; } - // The tab is closed, remove its spy lookup. - tabToSpy.remove(tab); - // Remove content from navigation tracking. spy.remove(tab.getContent()); @@ -103,14 +102,6 @@ public NavigationManager(@Nonnull NavigationManagerConfig config, dockingTab.close(); } - // Validate tabs were closed, except un-closable ones. - // All the closable ones should have been done so above. - if (!tabToSpy.isEmpty()) { - if (tabToSpy.keySet().stream().anyMatch(Tab::isClosable)) - logger.warn("Closable tab reference was not cleared after workspace closure in navigation manager"); - tabToSpy.clear(); - } - // Validate all child references have been removed. if (!children.isEmpty()) { logger.warn("Navigation manager children list was not empty after workspace closure"); diff --git a/recaf-ui/src/main/java/software/coley/recaf/ui/docking/DockingManager.java b/recaf-ui/src/main/java/software/coley/recaf/ui/docking/DockingManager.java index 8fe60c664..784230e20 100644 --- a/recaf-ui/src/main/java/software/coley/recaf/ui/docking/DockingManager.java +++ b/recaf-ui/src/main/java/software/coley/recaf/ui/docking/DockingManager.java @@ -93,13 +93,16 @@ boolean onRegionClose(@Nonnull DockingRegion region) { if (!allowClosure) return false; - // Update internal state + // Update internal state. regions.remove(region); - // Needed in case a window containing the region gets closed + // Needed in case a window containing the region gets closed. for (DockingTab tab : new ArrayList<>(region.getDockTabs())) tab.close(); + // Tell the region it is closed, removing its reference to this docking manager. + region.onClose(); + // Closure allowed. return true; } diff --git a/recaf-ui/src/main/java/software/coley/recaf/ui/docking/DockingRegion.java b/recaf-ui/src/main/java/software/coley/recaf/ui/docking/DockingRegion.java index d91302e0e..34b50504d 100644 --- a/recaf-ui/src/main/java/software/coley/recaf/ui/docking/DockingRegion.java +++ b/recaf-ui/src/main/java/software/coley/recaf/ui/docking/DockingRegion.java @@ -24,7 +24,7 @@ */ public class DockingRegion extends DetachableTabPane { private static final Logger logger = Logging.get(DockingRegion.class); - private final DockingManager manager; + private DockingManager manager; DockingRegion(@Nonnull DockingManager manager) { this.manager = manager; @@ -42,7 +42,8 @@ public class DockingRegion extends DetachableTabPane { * * @return Created tab. */ - public DockingTab createTab(String title, Node content) { + @Nonnull + public DockingTab createTab(@Nonnull String title, @Nonnull Node content) { return createTab(() -> new DockingTab(title, content)); } @@ -56,7 +57,8 @@ public DockingTab createTab(String title, Node content) { * * @return Created tab. */ - public DockingTab createTab(ObservableValue title, Node content) { + @Nonnull + public DockingTab createTab(@Nonnull ObservableValue title, @Nonnull Node content) { return createTab(() -> new DockingTab(title, content)); } @@ -68,7 +70,8 @@ public DockingTab createTab(ObservableValue title, Node content) { * * @return Created tab. */ - public DockingTab createTab(Supplier factory) { + @Nonnull + public DockingTab createTab(@Nonnull Supplier factory) { DockingTab tab = factory.get(); // Ensure we record tabs closing (and let them still declare close handlers via the factory) @@ -77,9 +80,12 @@ public DockingTab createTab(Supplier factory) { // We use on-close-request so that 'getRegion' still has a reference to the containing tab-pane. // Using on-close, the region reference is removed. if (tab.isClosable()) { - if (closeHandler != null) - closeHandler.handle(e); - manager.onTabClose(tab.getRegion(), tab); + DockingRegion region = tab.getRegion(); + if (region.getDockTabs().contains(tab)) { + if (closeHandler != null) + closeHandler.handle(e); + getAndCheckManager().onTabClose(region, tab); + } } }); @@ -92,9 +98,9 @@ public DockingTab createTab(Supplier factory) { if (tab.isSelected()) { // Use the tab's parent, but if none is set then we are likely spawning the tab in 'this' region. if (tab.getTabPane() instanceof DockingRegion parentRegion) { - manager.onTabSelection(parentRegion, tab); + getAndCheckManager().onTabSelection(parentRegion, tab); } else { - manager.onTabSelection(thisRegion, tab); + getAndCheckManager().onTabSelection(thisRegion, tab); } } }); @@ -105,7 +111,7 @@ public DockingTab createTab(Supplier factory) { // It will be null while the tab is being dragged. if (oldValue instanceof DockingRegion oldParent && newValue instanceof DockingRegion newParent) { - manager.onTabMove(oldParent, newParent, tab); + getAndCheckManager().onTabMove(oldParent, newParent, tab); } }); @@ -117,13 +123,14 @@ public DockingTab createTab(Supplier factory) { logger.error("Tab was already added to region, prior to 'createTab' call.\n" + "Please ensure tabs are only added via 'createTab' to ensure consistent behavior.", new IllegalStateException("Stack dump")); - manager.onTabCreate(this, tab); + getAndCheckManager().onTabCreate(this, tab); return tab; } /** * @return List of all docking tabs in the region. */ + @Nonnull @SuppressWarnings("unchecked") public List getDockTabs() { // We do not want to use a 'stream.map()' operation just to satisfy generic contracts. @@ -131,6 +138,33 @@ public List getDockTabs() { return (List) (Object) super.getTabs(); } + /** + * @return {@code true} when this region has been marked as closed. + */ + public boolean isClosed() { + return manager == null; + } + + /** + * Called when this region is closed. + */ + void onClose() { + // We no longer need a reference to the manager that spawned this region. + manager = null; + } + + /** + * Exists so we don't directly reference the manager, which will NPE after the region is closed. + * + * @return Manager instance. + */ + @Nonnull + private DockingManager getAndCheckManager() { + if (manager == null) + throw new IllegalStateException("Region has been closed, the manager reference has been cleared"); + return manager; + } + /** * This is a slightly modified version of the default {@link TabDropHint} which fixes some size constants. */ diff --git a/recaf-ui/src/main/java/software/coley/recaf/ui/docking/DockingTab.java b/recaf-ui/src/main/java/software/coley/recaf/ui/docking/DockingTab.java index 49ef04a9b..38e2de6e5 100644 --- a/recaf-ui/src/main/java/software/coley/recaf/ui/docking/DockingTab.java +++ b/recaf-ui/src/main/java/software/coley/recaf/ui/docking/DockingTab.java @@ -1,12 +1,20 @@ package software.coley.recaf.ui.docking; import com.panemu.tiwulfx.control.dock.DetachableTab; +import com.panemu.tiwulfx.control.dock.DetachableTabPane; +import com.sun.javafx.collections.ObservableListWrapper; import javafx.beans.value.ObservableValue; +import javafx.collections.ObservableList; import javafx.event.Event; import javafx.scene.Node; import javafx.scene.control.Tab; import javafx.scene.control.TabPane; import software.coley.recaf.util.FxThreadUtil; +import software.coley.recaf.util.ReflectUtil; + +import java.lang.reflect.Field; +import java.util.List; +import java.util.function.Consumer; /** * {@link Tab} extension to track additional information required for {@link DockingManager} operations. @@ -14,6 +22,8 @@ * @author Matt Coley */ public class DockingTab extends DetachableTab { + private volatile boolean firedClose; + /** * @param title * Initial tab title. @@ -49,14 +59,32 @@ public DockingRegion getRegion() { */ public void close() { if (isClosable()) { - // It is important that this event is the same as the one that the containing DockingRegion registers - // a listener for. We use close requests instead of direct closes. - Event.fireEvent(this, new Event(Tab.TAB_CLOSE_REQUEST_EVENT)); + // Fire the close event if we've not done so yet. + // The event should only be fired once, even if 'close()' is called multiple times. + // We double-lock this to ensure the event really is only ever called once if 'close()' is ran across threads. + if (!firedClose) { + synchronized (this) { + if (!firedClose) { + // It is important that this event is the same as the one that the containing DockingRegion + // registers a listener for. We use close requests instead of direct closes. + Event.fireEvent(this, new Event(Tab.TAB_CLOSE_REQUEST_EVENT)); + firedClose = true; + } + } + } // Remove from containing tab-pane. TabPane tabPane = getTabPane(); if (tabPane != null) - FxThreadUtil.run(() -> tabPane.getTabs().remove(this)); + FxThreadUtil.run(() -> { + // In cases where this is handled after the scene has been closed + // we can skip this process as the docking library we use does not + // consider such cases and throws an exception. + ObservableList tabList = tabPane.getTabs(); + if (tabPane.getScene() != null) { + tabList.remove(this); + } + }); } } diff --git a/recaf-ui/src/main/java/software/coley/recaf/ui/pane/MappingProgressPane.java b/recaf-ui/src/main/java/software/coley/recaf/ui/pane/MappingProgressPane.java index 8bb086472..21615fa8d 100644 --- a/recaf-ui/src/main/java/software/coley/recaf/ui/pane/MappingProgressPane.java +++ b/recaf-ui/src/main/java/software/coley/recaf/ui/pane/MappingProgressPane.java @@ -18,6 +18,7 @@ import javafx.scene.effect.Effect; import javafx.scene.effect.Glow; import javafx.scene.layout.BorderPane; +import javafx.scene.layout.Region; import javafx.scene.layout.VBox; import javafx.scene.paint.Color; import javafx.scene.shape.Rectangle; @@ -123,6 +124,18 @@ public MappingProgressPane(@Nonnull CellConfigurationService configurationServic workspace.getPrimaryResource().addListener(this); }); + workspaceManager.addWorkspaceCloseListener(workspace -> { + // The pending update can be cleared once the workspace is closed. + pendingUpdate = null; + + // And the tree content can be cleared. + treeContentListDelegate.clear(); + + // Clear selection, as it holds a path which contains workspace references, + // which can prevent GC from freeing it. + selectedPath.set(null); + }); + // The tree update passes data to this delegate list. // We use ReactFX to merge rapid changes to limit redundant work. EventStreams.changesOf(treeContentListDelegate) @@ -150,6 +163,10 @@ public MappingProgressPane(@Nonnull CellConfigurationService configurationServic comboMetric.getSelectionModel().select(0); BorderPane wrapper = new BorderPane(); wrapper.centerProperty().bind(selectedPath.map(selection -> { + // No selection? Empty region. + if (selection == null) + return new Region(); + ClassPathNode path = selection.path(); Label className = new Label(configurationService.textOf(path)); diff --git a/recaf-ui/src/main/java/software/coley/recaf/ui/pane/WorkspaceRootPane.java b/recaf-ui/src/main/java/software/coley/recaf/ui/pane/WorkspaceRootPane.java index 0594827df..4c5a5881a 100644 --- a/recaf-ui/src/main/java/software/coley/recaf/ui/pane/WorkspaceRootPane.java +++ b/recaf-ui/src/main/java/software/coley/recaf/ui/pane/WorkspaceRootPane.java @@ -3,17 +3,22 @@ import com.panemu.tiwulfx.control.dock.DetachableTab; import jakarta.annotation.Nonnull; import jakarta.enterprise.context.Dependent; +import jakarta.enterprise.inject.Instance; import jakarta.inject.Inject; import javafx.scene.control.SplitPane; import javafx.scene.layout.BorderPane; import org.kordamp.ikonli.carbonicons.CarbonIcons; +import software.coley.recaf.RecafApplication; import software.coley.recaf.ui.control.FontIconView; -import software.coley.recaf.ui.docking.DockingTab; import software.coley.recaf.ui.docking.DockingManager; import software.coley.recaf.ui.docking.DockingRegion; +import software.coley.recaf.ui.docking.DockingTab; import software.coley.recaf.util.Lang; +import software.coley.recaf.workspace.WorkspaceManager; import software.coley.recaf.workspace.model.Workspace; +import java.util.concurrent.atomic.AtomicReference; + /** * Root panel for displaying a {@link Workspace}. *
@@ -21,6 +26,7 @@ *
    *
  • Searchable tree layout - {@link WorkspaceExplorerPane}
  • *
+ * There should only ever be a single instance of this, managed by {@link RecafApplication}. * * @author Matt Coley */ @@ -28,28 +34,72 @@ public class WorkspaceRootPane extends BorderPane { @Inject public WorkspaceRootPane(@Nonnull DockingManager dockingManager, - @Nonnull WorkspaceExplorerPane explorerPane, - @Nonnull WorkspaceInformationPane informationPane) { + @Nonnull WorkspaceManager workspaceManager, + @Nonnull Instance explorerPaneProvider, + @Nonnull Instance informationPaneProvider) { getStyleClass().add("bg-inset"); - // Add workspace explorer tree. - DockingRegion dockTree = dockingManager.newRegion(); - DockingTab treeTab = dockTree.createTab(Lang.getBinding("workspace.title"), explorerPane); - treeTab.setGraphic(new FontIconView(CarbonIcons.TREE_VIEW)); - treeTab.setClosable(false); - treeTab.setDetachable(false); + AtomicReference lastTreeRegion = new AtomicReference<>(); + AtomicReference lastPrimaryRegion = new AtomicReference<>(); + + // Register an open listener to fill the pane's regions when a workspace is loaded. + workspaceManager.addWorkspaceOpenListener(workspace -> { + DockingRegion dockTree = dockingManager.newRegion(); + DockingRegion dockPrimary = dockingManager.getPrimaryRegion(); + createWorkspaceExplorerTab(dockTree, explorerPaneProvider); + createPrimaryTab(dockPrimary, informationPaneProvider); + + // Layout + SplitPane split = new SplitPane(dockTree, dockPrimary); + SplitPane.setResizableWithParent(dockTree, false); + split.setDividerPositions(0.333); + setCenter(split); + // Record last regions + lastTreeRegion.set(dockTree); + lastPrimaryRegion.set(dockPrimary); + }); + + // Register a close listener to remove the old workspace's content from the pane's regions. + workspaceManager.addWorkspaceCloseListener(workspace -> { + DockingRegion dockTree = lastTreeRegion.get(); + if (dockTree != null) { + // Close all tabs. + for (DockingTab tab : dockTree.getDockTabs()) { + // Mark as closable so they can be closed. + tab.setClosable(true); + + // When the last tab in the region is closed, + // the close handler should kick in and clean things up for us. + // We will validate this below. + tab.close(); + } + } + DockingRegion dockPrimary = lastPrimaryRegion.get(); + if (dockPrimary != null) { + // Only close closable tabs. + for (DockingTab tab : dockPrimary.getDockTabs()) { + tab.close(); + } + } + }); + } + + private void createPrimaryTab(@Nonnull DockingRegion region, + @Nonnull Instance informationPaneProvider) { // Add summary of workspace, targeting the primary region. // In the UI, this region will persist and be the default location for // opening most 'new' content/tabs. - DockingRegion dockInfo = dockingManager.getPrimaryRegion(); - DetachableTab infoTab = dockInfo.createTab(Lang.getBinding("workspace.info"), informationPane); + DetachableTab infoTab = region.createTab(Lang.getBinding("workspace.info"), informationPaneProvider.get()); infoTab.setGraphic(new FontIconView(CarbonIcons.INFORMATION)); + } - // Layout - SplitPane split = new SplitPane(dockTree, dockInfo); - SplitPane.setResizableWithParent(dockTree, false); - split.setDividerPositions(0.333); - setCenter(split); + private void createWorkspaceExplorerTab(@Nonnull DockingRegion region, + @Nonnull Instance explorerPaneProvider) { + // Add workspace explorer tree. + DockingTab workspaceTab = region.createTab(Lang.getBinding("workspace.title"), explorerPaneProvider.get()); + workspaceTab.setGraphic(new FontIconView(CarbonIcons.TREE_VIEW)); + workspaceTab.setClosable(false); + workspaceTab.setDetachable(false); } } From 837ded772568ee00544a3575eb5272d3aba6db00 Mon Sep 17 00:00:00 2001 From: Col-E Date: Wed, 27 Sep 2023 10:14:23 -0400 Subject: [PATCH 05/19] Use SKIP_CODE when populating runtime resource class models Will skip the allocation of MANY BasicLocalVariable instances --- .../info/builder/JvmClassInfoBuilder.java | 20 +++++++++++++++++-- .../resource/RuntimeWorkspaceResource.java | 4 +++- 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/recaf-core/src/main/java/software/coley/recaf/info/builder/JvmClassInfoBuilder.java b/recaf-core/src/main/java/software/coley/recaf/info/builder/JvmClassInfoBuilder.java index 47087fe1f..0d5e97f2e 100644 --- a/recaf-core/src/main/java/software/coley/recaf/info/builder/JvmClassInfoBuilder.java +++ b/recaf-core/src/main/java/software/coley/recaf/info/builder/JvmClassInfoBuilder.java @@ -64,6 +64,7 @@ public JvmClassInfoBuilder(@Nonnull byte[] bytecode) { /** * Copies over values by reading the contents of the class file in the reader. + * Calls {@link #adaptFrom(ClassReader, int)} with {@code flags=0}. * * @param reader * ASM class reader to pull data from. @@ -71,9 +72,24 @@ public JvmClassInfoBuilder(@Nonnull byte[] bytecode) { * @return Builder. */ @Nonnull - @SuppressWarnings("deprecation") public JvmClassInfoBuilder adaptFrom(@Nonnull ClassReader reader) { - reader.accept(new ClassBuilderAppender(), 0); + return adaptFrom(reader, 0); + } + + /** + * Copies over values by reading the contents of the class file in the reader. + * + * @param reader + * ASM class reader to pull data from. + * @param flags + * Reader flags to use when populating information. + * + * @return Builder. + */ + @Nonnull + @SuppressWarnings(value = "deprecation") + public JvmClassInfoBuilder adaptFrom(@Nonnull ClassReader reader, int flags) { + reader.accept(new ClassBuilderAppender(), flags); return withBytecode(reader.b); } diff --git a/recaf-core/src/main/java/software/coley/recaf/workspace/model/resource/RuntimeWorkspaceResource.java b/recaf-core/src/main/java/software/coley/recaf/workspace/model/resource/RuntimeWorkspaceResource.java index eb15edfd9..38e6c2c3e 100644 --- a/recaf-core/src/main/java/software/coley/recaf/workspace/model/resource/RuntimeWorkspaceResource.java +++ b/recaf-core/src/main/java/software/coley/recaf/workspace/model/resource/RuntimeWorkspaceResource.java @@ -69,7 +69,9 @@ public JvmClassInfo get(@Nonnull Object name) { cache.put(key, STUB); return null; } - JvmClassInfo info = new JvmClassInfoBuilder(new ClassReader(value)).build(); + JvmClassInfo info = new JvmClassInfoBuilder() + .adaptFrom(new ClassReader(value), ClassReader.SKIP_CODE) + .build(); cache.put(key, info); return info; } From 5945a28410ada589f09700843a6b4e3d60b1ae09 Mon Sep 17 00:00:00 2001 From: Col-E Date: Fri, 29 Sep 2023 19:09:22 -0400 Subject: [PATCH 06/19] Fix tree nodes not removing if similarly prefixed packages exist before it in the tree model --- .../coley/recaf/path/DirectoryPathNode.java | 9 ++++++- .../control/tree/WorkspaceTreeNodeTest.java | 27 +++++++++++++++++-- 2 files changed, 33 insertions(+), 3 deletions(-) diff --git a/recaf-core/src/main/java/software/coley/recaf/path/DirectoryPathNode.java b/recaf-core/src/main/java/software/coley/recaf/path/DirectoryPathNode.java index cbf2aa86b..8faf603dc 100644 --- a/recaf-core/src/main/java/software/coley/recaf/path/DirectoryPathNode.java +++ b/recaf-core/src/main/java/software/coley/recaf/path/DirectoryPathNode.java @@ -94,7 +94,14 @@ public boolean hasEqualOrChildValue(@Nonnull PathNode other) { if (other instanceof DirectoryPathNode otherDirectory) { String dir = getValue(); String maybeParentDir = otherDirectory.getValue(); - return dir.startsWith(maybeParentDir); + + // We cannot do just a basic 'startsWith' check on the path values since they do not + // end with a trailing slash. This could lead to cases where: + // 'co' is a parent value of 'com/foo' + // + // By doing an equals check, we allow for 'co' vs 'com' to fail but 'co' vs 'co' to pass, + // and the following startsWith check with a slash allows us to not fall to the suffix issue described above. + return dir.equals(maybeParentDir) || dir.startsWith(maybeParentDir + "/"); } return super.hasEqualOrChildValue(other); diff --git a/recaf-ui/src/test/java/software/coley/recaf/ui/control/tree/WorkspaceTreeNodeTest.java b/recaf-ui/src/test/java/software/coley/recaf/ui/control/tree/WorkspaceTreeNodeTest.java index 185b2e23d..ca2bc85c7 100644 --- a/recaf-ui/src/test/java/software/coley/recaf/ui/control/tree/WorkspaceTreeNodeTest.java +++ b/recaf-ui/src/test/java/software/coley/recaf/ui/control/tree/WorkspaceTreeNodeTest.java @@ -22,8 +22,7 @@ import java.util.Objects; import static org.junit.jupiter.api.Assertions.*; -import static software.coley.recaf.test.TestClassUtils.fromClasses; -import static software.coley.recaf.test.TestClassUtils.fromRuntimeClass; +import static software.coley.recaf.test.TestClassUtils.*; import static software.coley.recaf.ui.control.tree.WorkspaceTreeNode.getOrInsertIntoTree; /** @@ -95,6 +94,30 @@ static void setup() throws IOException { z1 = z2.child(new BasicFileInfo("///zero.txt", new byte[0], new BasicPropertyContainer())); } + @Test + void testPackageDoesNotPreventRemovalOfPackageWithSamePrefix() { + DirectoryPathNode firstDir = new DirectoryPathNode("co/fizz"); + DirectoryPathNode secondDir = new DirectoryPathNode("com/foo"); + ClassPathNode firstClass = firstDir.child(createEmptyClass(firstDir.getValue() + "/Buzz")); + ClassPathNode secondClass = secondDir.child(createEmptyClass(secondDir.getValue() + "/Bar")); + + // Create the tree: + // root: + // co/ + // fizz/ + // Buzz + // com/ + // foo/ + // Bar + WorkspaceTreeNode root = new WorkspaceTreeNode(p5); + root.getOrCreateNodeByPath(firstClass); + root.getOrCreateNodeByPath(secondClass); + + // Removal of 'com/foo/Bar' should not be blocked by existence of 'co/fizz/Buzz' + // This ensures our package parent checks do not regress. + assertTrue(root.removeNodeByPath(secondClass)); + } + @Test void testGetAndRemoveWithDifferentNodesOfEqualValue() { ClassPathNode[] array = new ClassPathNode[]{p1a, p1b, p1c, p1d}; From 6cb39935809985cfadcede13097e8a96a72717bb Mon Sep 17 00:00:00 2001 From: Col-E Date: Sat, 30 Sep 2023 09:23:20 -0400 Subject: [PATCH 07/19] Bump JavaFX version to 21 --- recaf-ui/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/recaf-ui/build.gradle b/recaf-ui/build.gradle index a83d73d20..25ad0da37 100644 --- a/recaf-ui/build.gradle +++ b/recaf-ui/build.gradle @@ -4,7 +4,7 @@ plugins { id 'com.github.johnrengelman.shadow' version '7.1.2' } -def javaFxVersion = '19' +def javaFxVersion = '21' def javaFxIncludeInDist = System.getProperty('skip.jfx.bundle') == null application { From 57109f820efdbe5794a32a4e00902922d3cbf6ba Mon Sep 17 00:00:00 2001 From: Col-E Date: Sat, 7 Oct 2023 19:10:23 -0400 Subject: [PATCH 08/19] Update JFX gradle plugin, skip tests when creating dist-jar --- .github/workflows/build.yml | 2 +- recaf-ui/build.gradle | 20 ++++++++++++++------ 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 70c67572c..99c9e7109 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -57,7 +57,7 @@ jobs: **/hs_err_pid* # Build the distribution jar and upload it, without bundling JavaFX in the jar - name: Create distribution jar - run: ./gradlew build -x test -Dskip.jfx.bundle=true + run: ./gradlew assemble -x compileTestJava -Dskip.jfx.bundle=true - name: Upload distribution jar if: always() uses: actions/upload-artifact@v3 diff --git a/recaf-ui/build.gradle b/recaf-ui/build.gradle index 25ad0da37..d4a6a78b7 100644 --- a/recaf-ui/build.gradle +++ b/recaf-ui/build.gradle @@ -1,20 +1,16 @@ plugins { id 'application' - id 'org.openjfx.javafxplugin' version '0.0.13' + id 'org.openjfx.javafxplugin' version '0.1.0' id 'com.github.johnrengelman.shadow' version '7.1.2' } def javaFxVersion = '21' def javaFxIncludeInDist = System.getProperty('skip.jfx.bundle') == null -application { - mainClass = 'software.coley.recaf.Main' -} - javafx { version = javaFxVersion modules = ['javafx.controls', 'javafx.media'] - configuration = javaFxIncludeInDist ? 'implementation' : 'compileOnly' + configurations = javaFxIncludeInDist ? [ 'implementation' ] : [ 'compileOnly', 'testImplementation' ] } dependencies { @@ -27,4 +23,16 @@ dependencies { implementation ikonli_pack implementation richtextfx implementation treemapfx +} + +application { + mainClass = 'software.coley.recaf.Main' +} + +shadowJar{ + exclude "META-INF/maven/**" + exclude "META-INF/rewrite/**" + exclude "META-INF/proguard/*" + exclude "META-INF/native-image/*" + exclude "META-INF/*.properties" } \ No newline at end of file From 130c014b6991656e4a532b6a703a8c35944586ca Mon Sep 17 00:00:00 2001 From: Col-E Date: Sat, 7 Oct 2023 19:10:37 -0400 Subject: [PATCH 09/19] Fix check processing plugin being applied to root project --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index bc3143223..9df8452a7 100644 --- a/build.gradle +++ b/build.gradle @@ -1,7 +1,7 @@ plugins { id "com.github.ben-manes.versions" version "0.42.0" apply false id 'gov.tak.gradle.plugins.coverage-report-aggregator' version '1.3.0' - id 'gov.tak.gradle.plugins.checker-processor' version "1.2.7" + id 'gov.tak.gradle.plugins.checker-processor' version "1.2.7" apply false } allprojects { From cb5e8039f1f23ace9e8622f4cd87a45c745289a1 Mon Sep 17 00:00:00 2001 From: Col-E Date: Sat, 7 Oct 2023 19:11:09 -0400 Subject: [PATCH 10/19] Workaround unchecked being ignored in selection popup classes --- .../coley/recaf/ui/control/popup/ItemListSelectionPopup.java | 2 +- .../coley/recaf/ui/control/popup/ItemTreeSelectionPopup.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/recaf-ui/src/main/java/software/coley/recaf/ui/control/popup/ItemListSelectionPopup.java b/recaf-ui/src/main/java/software/coley/recaf/ui/control/popup/ItemListSelectionPopup.java index 8255c433b..beec8ffdd 100644 --- a/recaf-ui/src/main/java/software/coley/recaf/ui/control/popup/ItemListSelectionPopup.java +++ b/recaf-ui/src/main/java/software/coley/recaf/ui/control/popup/ItemListSelectionPopup.java @@ -23,6 +23,7 @@ * * @author Matt Coley */ +@SuppressWarnings("unchecked") public class ItemListSelectionPopup extends SelectionPopup { private final ListView list = new ListView<>(); @@ -84,7 +85,6 @@ protected ObservableValue isNullSelection() { @Nonnull @Override - @SuppressWarnings("unchecked") public ItemListSelectionPopup withMultipleSelection() { list.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE); return this; diff --git a/recaf-ui/src/main/java/software/coley/recaf/ui/control/popup/ItemTreeSelectionPopup.java b/recaf-ui/src/main/java/software/coley/recaf/ui/control/popup/ItemTreeSelectionPopup.java index 65ea6b436..05ee75a04 100644 --- a/recaf-ui/src/main/java/software/coley/recaf/ui/control/popup/ItemTreeSelectionPopup.java +++ b/recaf-ui/src/main/java/software/coley/recaf/ui/control/popup/ItemTreeSelectionPopup.java @@ -21,6 +21,7 @@ * * @author Matt Coley */ +@SuppressWarnings("unchecked") public class ItemTreeSelectionPopup extends SelectionPopup { private final TreeView tree = new TreeView<>(); @@ -77,7 +78,6 @@ protected ObservableValue isNullSelection() { @Nonnull @Override - @SuppressWarnings("unchecked") public ItemTreeSelectionPopup withMultipleSelection() { tree.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE); return this; From 72a7f23c7ee1577f8789eeb3b337c17d99a39301 Mon Sep 17 00:00:00 2001 From: Col-E Date: Sat, 7 Oct 2023 19:29:08 -0400 Subject: [PATCH 11/19] Update from gradle 7.2 to 8.4 --- gradle/wrapper/gradle-wrapper.properties | 2 +- recaf-ui/build.gradle | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index ffed3a254..e411586a5 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/recaf-ui/build.gradle b/recaf-ui/build.gradle index d4a6a78b7..426e631ed 100644 --- a/recaf-ui/build.gradle +++ b/recaf-ui/build.gradle @@ -1,7 +1,7 @@ plugins { id 'application' id 'org.openjfx.javafxplugin' version '0.1.0' - id 'com.github.johnrengelman.shadow' version '7.1.2' + id 'com.github.johnrengelman.shadow' version '8.1.1' } def javaFxVersion = '21' @@ -29,7 +29,7 @@ application { mainClass = 'software.coley.recaf.Main' } -shadowJar{ +shadowJar { exclude "META-INF/maven/**" exclude "META-INF/rewrite/**" exclude "META-INF/proguard/*" From 3eca5999e55134539a262d3362a93f7174ca1e11 Mon Sep 17 00:00:00 2001 From: Col-E Date: Mon, 9 Oct 2023 14:08:19 -0400 Subject: [PATCH 12/19] Enable local build caching --- gradle.properties | 1 + settings.gradle | 7 +++++++ 2 files changed, 8 insertions(+) create mode 100644 gradle.properties diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 000000000..5f1ed7bbe --- /dev/null +++ b/gradle.properties @@ -0,0 +1 @@ +org.gradle.caching=true \ No newline at end of file diff --git a/settings.gradle b/settings.gradle index 962dc9128..cda23191b 100644 --- a/settings.gradle +++ b/settings.gradle @@ -3,3 +3,10 @@ rootProject.name = 'Recaf' include 'recaf-core' include 'recaf-ui' +buildCache { + local { + enabled = true + directory = new File('build/cache') + removeUnusedEntriesAfterDays = 30 + } +} \ No newline at end of file From 625c8c2bf3f88e68c7da696522d00eed184b11cc Mon Sep 17 00:00:00 2001 From: Col-E Date: Mon, 9 Oct 2023 18:30:15 -0400 Subject: [PATCH 13/19] Recursively close tree children when double clicking / using key navigation --- .../cell/CellConfigurationService.java | 2 + .../recaf/ui/control/tree/TreeItems.java | 39 ++++++++++++++++++- .../recaf/ui/control/tree/WorkspaceTree.java | 4 ++ 3 files changed, 43 insertions(+), 2 deletions(-) diff --git a/recaf-ui/src/main/java/software/coley/recaf/services/cell/CellConfigurationService.java b/recaf-ui/src/main/java/software/coley/recaf/services/cell/CellConfigurationService.java index 60cfa3970..d93458f5b 100644 --- a/recaf-ui/src/main/java/software/coley/recaf/services/cell/CellConfigurationService.java +++ b/recaf-ui/src/main/java/software/coley/recaf/services/cell/CellConfigurationService.java @@ -124,6 +124,8 @@ public void configure(@Nonnull Cell cell, @Nonnull PathNode item, @Nonnull openPath(item); else if (treeItem.isExpanded()) // Looks odd, but results in less rapid re-closures TreeItems.recurseOpen(treeItem); + else + TreeItems.recurseClose(treeCell.getTreeView(), treeItem); } } } diff --git a/recaf-ui/src/main/java/software/coley/recaf/ui/control/tree/TreeItems.java b/recaf-ui/src/main/java/software/coley/recaf/ui/control/tree/TreeItems.java index 44c904c14..6de0e3836 100644 --- a/recaf-ui/src/main/java/software/coley/recaf/ui/control/tree/TreeItems.java +++ b/recaf-ui/src/main/java/software/coley/recaf/ui/control/tree/TreeItems.java @@ -1,6 +1,10 @@ package software.coley.recaf.ui.control.tree; +import jakarta.annotation.Nonnull; +import javafx.scene.control.MultipleSelectionModel; import javafx.scene.control.TreeItem; +import javafx.scene.control.TreeView; +import software.coley.recaf.util.Unchecked; /** * Utilities for {@link TreeItem} types. @@ -11,7 +15,7 @@ public class TreeItems { /** * Expand all parents to this item. */ - public static void expandParents(TreeItem item) { + public static void expandParents(@Nonnull TreeItem item) { while ((item = item.getParent()) != null) item.setExpanded(true); } @@ -22,9 +26,40 @@ public static void expandParents(TreeItem item) { * @param item * Item to recursively open. */ - public static void recurseOpen(TreeItem item) { + public static void recurseOpen(@Nonnull TreeItem item) { item.setExpanded(true); if (item.getChildren().size() == 1) recurseOpen(item.getChildren().get(0)); } + + /** + * Closes children recursively. + * + * @param tree + * Tree containing the item. + * @param item + * Item to recursively close. + */ + public static void recurseClose(@Nonnull TreeView tree, @Nonnull TreeItem item) { + MultipleSelectionModel> selectionModel = Unchecked.cast(tree.getSelectionModel()); + boolean wasSelected = selectionModel.getSelectedItem() == item; + recurseClose(item); + + // For some reason closing a tree item screws with selection in weird ways. + // So we'll re-select the item afterward if it was previously the selected item. + if (wasSelected) selectionModel.select(item); + } + + /** + * Closes children recursively. + * + * @param item + * Item to recursively close. + */ + private static void recurseClose(@Nonnull TreeItem item) { + if (!item.isLeaf() && item.isExpanded()) { + item.setExpanded(false); + item.getChildren().forEach(TreeItems::recurseClose); + } + } } diff --git a/recaf-ui/src/main/java/software/coley/recaf/ui/control/tree/WorkspaceTree.java b/recaf-ui/src/main/java/software/coley/recaf/ui/control/tree/WorkspaceTree.java index 6cd6469e9..fc4e79436 100644 --- a/recaf-ui/src/main/java/software/coley/recaf/ui/control/tree/WorkspaceTree.java +++ b/recaf-ui/src/main/java/software/coley/recaf/ui/control/tree/WorkspaceTree.java @@ -66,6 +66,10 @@ public WorkspaceTree(@Nonnull CellConfigurationService configurationService, @No TreeItem> selected = getSelectionModel().getSelectedItem(); if (selected != null) TreeItems.recurseOpen(selected); + } else if (code == KeyCode.LEFT || code == KeyCode.KP_LEFT) { + TreeItem> selected = getSelectionModel().getSelectedItem(); + if (selected != null) + TreeItems.recurseClose(this, selected); } }); } From 0cd1fc18fc48a96daa63dd69fe6ed0981f9447c5 Mon Sep 17 00:00:00 2001 From: Col-E Date: Mon, 9 Oct 2023 18:30:30 -0400 Subject: [PATCH 14/19] Change cursor icon on line number graphics --- .../control/richtext/linegraphics/RootLineGraphicFactory.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/recaf-ui/src/main/java/software/coley/recaf/ui/control/richtext/linegraphics/RootLineGraphicFactory.java b/recaf-ui/src/main/java/software/coley/recaf/ui/control/richtext/linegraphics/RootLineGraphicFactory.java index b1c5ed672..b780e71e9 100644 --- a/recaf-ui/src/main/java/software/coley/recaf/ui/control/richtext/linegraphics/RootLineGraphicFactory.java +++ b/recaf-ui/src/main/java/software/coley/recaf/ui/control/richtext/linegraphics/RootLineGraphicFactory.java @@ -1,6 +1,7 @@ package software.coley.recaf.ui.control.richtext.linegraphics; import jakarta.annotation.Nonnull; +import javafx.scene.Cursor; import javafx.scene.Node; import javafx.scene.layout.BorderPane; import software.coley.recaf.ui.control.richtext.Editor; @@ -78,6 +79,7 @@ public Node apply(int paragraph) { // Wrap so the padding of the HBox expands the space of the 'lineno'. BorderPane wrapper = new BorderPane(lineContainer); wrapper.getStyleClass().add("lineno"); + wrapper.setCursor(Cursor.HAND); return wrapper; } From 16d5861a475afd74cf46742829c247157b2c4549 Mon Sep 17 00:00:00 2001 From: Col-E Date: Thu, 12 Oct 2023 01:35:51 -0400 Subject: [PATCH 15/19] Update checker-processor plugin --- build.gradle | 4 ++-- settings.gradle | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/build.gradle b/build.gradle index 9df8452a7..5c6ab107b 100644 --- a/build.gradle +++ b/build.gradle @@ -1,7 +1,7 @@ plugins { - id "com.github.ben-manes.versions" version "0.42.0" apply false + id 'com.github.ben-manes.versions' version '0.42.0' apply false id 'gov.tak.gradle.plugins.coverage-report-aggregator' version '1.3.0' - id 'gov.tak.gradle.plugins.checker-processor' version "1.2.7" apply false + id 'gov.tak.gradle.plugins.checker-processor' version '2.0.0' apply false } allprojects { diff --git a/settings.gradle b/settings.gradle index cda23191b..aa3c95e07 100644 --- a/settings.gradle +++ b/settings.gradle @@ -7,6 +7,5 @@ buildCache { local { enabled = true directory = new File('build/cache') - removeUnusedEntriesAfterDays = 30 } } \ No newline at end of file From 78e9fb018b1167cd49c90dd572dcd0511c831c29 Mon Sep 17 00:00:00 2001 From: Col-E Date: Thu, 12 Oct 2023 01:50:33 -0400 Subject: [PATCH 16/19] Update readme isection with launcher info --- README.md | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 1af2bf00e..7da5bdbf2 100644 --- a/README.md +++ b/README.md @@ -6,8 +6,16 @@ An easy to use modern Java bytecode editor that abstracts away the complexities ## Download -- [Managed launcher](https://github.com/Col-E/Recaf-Launcher) _(Pending)_ -- [Independent releases](https://github.com/Col-E/Recaf/releases) +- [Managed launcher](https://github.com/Col-E/Recaf-Launcher) + - Run the launcher with the following arguments: + - `update-ci -b dev4` + - `update-jfx` + - `compatibility` + - `run` + - To update all you need to do is run the first command. + - To use the `run` command you must have used `update-jfx` command at least once. + - To validate your local environment cn run Recaf the `compatibility` command tells you what conflicts exist, if any. +- [Independent releases](https://github.com/Col-E/Recaf/releases) _(None for 4X currently)_ ## Features From 6b158facd253f865fe8cb2c6f1f022eb7c97777d Mon Sep 17 00:00:00 2001 From: Col-E Date: Thu, 12 Oct 2023 01:55:11 -0400 Subject: [PATCH 17/19] Bit more detail in the launcher instructions --- README.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 7da5bdbf2..90b1f376f 100644 --- a/README.md +++ b/README.md @@ -12,9 +12,10 @@ An easy to use modern Java bytecode editor that abstracts away the complexities - `update-jfx` - `compatibility` - `run` - - To update all you need to do is run the first command. - - To use the `run` command you must have used `update-jfx` command at least once. - - To validate your local environment cn run Recaf the `compatibility` command tells you what conflicts exist, if any. + - To update Recaf use `update-ci -b dev4`. The `-b ` can be used to specify other 4X based branches. + - To run Recaf use the `run` command. + - To use the `run` command you must use the `update-jfx` command at least once. + - To validate your local environment can run Recaf the `compatibility` command tells you what conflicts exist, if any. - [Independent releases](https://github.com/Col-E/Recaf/releases) _(None for 4X currently)_ ## Features From 8cff344d59081434f9825eb92635ff9cb837080f Mon Sep 17 00:00:00 2001 From: Col-E Date: Thu, 12 Oct 2023 14:13:37 -0400 Subject: [PATCH 18/19] Tweak launcher instructions a bit, inclusion of alt auto option --- README.md | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 90b1f376f..ae771d9a9 100644 --- a/README.md +++ b/README.md @@ -7,15 +7,17 @@ An easy to use modern Java bytecode editor that abstracts away the complexities ## Download - [Managed launcher](https://github.com/Col-E/Recaf-Launcher) - - Run the launcher with the following arguments: - - `update-ci -b dev4` - - `update-jfx` - - `compatibility` - - `run` - - To update Recaf use `update-ci -b dev4`. The `-b ` can be used to specify other 4X based branches. - - To run Recaf use the `run` command. - - To use the `run` command you must use the `update-jfx` command at least once. - - To validate your local environment can run Recaf the `compatibility` command tells you what conflicts exist, if any. + - Use the following launcher commands, one after another, to keep Recaf up-to-date and run it: + - `update-ci -b dev4` + - `update-jfx` + - `compatibility` + - `run` + - Or run the launcher with the following argument to do that all for you in one go: + - `auto` + - To update Recaf use `update-ci -b dev4`. The `-b ` can be used to specify other 4X based branches. + - To run Recaf use the `run` command. + - To use the `run` command you must use the `update-jfx` command at least once. + - To validate your local environment can run Recaf the `compatibility` command tells you what conflicts exist, if any. - [Independent releases](https://github.com/Col-E/Recaf/releases) _(None for 4X currently)_ ## Features From 1260283d15607dcb4127a85356a9a79239e5d1c3 Mon Sep 17 00:00:00 2001 From: Col-E Date: Sat, 21 Oct 2023 03:35:13 -0400 Subject: [PATCH 19/19] Update dependencies --- dependencies.gradle | 18 +++++++++--------- .../services/source/DelegatingJavaParser.java | 11 +++++++++++ .../source/JavaContextActionSupport.java | 2 +- 3 files changed, 21 insertions(+), 10 deletions(-) diff --git a/dependencies.gradle b/dependencies.gradle index 8c4f72947..e9c85b997 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -1,6 +1,6 @@ -def asmVersion = '9.5' +def asmVersion = '9.6' def cafeDudeVersion = '1.10.2' -def junitVersion = '5.9.2' +def junitVersion = '5.10.0' project.ext { asm = "org.ow2.asm:asm:$asmVersion" @@ -12,21 +12,21 @@ project.ext { // Use our fork of Android's release with fixes and less transitive dependencies binary_resources = 'com.github.Col-E:binary-resources:31.3.0-alpha01.8' - atlantafx = 'io.github.mkpaz:atlantafx-base:2.0.0' + atlantafx = 'io.github.mkpaz:atlantafx-base:2.0.1' cafedude = "com.github.Col-E:CAFED00D:$cafeDudeVersion" cfr = 'org.benf:cfr:0.152' cdi_api = 'jakarta.enterprise:jakarta.enterprise.cdi-api:4.0.1' - cdi_impl = 'org.jboss.weld.se:weld-se-core:5.1.0.Final' + cdi_impl = 'org.jboss.weld.se:weld-se-core:5.1.2.Final' dex_translator = 'software.coley:dex-translator:1.1.1' directories = 'dev.dirs:directories:26' docking = 'com.github.Col-E:tiwulfx-dock:1.2.3' - downgrader = 'com.github.RaphiMC.JavaDowngrader:core:13e29798a8' + downgrader = 'com.github.RaphiMC.JavaDowngrader:core:1.1.1' extra_collections = 'software.coley:extra-collections:1.2.3' extra_observables = 'software.coley:extra-observables:1.3.0' @@ -50,15 +50,15 @@ project.ext { llzip = 'software.coley:lljzip:2.3.0' - logging_impl = 'ch.qos.logback:logback-classic:1.2.10' // newer releases break in jar releases + logging_impl = 'ch.qos.logback:logback-classic:1.4.11' // newer releases break in jar releases - mockito = 'org.mockito:mockito-core:4.9.0' + mockito = 'org.mockito:mockito-core:5.6.0' - openrewrite = 'org.openrewrite:rewrite-java-17:8.1.3' + openrewrite = 'org.openrewrite:rewrite-java-17:8.7.4' procyon = "org.bitbucket.mstrobel:procyon-compilertools:0.6.0" - picocli = 'info.picocli:picocli:4.7.1' + picocli = 'info.picocli:picocli:4.7.5' regex = 'com.github.tommyettinger:regexodus:0.1.15' diff --git a/recaf-core/src/main/java/software/coley/recaf/services/source/DelegatingJavaParser.java b/recaf-core/src/main/java/software/coley/recaf/services/source/DelegatingJavaParser.java index 6d7ca36af..ee5d43403 100644 --- a/recaf-core/src/main/java/software/coley/recaf/services/source/DelegatingJavaParser.java +++ b/recaf-core/src/main/java/software/coley/recaf/services/source/DelegatingJavaParser.java @@ -32,6 +32,13 @@ public DelegatingJavaParser(@Nonnull JavaParser delegate) { this.delegate = delegate; } + @Nonnull + @Override + public SourceFile requirePrintEqualsInput(@Nonnull SourceFile sourceFile, @Nonnull Input input, @Nullable Path relativeTo, @Nonnull ExecutionContext ctx) { + // Do not do any idempotent source file checks + return sourceFile; + } + @Nonnull @Override public Stream parseInputs(@Nonnull Iterable sources, @@ -41,6 +48,10 @@ public Stream parseInputs(@Nonnull Iterable sources, // The default source-set type generation logic is not well optimized. // We also do not gain significant benefits from it, so we can skip it entirely. ctx.putMessage(SKIP_SOURCE_SET_TYPE_GENERATION, true); + + // Do not do any idempotent source file checks + ctx.putMessage(ExecutionContext.REQUIRE_PRINT_EQUALS_INPUT, false); + return delegate.parseInputs(sources, relativeTo, ctx); } catch (Throwable t) { logger.error("Error while parsing source into AST", t); diff --git a/recaf-ui/src/main/java/software/coley/recaf/ui/control/richtext/source/JavaContextActionSupport.java b/recaf-ui/src/main/java/software/coley/recaf/ui/control/richtext/source/JavaContextActionSupport.java index 8e8b04bd3..40032e4b2 100644 --- a/recaf-ui/src/main/java/software/coley/recaf/ui/control/richtext/source/JavaContextActionSupport.java +++ b/recaf-ui/src/main/java/software/coley/recaf/ui/control/richtext/source/JavaContextActionSupport.java @@ -15,7 +15,6 @@ import org.fxmisc.richtext.model.PlainTextChange; import org.fxmisc.richtext.model.TwoDimensional; import org.kordamp.ikonli.carbonicons.CarbonIcons; -import org.openrewrite.ParseError; import org.openrewrite.ParseExceptionResult; import org.openrewrite.SourceFile; import org.openrewrite.Tree; @@ -23,6 +22,7 @@ import org.openrewrite.java.tree.J; import org.openrewrite.java.tree.JavaType; import org.openrewrite.marker.Range; +import org.openrewrite.tree.ParseError; import software.coley.recaf.analytics.logging.DebuggingLogger; import software.coley.recaf.analytics.logging.Logging; import software.coley.recaf.info.AndroidClassInfo;