Skip to content
Open
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 @@ -11,7 +11,6 @@
import software.coley.recaf.services.workspace.WorkspaceManager;
import software.coley.recaf.ui.RecafTheme;
import software.coley.recaf.ui.config.KeybindingConfig;
import software.coley.recaf.ui.config.WindowScaleConfig;
import software.coley.recaf.ui.menubar.MainMenu;
import software.coley.recaf.ui.pane.LoggingPane;
import software.coley.recaf.ui.docking.DockingManager;
Expand Down Expand Up @@ -57,7 +56,6 @@ public void start(Stage stage) {
wrapper.getStyleClass().addAll("padded", "bg-inset");

// Display
WindowScaleConfig scaleConfig = recaf.get(WindowScaleConfig.class);
Scene scene = new RecafScene(wrapper);
scene.addEventFilter(KeyEvent.KEY_PRESSED, (KeyEvent event) -> {
// Global keybind handling
Expand All @@ -69,18 +67,18 @@ public void start(Stage stage) {
recaf.get(PathExportingManager.class).export(workspaceManager.getCurrent());
}
});
stage.setMinWidth(450 / scaleConfig.getScale());
stage.setMinHeight(200 / scaleConfig.getScale());
stage.setWidth(900 / scaleConfig.getScale());
stage.setHeight(620 / scaleConfig.getScale());
stage.setMinWidth(450);
stage.setMinHeight(200);
stage.setWidth(900);
stage.setHeight(620);
stage.setScene(scene);
stage.getIcons().add(Icons.getImage(Icons.LOGO));
stage.setTitle("Recaf");
stage.setOnCloseRequest(e -> ExitDebugLoggingHook.exit(0));
stage.show();

// Register main window
windowManager.register(WindowManager.WIN_MAIN, stage);
stage.show();

// Publish UI init event
recaf.getContainer().getBeanContainer().getEvent().fire(new UiInitializationEvent());
Expand Down
14 changes: 0 additions & 14 deletions recaf-ui/src/main/java/software/coley/recaf/UIMain.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
import software.coley.recaf.launch.LaunchCommand;
import software.coley.recaf.services.plugin.PluginContainer;
import software.coley.recaf.services.plugin.PluginManager;
import software.coley.recaf.ui.config.WindowScaleConfig;
import software.coley.recaf.util.JFXValidation;
import software.coley.recaf.util.Lang;

Expand Down Expand Up @@ -77,22 +76,9 @@ private static void initialize(@Nonnull Recaf recaf,
launchBootstrap.initPlugins();
initPluginTranslations(recaf);
launchBootstrap.fireInitEvent();
initScale(recaf); // Needs to init after the init-event so config is loaded
RecafApplication.launch(RecafApplication.class, launchArgs.getArgs());
}

/**
* Assigns UI scaling properties based on the window scale config.
*/
private static void initScale(@Nonnull Recaf recaf) {
WindowScaleConfig scaleConfig = recaf.get(WindowScaleConfig.class);

double scale = scaleConfig.getScale();
System.setProperty("sun.java2d.uiScale", String.format("%.0f%%", 100 * scale));
System.setProperty("glass.win.uiScale", String.valueOf(scale));
System.setProperty("glass.gtk.uiScale", String.valueOf(scale));
}

/**
* Configure the JavaFX access logging agent.
* The logging is only active when the agent is passed as a launch argument to Recaf.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,19 @@
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.inject.Instance;
import jakarta.inject.Inject;
import javafx.beans.property.DoubleProperty;
import javafx.geometry.Dimension2D;
import javafx.geometry.Rectangle2D;
import javafx.scene.Scene;
import javafx.stage.Screen;
import javafx.stage.Stage;
import javafx.stage.WindowEvent;
import org.slf4j.Logger;
import software.coley.collections.observable.ObservableList;
import software.coley.recaf.analytics.logging.Logging;
import software.coley.recaf.services.Service;
import software.coley.recaf.ui.config.WindowScaleConfig;
import software.coley.recaf.ui.pane.ScalePane;
import software.coley.recaf.ui.window.IdentifiableStage;
import software.coley.recaf.util.NodeEvents;

Expand Down Expand Up @@ -48,16 +52,18 @@ public class WindowManager implements Service {
// Manager instance data
private final WindowStyling windowStyling;
private final WindowManagerConfig config;
private final WindowScaleConfig scaleConfig;
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,
@Nonnull Instance<IdentifiableStage> stages) {
@Nonnull WindowScaleConfig scaleConfig, @Nonnull Instance<IdentifiableStage> stages) {
this.windowStyling = windowStyling;
this.config = config;
this.scaleConfig = scaleConfig;

// Register identifiable stages.
// These will be @Dependent scoped, so we need to be careful with their instances.
Expand Down Expand Up @@ -99,6 +105,8 @@ public void register(@Nonnull String id, @Nonnull Stage stage) {
if (windowMappings.containsKey(id))
throw new IllegalStateException("The stage ID was already registered: " + id);

applyScale(stage);

// Add custom stylesheets if any are registered.
if (!windowStyling.getStylesheetUris().isEmpty())
NodeEvents.runOnceIfPresentOrOnChange(stage.sceneProperty(),
Expand Down Expand Up @@ -166,6 +174,26 @@ public void register(@Nonnull String id, @Nonnull Stage stage) {
logger.trace("Register stage: {}", id);
}

/**
* Wraps the stage's scene root in a {@link ScalePane}
*/
private void applyScale(@Nonnull Stage stage) {
var scale = scaleConfig.scaleProperty();
stage.sceneProperty().addListener((_, _, scene) -> wrapSceneRoot(scene, scale));
wrapSceneRoot(stage.getScene(), scale);
}

private static void wrapSceneRoot(@Nullable Scene scene, @Nonnull DoubleProperty scale) {
if (scene == null)
return;

var root = scene.getRoot();
if (root == null || root instanceof ScalePane)
return;

scene.setRoot(new ScalePane(root, scale));
}

/**
* Do not use this list to iterate over if within your loop you will be closing/creating windows.
* This will cause a {@link ConcurrentModificationException}. Wrap this result in a new collection
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,22 @@
package software.coley.recaf.ui.config;

import atlantafx.base.theme.Styles;
import jakarta.annotation.Nonnull;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import javafx.scene.Node;
import javafx.beans.binding.Bindings;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.geometry.Pos;
import javafx.scene.control.Label;
import javafx.scene.control.Slider;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.Region;
import software.coley.observables.ObservableDouble;
import software.coley.recaf.config.*;
import software.coley.recaf.services.config.ConfigComponentManager;
import software.coley.recaf.services.config.KeyedConfigComponentFactory;
import software.coley.recaf.util.FxThreadUtil;

/**
* Config for window scaling.
Expand All @@ -18,30 +26,68 @@
@ApplicationScoped
public class WindowScaleConfig extends BasicConfigContainer {
public static final String ID = "window-scale";
private static final double MIN = 0.5;
private static final double MAX = 2.0;
private final ObservableDouble scale = new ObservableDouble(1);
private final DoubleProperty scaleProperty = new SimpleDoubleProperty(1);
private Slider slider;

@Inject
public WindowScaleConfig(@Nonnull ConfigComponentManager componentManager) {
super(ConfigGroups.SERVICE_UI, ID + CONFIG_SUFFIX);
addValue(new BasicConfigValue<>("scale", double.class, scale));

scaleProperty.set(getScale());
scale.addChangeListener((_, _, cur) -> FxThreadUtil.run(() -> scaleProperty.set(Math.clamp(cur, MIN, MAX))));

componentManager.register(this, "scale", false, (container, value) -> {
slider = new Slider(0.5, 2.0, getScale());
slider = new Slider(MIN, MAX, getScale());
slider.setSnapToTicks(true);
slider.setShowTickLabels(true);
slider.setBlockIncrement(0.5);
slider.setBlockIncrement(0.25);
slider.setMajorTickUnit(0.5);
slider.setMinorTickCount(5);
slider.valueProperty().addListener((ob, old, cur) -> scale.setValue(cur.doubleValue()));
return slider;
slider.setMinorTickCount(1);

//Live readout of the current setting
var readout = new Label();
readout.textProperty().bind(Bindings.createStringBinding(() -> Math.round(slider.getValue() * 100) + "%", slider.valueProperty()));
readout.getStyleClass().add(Styles.TEXT_SUBTLE);
readout.setMinWidth(Region.USE_PREF_SIZE);
readout.setPrefWidth(45);
readout.setAlignment(Pos.CENTER_RIGHT);

//Commit only when not dragging
slider.valueProperty().addListener((_, _, cur) -> {
if (!slider.isValueChanging())
commitScale(cur.doubleValue());
});
slider.valueChangingProperty().addListener((_, _, isChanging) -> {
if (!isChanging)
commitScale(slider.getValue());
});

var box = new HBox(10, slider, readout);
box.setAlignment(Pos.CENTER_LEFT);
HBox.setHgrow(slider, Priority.ALWAYS);
return box;
});
}

private void commitScale(double value) {
var clamped = Math.clamp(value, MIN, MAX);
if (Double.compare(clamped, scale.getValue()) != 0)
scale.setValue(clamped);
}

@Nonnull
public DoubleProperty scaleProperty() {
return scaleProperty;
}

/**
* @return Window scale.
*/
public double getScale() {
return Math.clamp(scale.getValue(), 0.5, 2);
return Math.clamp(scale.getValue(), MIN, MAX);
}
}
55 changes: 55 additions & 0 deletions recaf-ui/src/main/java/software/coley/recaf/ui/pane/ScalePane.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package software.coley.recaf.ui.pane;

import jakarta.annotation.Nonnull;
import javafx.beans.property.DoubleProperty;
import javafx.scene.Node;
import javafx.scene.layout.Pane;
import javafx.scene.transform.Scale;

/**
* Wraps a child node and scales it by {@code scale} while keeping it sized to fill this pane.
*/
public class ScalePane extends Pane {
private final DoubleProperty scale;
private final Node content;

/**
* @param content
* Original scene root to wrap (must have no parent!).
* @param scale
* Factor to scale by.
*/
public ScalePane(@Nonnull Node content, @Nonnull DoubleProperty scale) {
this.scale = scale;
this.content = content;

//Pivot so the child grows down/right to fill the window
var transform = new Scale();
transform.xProperty().bind(scale);
transform.yProperty().bind(scale);
content.getTransforms().add(transform);

//Reparent the orphaned root into this pane
getChildren().add(content);

scale.addListener(o -> requestLayout());
}

/**
* @return The wrapped original root.
*/
@Nonnull
public Node getContent() {
return content;
}

@Override
protected void layoutChildren() {
var scale = this.scale.get();
if (scale <= 0)
scale = 1;

content.resize(getWidth() / scale, getHeight() / scale);
content.relocate(0, 0);
}
}
Loading