From 588637c19957017691292e0fe50cd240fa4b13c8 Mon Sep 17 00:00:00 2001 From: SooStrator <49093091+SooStrator1136@users.noreply.github.com> Date: Wed, 27 May 2026 22:47:21 +0200 Subject: [PATCH 1/3] Center subwindows on main window screen --- .../recaf/services/window/WindowManager.java | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) 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 87a9527a9..97d5e2699 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,6 +5,8 @@ import jakarta.enterprise.context.ApplicationScoped; import jakarta.enterprise.inject.Instance; import jakarta.inject.Inject; +import javafx.scene.Scene; +import javafx.stage.Screen; import javafx.stage.Stage; import javafx.stage.WindowEvent; import org.slf4j.Logger; @@ -17,6 +19,7 @@ import java.util.Collection; import java.util.ConcurrentModificationException; import java.util.HashMap; +import java.util.IdentityHashMap; import java.util.Map; import java.util.Objects; import java.util.UUID; @@ -46,6 +49,7 @@ public class WindowManager implements Service { private final WindowManagerConfig config; private final ObservableList activeWindows = new ObservableList<>(); private final Map windowMappings = new HashMap<>(); + private final Map lastStageScreen = new IdentityHashMap<>(); @Inject public WindowManager(@Nonnull WindowStyling windowStyling, @Nonnull WindowManagerConfig config, @@ -98,12 +102,35 @@ public void register(@Nonnull String id, @Nonnull Stage stage) { NodeEvents.runOnceIfPresentOrOnChange(stage.sceneProperty(), scene -> scene.getStylesheets().addAll(windowStyling.getStylesheetUris())); + // When a window is about to show, check if the main window has moved to a different screen since the last time + // this subwindow was visible. If so, center the subwindow on the main window. + stage.addEventFilter(WindowEvent.WINDOW_SHOWING, e -> { + Stage mainWindow = windowMappings.get(WIN_MAIN); + if (mainWindow == null || stage == mainWindow) + return; + + Screen mainScreen = getScreenForStage(mainWindow); + if (mainScreen == null) + return; + + Screen lastScreen = lastStageScreen.get(stage); + if (lastScreen == null || mainScreen != lastScreen) { + Scene scene = stage.getScene(); + var stageWidth = stage.getWidth() > 0 ? stage.getWidth() : scene.getWidth(); + var stageHeight = stage.getHeight() > 0 ? stage.getHeight() : scene.getHeight(); + + stage.setX(mainWindow.getX() + ((mainWindow.getWidth() - stageWidth) / 2)); + stage.setY(mainWindow.getY() + ((mainWindow.getHeight() - stageHeight) / 2)); + } + }); + // Record when windows are 'active' based on visibility. // 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); + lastStageScreen.put(stage, getScreenForStage(stage)); }); stage.addEventFilter(WindowEvent.WINDOW_HIDDEN, e -> { logger.trace("Stage hiding: {}", id); @@ -114,6 +141,7 @@ public void register(@Nonnull String id, @Nonnull Stage stage) { if (id.startsWith(ANON_PREFIX)) { logger.trace("Stage pruned: {} ({})", id, stage.getTitle()); windowMappings.remove(id); + lastStageScreen.remove(stage); } }); @@ -216,4 +244,24 @@ public String getServiceId() { public WindowManagerConfig getServiceConfig() { return config; } + + /** + * Determines which {@link Screen} the given stage's center is on. + * + * @param stage Stage to find the screen for. + * + * @return Screen containing the stage's center point. + */ + @Nullable + private Screen getScreenForStage(@Nonnull Stage stage) { + var centerX = stage.getX() + (stage.getWidth() / 2); + var centerY = stage.getY() + (stage.getHeight() / 2); + + for (var screen : Screen.getScreens()) { + if (screen.getBounds().contains(centerX, centerY)) + return screen; + } + + return null; + } } From 28b8ec4e196f066097effaaad0a5323831ced7e2 Mon Sep 17 00:00:00 2001 From: Matt Date: Thu, 28 May 2026 04:00:36 -0400 Subject: [PATCH 2/3] Change deobf/workspace-edit + popped dockables to track as anonymous stages --- .../java/software/coley/recaf/ui/docking/DockingManager.java | 2 +- .../java/software/coley/recaf/ui/menubar/AnalysisMenu.java | 2 +- .../main/java/software/coley/recaf/ui/menubar/FileMenu.java | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) 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 ca1566a19..b8d1959e4 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 @@ -169,7 +169,7 @@ public DockingManager(@Nonnull WorkspaceManager workspaceManager, // Stages created via our docking framework need to be tracked in the window manager. bento.stageBuilding().setStageFactory(originScene -> { DragDropStage stage = new DragDropStage(true); - windowManager.register("dnd-" + UUID.randomUUID(), stage); + windowManager.registerAnonymous(stage); return stage; }); bento.stageBuilding().setSceneFactory((sourceScene, content, width, height) -> { diff --git a/recaf-ui/src/main/java/software/coley/recaf/ui/menubar/AnalysisMenu.java b/recaf-ui/src/main/java/software/coley/recaf/ui/menubar/AnalysisMenu.java index d4afaeee7..6f2daae10 100644 --- a/recaf-ui/src/main/java/software/coley/recaf/ui/menubar/AnalysisMenu.java +++ b/recaf-ui/src/main/java/software/coley/recaf/ui/menubar/AnalysisMenu.java @@ -65,6 +65,6 @@ private void openDeobfuscation() { deobfuscationWindow.show(); deobfuscationWindow.requestFocus(); deobfuscationWindow.setOnCloseRequest(e -> deobfuscationWindowProvider.destroy(deobfuscationWindow)); - windowManager.register("deobfuscation-" + UUID.randomUUID(), deobfuscationWindow); + windowManager.registerAnonymous(deobfuscationWindow); } } diff --git a/recaf-ui/src/main/java/software/coley/recaf/ui/menubar/FileMenu.java b/recaf-ui/src/main/java/software/coley/recaf/ui/menubar/FileMenu.java index 3def4cc8b..7e726a2e1 100644 --- a/recaf-ui/src/main/java/software/coley/recaf/ui/menubar/FileMenu.java +++ b/recaf-ui/src/main/java/software/coley/recaf/ui/menubar/FileMenu.java @@ -187,13 +187,13 @@ public void refreshRecent() { */ private void openWorkspace() { RecafStage stage = new RecafStage().hideOnEscape(); + windowManager.registerAnonymous(stage); WorkspaceBuilderPane root = new WorkspaceBuilderPane(pathLoadingManager, recentFilesConfig, () -> FxThreadUtil.run(stage::close)); stage.titleProperty().bind(Lang.getBinding("dialog.title.create-workspace")); stage.setScene(new RecafScene(root)); stage.setMinWidth(650); stage.setMinHeight(400); stage.show(); - windowManager.registerAnonymous(stage); } /** @@ -203,13 +203,13 @@ private void addToWorkspace() { if (!workspaceManager.hasCurrentWorkspace()) return; RecafStage stage = new RecafStage().hideOnEscape(); + windowManager.registerAnonymous(stage); WorkspaceBuilderPane root = new WorkspaceBuilderPane(pathLoadingManager, recentFilesConfig, workspaceManager.getCurrent(), () -> FxThreadUtil.run(stage::close)); stage.titleProperty().bind(Lang.getBinding("menu.file.addtoworkspace")); stage.setScene(new RecafScene(root)); stage.setMinWidth(650); stage.setMinHeight(400); stage.show(); - windowManager.registerAnonymous(stage); } /** From aa063f6dde72d8e691d8a0d4e0421faa428bf5d1 Mon Sep 17 00:00:00 2001 From: Matt Date: Thu, 28 May 2026 04:14:56 -0400 Subject: [PATCH 3/3] Move sizing to SHOW instead of SHOWING, use real-size when possible --- .../recaf/services/window/WindowManager.java | 96 ++++++++++++++++--- 1 file changed, 81 insertions(+), 15 deletions(-) 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 97d5e2699..34d6ee3de 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,8 @@ import jakarta.enterprise.context.ApplicationScoped; import jakarta.enterprise.inject.Instance; import jakarta.inject.Inject; -import javafx.scene.Scene; +import javafx.geometry.Dimension2D; +import javafx.geometry.Rectangle2D; import javafx.stage.Screen; import javafx.stage.Stage; import javafx.stage.WindowEvent; @@ -50,6 +51,7 @@ public class WindowManager implements Service { private final ObservableList activeWindows = new ObservableList<>(); private final Map windowMappings = new HashMap<>(); private final Map lastStageScreen = new IdentityHashMap<>(); + private final Map pendingStageRecentering = new IdentityHashMap<>(); @Inject public WindowManager(@Nonnull WindowStyling windowStyling, @Nonnull WindowManagerConfig config, @@ -113,15 +115,11 @@ public void register(@Nonnull String id, @Nonnull Stage stage) { if (mainScreen == null) return; + // Schedule a recenter if the main window has moved since the last time this stage was visible. + // We can't do this yet since the stage content hasn't been sized. Screen lastScreen = lastStageScreen.get(stage); - if (lastScreen == null || mainScreen != lastScreen) { - Scene scene = stage.getScene(); - var stageWidth = stage.getWidth() > 0 ? stage.getWidth() : scene.getWidth(); - var stageHeight = stage.getHeight() > 0 ? stage.getHeight() : scene.getHeight(); - - stage.setX(mainWindow.getX() + ((mainWindow.getWidth() - stageWidth) / 2)); - stage.setY(mainWindow.getY() + ((mainWindow.getHeight() - stageHeight) / 2)); - } + if (lastScreen == null || mainScreen != lastScreen) + pendingStageRecentering.put(stage, true); }); // Record when windows are 'active' based on visibility. @@ -130,11 +128,26 @@ public void register(@Nonnull String id, @Nonnull Stage stage) { stage.addEventFilter(WindowEvent.WINDOW_SHOWN, e -> { logger.trace("Stage showing: {}", id); activeWindows.add(stage); + + // Check if we had a pending recenter for this stage. + // Since we're in the 'shown' event, the stage's content should be properly sized and ready to be centered if needed. + if (pendingStageRecentering.remove(stage) != null) { + Stage mainWindow = windowMappings.get(WIN_MAIN); + if (mainWindow != null && stage != mainWindow) { + Screen mainScreen = getScreenForStage(mainWindow); + Dimension2D size = getStageSize(stage); + if (mainScreen != null && size != null) + recenterStage(stage, mainWindow, mainScreen, size); + } + } + + // Track the screen the stage is currently on so we can detect moves for future re-centers. lastStageScreen.put(stage, getScreenForStage(stage)); }); stage.addEventFilter(WindowEvent.WINDOW_HIDDEN, e -> { logger.trace("Stage hiding: {}", id); activeWindows.remove(stage); + pendingStageRecentering.remove(stage); // 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. @@ -245,23 +258,76 @@ public WindowManagerConfig getServiceConfig() { return config; } + /** + * Centers the stage on the main window, ensuring it stays within the bounds of the screen. + * + * @param stage + * Stage to center. + * @param mainWindow + * Main window to center on. + * @param mainScreen + * Main window's screen, used for bounds checking. + * @param stageSize + * Size of the stage to center. + */ + private static void recenterStage(@Nonnull Stage stage, @Nonnull Stage mainWindow, @Nonnull Screen mainScreen, + @Nonnull Dimension2D stageSize) { + Rectangle2D bounds = mainScreen.getVisualBounds(); + double centeredX = mainWindow.getX() + ((mainWindow.getWidth() - stageSize.getWidth()) / 2); + double centeredY = mainWindow.getY() + ((mainWindow.getHeight() - stageSize.getHeight()) / 2); + double maxX = Math.max(bounds.getMinX(), bounds.getMaxX() - stageSize.getWidth()); + double maxY = Math.max(bounds.getMinY(), bounds.getMaxY() - stageSize.getHeight()); + + stage.setX(Math.clamp(centeredX, bounds.getMinX(), maxX)); + stage.setY(Math.clamp(centeredY, bounds.getMinY(), maxY)); + } + /** * Determines which {@link Screen} the given stage's center is on. * - * @param stage Stage to find the screen for. + * @param stage + * Stage to find the screen for. * * @return Screen containing the stage's center point. */ @Nullable private Screen getScreenForStage(@Nonnull Stage stage) { - var centerX = stage.getX() + (stage.getWidth() / 2); - var centerY = stage.getY() + (stage.getHeight() / 2); - - for (var screen : Screen.getScreens()) { - if (screen.getBounds().contains(centerX, centerY)) + double centerX = stage.getX() + (stage.getWidth() / 2); + double centerY = stage.getY() + (stage.getHeight() / 2); + for (Screen screen : Screen.getScreens()) { + // Check true center, and then top center (in case stage is dragged near the bottom of the screen). + Rectangle2D bounds = screen.getBounds(); + if (bounds.contains(centerX, centerY) || bounds.contains(centerX, stage.getY())) return screen; } + return null; + } + + /** + * Attempts to get the size of the stage. + * + * @param stage + * Stage to get the size of. + * + * @return Stage size, if known. + */ + @Nullable + private static Dimension2D getStageSize(@Nonnull Stage stage) { + double width = stage.getWidth(); + double height = stage.getHeight(); + if (isRealized(width) && isRealized(height)) + return new Dimension2D(width, height); + + // Fallback to min-size if set. + width = stage.getMinWidth(); + height = stage.getMinHeight(); + if (isRealized(width) && isRealized(height)) + return new Dimension2D(width, height); return null; } + + private static boolean isRealized(double value) { + return Double.isFinite(value) && value > 0; + } }