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
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(initX);
contentY.set(initY);
}

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
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ private void openFileChooser(JFXTextField customField) {
FileChooser chooser = new FileChooser();
chooser.getExtensionFilters().addAll(getExtensionFilters());
chooser.setTitle(StringUtils.isBlank(chooserTitle) ? i18n("selector.choose_file") : chooserTitle);
Path file = FileUtils.toPath(chooser.showOpenDialog(Controllers.getStage()));
Path file = Controllers.showOpenDialog(chooser);
if (file != null) {
String path = FileUtils.getAbsolutePath(file);
customField.setText(path);
Expand All @@ -131,7 +131,7 @@ private void openFileChooser(JFXTextField customField) {
private void openDirectoryChooser(JFXTextField customField) {
DirectoryChooser chooser = new DirectoryChooser();
chooser.setTitle(StringUtils.isBlank(chooserTitle) ? i18n("selector.choose_directory") : chooserTitle);
Path dir = FileUtils.toPath(chooser.showDialog(Controllers.getStage()));
Path dir = Controllers.showDialog(chooser);
if (dir != null) {
String path = FileUtils.getAbsolutePath(dir);
customField.setText(path);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,11 @@
import javafx.beans.property.*;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.Scene;
import javafx.stage.DirectoryChooser;
import javafx.stage.FileChooser;
import javafx.stage.Stage;
import javafx.stage.Window;
import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.SVG;
import org.jackhuang.hmcl.util.io.FileUtils;

Expand Down Expand Up @@ -57,7 +57,9 @@ private String processPath(Path path) {
public void fire() {
super.fire();

Stage owner = Controllers.getStage(); // TODO: Allow user to set owner stage
Scene scene = this.getScene();

Window owner = scene != null ? scene.getWindow() : null; // TODO: Allow user to set owner stage
String windowTitle = getFileChooserTitle();

Path initialDirectory = null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ public LocalModpackPage(WizardController controller) {
FileChooser chooser = new FileChooser();
chooser.setTitle(i18n("modpack.choose"));
chooser.getExtensionFilters().add(new FileChooser.ExtensionFilter(i18n("modpack"), "*.zip"));
selectedFile = FileUtils.toPath(chooser.showOpenDialog(Controllers.getStage()));
selectedFile = Controllers.showOpenDialog(chooser);
if (selectedFile == null) {
controller.onEnd();
return;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,6 @@
import org.jackhuang.hmcl.util.SettingsMap;
import org.jackhuang.hmcl.util.TaskCancellationAction;
import org.jackhuang.hmcl.util.gson.JsonUtils;
import org.jackhuang.hmcl.util.io.FileUtils;

import java.io.IOException;
import java.nio.file.Files;
Expand Down Expand Up @@ -112,7 +111,7 @@ private void onChooseLocalFile() {
FileChooser chooser = new FileChooser();
chooser.setTitle(i18n("modpack.choose"));
chooser.getExtensionFilters().add(new FileChooser.ExtensionFilter(i18n("modpack"), "*.zip", "*.mrpack"));
Path selectedFile = FileUtils.toPath(chooser.showOpenDialog(Controllers.getStage()));
Path selectedFile = Controllers.showOpenDialog(chooser);
if (selectedFile == null) {
Platform.runLater(controller::onEnd);
return;
Expand Down
Loading
Loading