Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
1aecac4
Refactor: Replace FileUtils calls with Controllers methods for file d…
Glavo Jul 17, 2026
cddefb3
Refactor: Update saveWindowStates method to handle window state saving
Glavo Jul 17, 2026
a52b00f
Refactor: Simplify window state management and remove redundant methods
Glavo Jul 17, 2026
4e76144
update
Glavo Jul 17, 2026
a7a60cb
Refactor: Update width and height bindings to use window properties
Glavo Jul 17, 2026
47ebdf6
Refactor: Update maxHeight calculation to use Controllers for window …
Glavo Jul 17, 2026
031d1e1
Refactor: Remove unused Stage import from ResourcePackListPage
Glavo Jul 17, 2026
ffe2b3d
Refactor: Remove unused getScene method from Controllers
Glavo Jul 17, 2026
5603be9
Refactor: Consolidate width binding calculations in ModListPageSkin
Glavo Jul 18, 2026
c720582
Refactor: Rename stage properties to content properties for clarity
Glavo Jul 18, 2026
c95a718
Refactor: Replace SCREEN constant with PRIMARY_SCREEN_BOUNDS for impr…
Glavo Jul 18, 2026
e7befe1
Refactor: Update screen bounds retrieval to use dynamic screen dimens…
Glavo Jul 18, 2026
74a8ce3
Refactor: Update content width and height bindings to use initialized…
Glavo Jul 18, 2026
f78e6a4
Refactor: Simplify window state management by integrating state savin…
Glavo Jul 18, 2026
f5d5737
Refactor: Ensure window property fallback to a new SimpleObjectProper…
Glavo Jul 18, 2026
3cd4a95
Refactor: Replace FileSaver with SettingsManager for improved shutdow…
Glavo Jul 19, 2026
cb22ca3
Refactor: Enhance shutdown process by saving pending changes before f…
Glavo Jul 20, 2026
ca4bf14
Refactor: Replace FileSaver shutdown call with SettingsManager for im…
Glavo Jul 20, 2026
140f4c8
Refactor: Remove unused import of SettingsManager from EntryPoint.java
Glavo Jul 20, 2026
490cd0e
Refactor: Remove unused import of SMModel from UpdateHandler.java
Glavo Jul 20, 2026
6fb3b32
Refactor: Update content position initialization in Controllers.java
Glavo Jul 20, 2026
bbe5151
Merge remote-tracking branch 'upstream/main' into stage
Glavo Jul 20, 2026
031908d
refactor: Replace Stage usage with Controllers methods in ResourcePac…
Glavo Jul 20, 2026
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
2 changes: 1 addition & 1 deletion HMCL/src/main/java/org/jackhuang/hmcl/Launcher.java
Original file line number Diff line number Diff line change
Expand Up @@ -266,7 +266,7 @@ private static void checkConfigInTempDir() {
@Override
public void stop() throws Exception {
Controllers.onApplicationStop();
FileSaver.shutdown();
SettingsManager.shutdown();
LOG.shutdown();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,14 @@ private LoadResult<T> result(T value, SettingFileAccess access) {
///
/// @param value the settings object to observe
void installAutoSave(T value) {
value.addListener(source -> save(value));
value.addListener(observable -> {
if (value.shouldSaveImmediately(observable)) {
save(value);
value.setSavePending(false);
} else {
value.setSavePending(true);
}
});
}

/// Saves a settings object.
Expand Down
10 changes: 10 additions & 0 deletions HMCL/src/main/java/org/jackhuang/hmcl/setting/LauncherState.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@

import com.google.gson.annotations.JsonAdapter;
import com.google.gson.annotations.SerializedName;
import javafx.beans.Observable;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleDoubleProperty;
Expand Down Expand Up @@ -54,6 +55,15 @@ public LauncherState() {
register();
}

/// Returns whether the changed field should trigger automatic persistence immediately.
@Override
public boolean shouldSaveImmediately(Observable observable) {
return observable != x
&& observable != y
&& observable != width
&& observable != height;
}

/// The schema used by this launcher state file.
@SerializedName(JsonSchema.PROPERTY_SCHEMA)
private final ObjectProperty<JsonSchema> schema = new SimpleObjectProperty<>(CURRENT_SCHEMA);
Expand Down
14 changes: 14 additions & 0 deletions HMCL/src/main/java/org/jackhuang/hmcl/setting/SettingsManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,20 @@ public final class SettingsManager {
private SettingsManager() {
}

/// Saves deferred launcher state changes and shutdown file saver.
public static void shutdown() {
savePendingChanges();
FileSaver.shutdown();
}

/// Saves deferred launcher state changes.
public static void savePendingChanges() {
if (launcherState != null && launcherState.isSavable() && launcherState.isSavePending()) {
launcherState.setSavePending(false);
STATE_FILE.save(launcherState);
}
}

/// The local directory storing per-workspace configuration files.
private static final Path LOCAL_CONFIG_FILES_DIRECTORY = Metadata.HMCL_LOCAL_HOME.resolve("config");

Expand Down
210 changes: 88 additions & 122 deletions HMCL/src/main/java/org/jackhuang/hmcl/ui/Controllers.java
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,7 @@
import javafx.scene.control.Label;
import javafx.scene.layout.Region;
import javafx.scene.paint.Color;
import javafx.stage.Screen;
import javafx.stage.Stage;
import javafx.stage.StageStyle;
import javafx.stage.*;
import javafx.util.Duration;
import org.jackhuang.hmcl.Launcher;
import org.jackhuang.hmcl.Metadata;
Expand Down Expand Up @@ -73,6 +71,7 @@
import org.jetbrains.annotations.Nullable;

import java.io.IOException;
import java.nio.file.Path;
import java.time.LocalDate;
import java.util.List;
import java.util.concurrent.CompletableFuture;
Expand All @@ -97,12 +96,12 @@ public final class Controllers {
public static final int MIN_CONTENT_HEIGHT = 450 + 2 + 40; // bg height + border width*2 + toolbar height
public static final int MIN_WIDTH = MIN_CONTENT_WIDTH + CUSTOM_DECORATION_SHADOW_EXTENT;
public static final int MIN_HEIGHT = MIN_CONTENT_HEIGHT + CUSTOM_DECORATION_SHADOW_EXTENT;
public static final Screen SCREEN = Screen.getPrimary();
public static final Rectangle2D PRIMARY_SCREEN_BOUNDS = Screen.getPrimary().getBounds();
private static InvalidationListener stageSizeChangeListener;
private static DoubleProperty stageX = new SimpleDoubleProperty();
private static DoubleProperty stageY = new SimpleDoubleProperty();
private static DoubleProperty stageWidth = new SimpleDoubleProperty();
private static DoubleProperty stageHeight = new SimpleDoubleProperty();
private static final DoubleProperty contentX = new SimpleDoubleProperty();
private static final DoubleProperty contentY = new SimpleDoubleProperty();
private static final DoubleProperty contentWidth = new SimpleDoubleProperty();
private static final DoubleProperty contentHeight = new SimpleDoubleProperty();

private static Scene scene;
private static Stage stage;
Expand Down Expand Up @@ -133,12 +132,16 @@ public interface ThrowingRunnable {
void run() throws Exception;
}

public static Scene getScene() {
return scene;
public static @Nullable Stage getStage() {
return stage;
}

public static Stage getStage() {
return stage;
public static ReadOnlyDoubleProperty windowWidthProperty() {
return contentWidth;
}

public static ReadOnlyDoubleProperty windowHeightProperty() {
return contentHeight;
}

@FXThread
Expand Down Expand Up @@ -214,64 +217,8 @@ public static DecoratorController getDecorator() {
return decorator;
}

public static void saveWindowStates() {
saveWindowBounds();
}

private static void saveWindowBounds() {
if (stageX != null) {
state().setX(toContentX(stageX.get()) / SCREEN.getBounds().getWidth());
}
if (stageY != null) {
state().setY(toContentY(stageY.get()) / SCREEN.getBounds().getHeight());
}
if (stageHeight != null) {
state().setHeight(toContentHeight(stageHeight.get()));
}
if (stageWidth != null) {
state().setWidth(toContentWidth(stageWidth.get()));
}
}

public static void onApplicationStop() {
stageSizeChangeListener = null;
saveWindowBounds();
stageX = null;
stageY = null;
stageHeight = null;
stageWidth = null;
}

private static double toContentX(double stageX) {
return stageX + CUSTOM_DECORATION_SHADOW_SIZE;
}

private static double toContentY(double stageY) {
return stageY + CUSTOM_DECORATION_SHADOW_SIZE;
}

private static double toStageX(double contentX) {
return contentX - CUSTOM_DECORATION_SHADOW_SIZE;
}

private static double toStageY(double contentY) {
return contentY - CUSTOM_DECORATION_SHADOW_SIZE;
}

private static double toContentWidth(double stageWidth) {
return Math.max(0.0, stageWidth - CUSTOM_DECORATION_SHADOW_EXTENT);
}

private static double toContentHeight(double stageHeight) {
return Math.max(0.0, stageHeight - CUSTOM_DECORATION_SHADOW_EXTENT);
}

private static double toStageWidth(double contentWidth) {
return contentWidth + CUSTOM_DECORATION_SHADOW_EXTENT;
}

private static double toStageHeight(double contentHeight) {
return contentHeight + CUSTOM_DECORATION_SHADOW_EXTENT;
}

public static void initialize(Stage stage) {
Expand All @@ -285,60 +232,22 @@ public static void initialize(Stage stage) {
LOG.info("Enable sub-pixel antialiasing");
System.getProperties().put("prism.lcdtext", "true");
} else if ("gray".equalsIgnoreCase(fontAntiAliasing)
|| OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS && SCREEN.getOutputScaleX() > 1) {
|| OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS && Screen.getPrimary().getOutputScaleX() > 1) {
LOG.info("Disable sub-pixel antialiasing");
System.getProperties().put("prism.lcdtext", "false");
}
}

Controllers.stage = stage;

stageSizeChangeListener = o -> {
ReadOnlyDoubleProperty sourceProperty = (ReadOnlyDoubleProperty) o;
DoubleProperty targetProperty;
switch (sourceProperty.getName()) {
case "x": {
targetProperty = stageX;
break;
}
case "y": {
targetProperty = stageY;
break;
}
case "width": {
targetProperty = stageWidth;
break;
}
case "height": {
targetProperty = stageHeight;
break;
}
default: {
targetProperty = null;
}
}

if (targetProperty != null
&& Controllers.stage != null
&& !Controllers.stage.isIconified()
// https://github.com/HMCL-dev/HMCL/issues/4290
&& (OperatingSystem.CURRENT_OS == OperatingSystem.MACOS ||
!Controllers.stage.isFullScreen() && !Controllers.stage.isMaximized())
) {
targetProperty.set(sourceProperty.get());
}
};

WeakInvalidationListener weakListener = new WeakInvalidationListener(stageSizeChangeListener);

double initContentWidth = Math.max(MIN_CONTENT_WIDTH, state().getWidth());
double initContentHeight = Math.max(MIN_CONTENT_HEIGHT, state().getHeight());
double initWidth = toStageWidth(initContentWidth);
double initHeight = toStageHeight(initContentHeight);
double initWidth = initContentWidth + CUSTOM_DECORATION_SHADOW_EXTENT;
double initHeight = initContentHeight + CUSTOM_DECORATION_SHADOW_EXTENT;

{
double initContentX = state().getX() * SCREEN.getBounds().getWidth();
double initContentY = state().getY() * SCREEN.getBounds().getHeight();
double initContentX = state().getX() * PRIMARY_SCREEN_BOUNDS.getWidth();
double initContentY = state().getY() * PRIMARY_SCREEN_BOUNDS.getHeight();

boolean invalid = true;
double border = 20D;
Expand All @@ -355,24 +264,65 @@ public static void initialize(Stage stage) {
}

if (invalid) {
initContentX = (0.5D - initContentWidth / SCREEN.getBounds().getWidth() / 2)
* SCREEN.getBounds().getWidth();
initContentY = (0.5D - initContentHeight / SCREEN.getBounds().getHeight() / 2)
* SCREEN.getBounds().getHeight();
initContentX = (0.5D - initContentWidth / PRIMARY_SCREEN_BOUNDS.getWidth() / 2)
* PRIMARY_SCREEN_BOUNDS.getWidth();
initContentY = (0.5D - initContentHeight / PRIMARY_SCREEN_BOUNDS.getHeight() / 2)
* PRIMARY_SCREEN_BOUNDS.getHeight();
}

double initX = toStageX(initContentX);
double initY = toStageY(initContentY);
double initX = initContentX - CUSTOM_DECORATION_SHADOW_SIZE;
double initY = initContentY - CUSTOM_DECORATION_SHADOW_SIZE;
stage.setX(initX);
stage.setY(initY);
stageX.set(initX);
stageY.set(initY);
contentX.set(initContentX);
contentY.set(initContentY);
}

stage.setHeight(initHeight);
stage.setWidth(initWidth);
stageHeight.set(initHeight);
stageWidth.set(initWidth);
contentHeight.set(initContentHeight);
contentWidth.set(initContentWidth);

stageSizeChangeListener = o -> {
ReadOnlyDoubleProperty property = (ReadOnlyDoubleProperty) o;
Stage currentStage = property.getBean() instanceof Stage s ? s : null;
if (currentStage == null)
return;

boolean saveState = !currentStage.isIconified()
// https://github.com/HMCL-dev/HMCL/issues/4290
&& (OperatingSystem.CURRENT_OS == OperatingSystem.MACOS
|| !currentStage.isFullScreen() && !currentStage.isMaximized());

switch (property.getName()) {
case "x" -> {
double value = property.get() + CUSTOM_DECORATION_SHADOW_SIZE;
contentX.set(value);
if (saveState)
state().setX(value / PRIMARY_SCREEN_BOUNDS.getWidth());
}
case "y" -> {
double value = property.get() + CUSTOM_DECORATION_SHADOW_SIZE;
contentY.set(value);
if (saveState)
state().setY(value / PRIMARY_SCREEN_BOUNDS.getHeight());
}
case "width" -> {
double value = Math.max(MIN_CONTENT_WIDTH, property.get() - CUSTOM_DECORATION_SHADOW_EXTENT);
contentWidth.set(value);
if (saveState)
state().setWidth(value);
}
case "height" -> {
double value = Math.max(MIN_CONTENT_HEIGHT, property.get() - CUSTOM_DECORATION_SHADOW_EXTENT);
contentHeight.set(value);
if (saveState)
state().setHeight(value);
}
}
};

WeakInvalidationListener weakListener = new WeakInvalidationListener(stageSizeChangeListener);
stage.xProperty().addListener(weakListener);
stage.yProperty().addListener(weakListener);
stage.heightProperty().addListener(weakListener);
Expand Down Expand Up @@ -585,7 +535,7 @@ public static void confirm(String text, String title, MessageType type, Runnable

/// Shows a warning that confirms backing up a read-only settings file before overwriting it.
///
/// @param text the file-specific read-only warning
/// @param text the file-specific read-only warning
/// @param overwrite the action that backs up and overwrites the file
public static void confirmBackupAndOverwrite(String text, ThrowingRunnable overwrite) {
dialog(new MessageDialogPane.Builder(
Expand Down Expand Up @@ -694,6 +644,22 @@ public static void showToast(String content) {
decorator.showToast(content);
}

public static @Nullable Path showDialog(DirectoryChooser directoryChooser) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Declare the required class-level nullability default

These new helper signatures introduce implicitly non-null chooser parameters, but Controllers is not annotated with @NotNullByDefault. This violates the repository AGENTS.md requirement that every class carry that annotation and that nullability never be implicit.

Useful? React with 👍 / 👎.

return FileUtils.toPath(directoryChooser.showDialog(stage));
}
Comment on lines +647 to +649

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Add required documentation to the new dialog helpers

The repository AGENTS.md requires every method to have /// Markdown-style documentation, but this and the other three newly introduced dialog helper methods have no documentation describing their owner behavior, nullable cancellation result, or side effects.

Useful? React with 👍 / 👎.


public static @Nullable Path showOpenDialog(FileChooser fileChooser) {
return FileUtils.toPath(fileChooser.showOpenDialog(stage));
}

public static @Nullable Path showSaveDialog(FileChooser fileChooser) {
return FileUtils.toPath(fileChooser.showSaveDialog(stage));
}

public static @Nullable List<Path> showOpenMultipleDialog(FileChooser fileChooser) {
return FileUtils.toPaths(fileChooser.showOpenMultipleDialog(stage));
}

public static void onHyperlinkAction(String href) {
if (href.startsWith("hmcl://")) {
switch (href) {
Expand Down
4 changes: 2 additions & 2 deletions HMCL/src/main/java/org/jackhuang/hmcl/ui/UpgradeDialog.java
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,8 @@
public final class UpgradeDialog extends JFXDialogLayout {

public UpgradeDialog(RemoteVersion remoteVersion, Runnable updateRunnable) {
maxWidthProperty().bind(Controllers.getScene().widthProperty().multiply(0.7));
maxHeightProperty().bind(Controllers.getScene().heightProperty().multiply(0.7));
maxWidthProperty().bind(Controllers.windowWidthProperty().multiply(0.7));
maxHeightProperty().bind(Controllers.windowHeightProperty().multiply(0.7));

setHeading(new Label(i18n("update.changelog")));
setBody(new JFXSpinner());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,6 @@
import org.jackhuang.hmcl.ui.DialogController;
import org.jackhuang.hmcl.ui.construct.MessageDialogPane.MessageType;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.skin.InvalidSkinException;
import org.jackhuang.hmcl.util.skin.NormalizedSkin;
import org.jetbrains.annotations.Nullable;
Expand Down Expand Up @@ -151,7 +150,7 @@ public Task<?> uploadSkin() {
FileChooser chooser = new FileChooser();
chooser.setTitle(i18n("account.skin.upload"));
chooser.getExtensionFilters().add(new FileChooser.ExtensionFilter(i18n("account.skin.file"), "*.png"));
Path selectedFile = FileUtils.toPath(chooser.showOpenDialog(Controllers.getStage()));
Path selectedFile = Controllers.showOpenDialog(chooser);
if (selectedFile == null) {
return null;
}
Expand Down
Loading
Loading