Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.inject.Instance;
import jakarta.inject.Inject;
import javafx.geometry.Dimension2D;
import javafx.geometry.Rectangle2D;
import javafx.stage.Screen;
import javafx.stage.Stage;
import javafx.stage.WindowEvent;
import org.slf4j.Logger;
Expand All @@ -17,6 +20,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;
Expand Down Expand Up @@ -46,6 +50,8 @@ public class WindowManager implements Service {
private final WindowManagerConfig config;
private final ObservableList<Stage> activeWindows = new ObservableList<>();
private final Map<String, Stage> windowMappings = new HashMap<>();
private final Map<Stage, Screen> lastStageScreen = new IdentityHashMap<>();
private final Map<Stage, Boolean> pendingStageRecentering = new IdentityHashMap<>();

@Inject
public WindowManager(@Nonnull WindowStyling windowStyling, @Nonnull WindowManagerConfig config,
Expand Down Expand Up @@ -98,22 +104,57 @@ 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;

// 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)
pendingStageRecentering.put(stage, true);
});

// 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);

// 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.
if (id.startsWith(ANON_PREFIX)) {
logger.trace("Stage pruned: {} ({})", id, stage.getTitle());
windowMappings.remove(id);
lastStageScreen.remove(stage);
}
});

Expand Down Expand Up @@ -216,4 +257,77 @@ public String getServiceId() {
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.
*
* @return Screen containing the stage's center point.
*/
@Nullable
private Screen getScreenForStage(@Nonnull Stage stage) {
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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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) -> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

/**
Expand All @@ -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);
}

/**
Expand Down