diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/setting/Profile.java b/HMCL/src/main/java/org/jackhuang/hmcl/setting/Profile.java index 78724cea0..0c44eb222 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/setting/Profile.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/setting/Profile.java @@ -19,6 +19,9 @@ package org.jackhuang.hmcl.setting; import com.google.gson.*; import javafx.beans.InvalidationListener; +import javafx.beans.property.BooleanProperty; +import javafx.beans.property.SimpleBooleanProperty; + import org.jackhuang.hmcl.game.HMCLDependencyManager; import org.jackhuang.hmcl.game.HMCLGameRepository; import org.jackhuang.hmcl.mod.ModManager; @@ -83,8 +86,18 @@ public final class Profile { nameProperty.set(name); } - public Profile() { - this("Default"); + private BooleanProperty useRelativePathProperty = new SimpleBooleanProperty(this, "useRelativePath", false); + + public BooleanProperty useRelativePathProperty() { + return useRelativePathProperty(); + } + + public boolean isUseRelativePath() { + return useRelativePathProperty.get(); + } + + public void setUseRelativePath(boolean useRelativePath) { + useRelativePathProperty.set(useRelativePath); } public Profile(String name) { @@ -153,6 +166,7 @@ public final class Profile { return new ToStringBuilder(this) .append("gameDir", getGameDir()) .append("name", getName()) + .append("useRelativePath", isUseRelativePath()) .toString(); } @@ -160,6 +174,7 @@ public final class Profile { nameProperty.addListener(listener); globalProperty.addListener(listener); gameDirProperty.addListener(listener); + useRelativePathProperty.addListener(listener); } public static final class Serializer implements JsonSerializer, JsonDeserializer { @@ -176,6 +191,7 @@ public final class Profile { JsonObject jsonObject = new JsonObject(); jsonObject.add("global", context.serialize(src.getGlobal())); jsonObject.addProperty("gameDir", src.getGameDir().getPath()); + jsonObject.addProperty("useRelativePath", src.isUseRelativePath()); return jsonObject; } @@ -188,6 +204,8 @@ public final class Profile { Profile profile = new Profile("Default", new File(gameDir)); profile.setGlobal(context.deserialize(obj.get("global"), VersionSetting.class)); + + profile.setUseRelativePath(Optional.ofNullable(obj.get("useRelativePath")).map(JsonElement::getAsBoolean).orElse(false)); return profile; } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/setting/Settings.java b/HMCL/src/main/java/org/jackhuang/hmcl/setting/Settings.java index 43e8411f8..378f25ade 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/setting/Settings.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/setting/Settings.java @@ -512,8 +512,12 @@ public class Settings { private void checkProfileMap() { if (getProfileMap().isEmpty()) { - getProfileMap().put(Profiles.DEFAULT_PROFILE, new Profile(Profiles.DEFAULT_PROFILE)); - getProfileMap().put(Profiles.HOME_PROFILE, new Profile(Profiles.HOME_PROFILE, Launcher.MINECRAFT_DIRECTORY)); + Profile current = new Profile(Profiles.DEFAULT_PROFILE); + current.setUseRelativePath(true); + getProfileMap().put(Profiles.DEFAULT_PROFILE, current); + + Profile home = new Profile(Profiles.HOME_PROFILE, Launcher.MINECRAFT_DIRECTORY); + getProfileMap().put(Profiles.HOME_PROFILE, home); } } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/ProfilePage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/ProfilePage.java index 8241fd976..deface84d 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/ProfilePage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/ProfilePage.java @@ -18,6 +18,7 @@ package org.jackhuang.hmcl.ui; import com.jfoenix.controls.JFXButton; +import com.jfoenix.controls.JFXCheckBox; import com.jfoenix.controls.JFXTextField; import javafx.beans.property.SimpleStringProperty; import javafx.beans.property.StringProperty; @@ -40,12 +41,11 @@ public final class ProfilePage extends StackPane implements DecoratorPage { private final StringProperty location; private final Profile profile; - @FXML - private JFXTextField txtProfileName; - @FXML - private FileItem gameDir; + @FXML private JFXTextField txtProfileName; + @FXML private FileItem gameDir; @FXML private JFXButton btnSave; @FXML private JFXButton btnDelete; + @FXML private JFXCheckBox toggleUseRelativePath; /** * @param profile null if creating a new profile. @@ -69,9 +69,12 @@ public final class ProfilePage extends StackPane implements DecoratorPage { FXUtils.onChangeAndOperate(location, it -> { btnSave.setDisable(!txtProfileName.validate() || StringUtils.isBlank(getLocation())); }); - - if (profile == null) + gameDir.convertToRelativePathProperty().bind(toggleUseRelativePath.selectedProperty()); + if (profile == null) { btnDelete.setVisible(false); + } else { + toggleUseRelativePath.setSelected(profile.isUseRelativePath()); + } } @FXML @@ -86,13 +89,17 @@ public final class ProfilePage extends StackPane implements DecoratorPage { private void onSave() { if (profile != null) { profile.setName(txtProfileName.getText()); - if (StringUtils.isNotBlank(getLocation())) + profile.setUseRelativePath(toggleUseRelativePath.isSelected()); + if (StringUtils.isNotBlank(getLocation())) { profile.setGameDir(new File(getLocation())); + } } else { if (StringUtils.isBlank(getLocation())) { gameDir.onExplore(); } - Settings.INSTANCE.putProfile(new Profile(txtProfileName.getText(), new File(getLocation()))); + Profile newProfile = new Profile(txtProfileName.getText(), new File(getLocation())); + newProfile.setUseRelativePath(toggleUseRelativePath.isSelected()); + Settings.INSTANCE.putProfile(newProfile); } Settings.INSTANCE.onProfileLoading(); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/FileItem.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/FileItem.java index 35a341f77..474a3c431 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/FileItem.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/FileItem.java @@ -18,6 +18,9 @@ package org.jackhuang.hmcl.ui.construct; import com.jfoenix.controls.JFXButton; + +import javafx.beans.property.BooleanProperty; +import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.property.SimpleStringProperty; import javafx.beans.property.StringProperty; import javafx.scene.control.Label; @@ -30,9 +33,13 @@ import org.jackhuang.hmcl.setting.Theme; import org.jackhuang.hmcl.ui.Controllers; import org.jackhuang.hmcl.ui.FXUtils; import org.jackhuang.hmcl.ui.SVG; + +import static org.jackhuang.hmcl.ui.FXUtils.onInvalidating; import static org.jackhuang.hmcl.util.i18n.I18n.i18n; import java.io.File; +import java.nio.file.Path; +import java.nio.file.Paths; public class FileItem extends BorderPane { private final Label lblPath = new Label(); @@ -41,6 +48,7 @@ public class FileItem extends BorderPane { private final SimpleStringProperty title = new SimpleStringProperty(this, "title"); private final SimpleStringProperty tooltip = new SimpleStringProperty(this, "tooltip"); private final SimpleStringProperty path = new SimpleStringProperty(this, "path"); + private final SimpleBooleanProperty convertToRelativePath = new SimpleBooleanProperty(this, "convertToRelativePath"); public FileItem() { VBox left = new VBox(); @@ -61,6 +69,23 @@ public class FileItem extends BorderPane { Tooltip tip = new Tooltip(); tip.textProperty().bind(tooltipProperty()); Tooltip.install(this, tip); + + convertToRelativePath.addListener(onInvalidating(() -> path.set(processPath(path.get())))); + } + + /** + * Converts the given path to absolute/relative(if possible) path according to {@link #convertToRelativePathProperty()}. + */ + private String processPath(String path) { + Path given = Paths.get(path).toAbsolutePath(); + if (isConvertToRelativePath()) { + try { + return Paths.get(".").toAbsolutePath().relativize(given).normalize().toString(); + } catch (IllegalArgumentException e) { + // the given path can't be relativized against current path + } + } + return given.normalize().toString(); } public void onExplore() { @@ -77,8 +102,9 @@ public class FileItem extends BorderPane { } chooser.titleProperty().bind(titleProperty()); File selectedDir = chooser.showDialog(Controllers.getStage()); - if (selectedDir != null) - path.set(selectedDir.getAbsolutePath()); + if (selectedDir != null) { + path.set(processPath(selectedDir.toString())); + } chooser.titleProperty().unbind(); } @@ -129,4 +155,16 @@ public class FileItem extends BorderPane { public void setPath(String path) { this.path.set(path); } + + public boolean isConvertToRelativePath() { + return convertToRelativePath.get(); + } + + public BooleanProperty convertToRelativePathProperty() { + return convertToRelativePath; + } + + public void setConvertToRelativePath(boolean convertToRelativePath) { + this.convertToRelativePath.set(convertToRelativePath); + } } diff --git a/HMCL/src/main/resources/assets/fxml/profile.fxml b/HMCL/src/main/resources/assets/fxml/profile.fxml index 9c2f80c87..8436678a9 100644 --- a/HMCL/src/main/resources/assets/fxml/profile.fxml +++ b/HMCL/src/main/resources/assets/fxml/profile.fxml @@ -1,11 +1,13 @@ + + + + + + + + diff --git a/HMCL/src/main/resources/assets/lang/I18N.properties b/HMCL/src/main/resources/assets/lang/I18N.properties index 389c3b3ff..290cf8f86 100644 --- a/HMCL/src/main/resources/assets/lang/I18N.properties +++ b/HMCL/src/main/resources/assets/lang/I18N.properties @@ -240,6 +240,7 @@ profile.instance_directory.choose=Choose Game Directory profile.new=New Config profile.title=Game Directories profile.selected=Selected +profile.use_relative_path=Use relative path for game directory if possible selector.choose=Choose selector.choose_file=Select a file diff --git a/HMCL/src/main/resources/assets/lang/I18N_zh.properties b/HMCL/src/main/resources/assets/lang/I18N_zh.properties index 6c39171ab..403fd3fb3 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh.properties @@ -240,6 +240,7 @@ profile.instance_directory.choose=選擇遊戲路徑 profile.new=新建配置 profile.title=遊戲目錄 profile.selected=已選中 +profile.use_relative_path=若可能,遊戲目錄使用相對路徑 selector.choose=選擇 selector.choose_file=選擇文件 diff --git a/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties b/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties index 79810f5f9..86e033a04 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties @@ -240,6 +240,7 @@ profile.instance_directory.choose=选择游戏路径 profile.new=新建配置 profile.title=游戏目录 profile.selected=已选中 +profile.use_relative_path=若可能,游戏目录使用相对路径 selector.choose=选择 selector.choose_file=选择文件