diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLGameRepository.java b/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLGameRepository.java index 015edc008..b0bfb3d5c 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLGameRepository.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLGameRepository.java @@ -38,7 +38,6 @@ import org.jackhuang.hmcl.util.Lang; import org.jackhuang.hmcl.util.StringUtils; import org.jackhuang.hmcl.util.gson.JsonUtils; import org.jackhuang.hmcl.util.io.FileUtils; -import org.jackhuang.hmcl.util.javafx.PropertyUtils; import org.jackhuang.hmcl.java.JavaRuntime; import org.jackhuang.hmcl.util.platform.OperatingSystem; import org.jackhuang.hmcl.util.versioning.VersionNumber; @@ -359,12 +358,9 @@ public class HMCLGameRepository extends DefaultGameRepository { vs = createLocalVersionSetting(id); if (vs == null) return null; - VersionIconType versionIcon = vs.getVersionIcon(); if (vs.isUsesGlobal()) { - PropertyUtils.copyProperties(profile.getGlobal(), vs); vs.setUsesGlobal(false); } - vs.setVersionIcon(versionIcon); // versionIcon is preserved return vs; } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/FXUtils.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/FXUtils.java index 20efb21eb..cf04ce7bd 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/FXUtils.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/FXUtils.java @@ -24,6 +24,7 @@ import javafx.application.Platform; import javafx.beans.InvalidationListener; import javafx.beans.Observable; import javafx.beans.WeakInvalidationListener; +import javafx.beans.WeakListener; import javafx.beans.property.BooleanProperty; import javafx.beans.property.Property; import javafx.beans.value.*; @@ -79,6 +80,7 @@ import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; import java.io.*; +import java.lang.ref.WeakReference; import java.net.*; import java.nio.file.Files; import java.nio.file.Path; @@ -636,26 +638,100 @@ public final class FXUtils { checkBox.selectedProperty().unbindBidirectional(property); } + private static final class EnumBidirectionalBinding> implements InvalidationListener, WeakListener { + private final WeakReference> comboBoxRef; + private final WeakReference> propertyRef; + private final int hashCode; + + private boolean updating = false; + + private EnumBidirectionalBinding(JFXComboBox comboBox, Property property) { + this.comboBoxRef = new WeakReference<>(comboBox); + this.propertyRef = new WeakReference<>(property); + this.hashCode = System.identityHashCode(comboBox) ^ System.identityHashCode(property); + } + + @Override + public void invalidated(Observable sourceProperty) { + if (!updating) { + final JFXComboBox comboBox = comboBoxRef.get(); + final Property property = propertyRef.get(); + + if (comboBox == null || property == null) { + if (comboBox != null) { + comboBox.getSelectionModel().selectedItemProperty().removeListener(this); + } + + if (property != null) { + property.removeListener(this); + } + } else { + updating = true; + try { + if (property == sourceProperty) { + E newValue = property.getValue(); + comboBox.getSelectionModel().select(newValue); + } else { + E newValue = comboBox.getSelectionModel().getSelectedItem(); + property.setValue(newValue); + } + } finally { + updating = false; + } + } + } + } + + @Override + public boolean wasGarbageCollected() { + return comboBoxRef.get() == null || propertyRef.get() == null; + } + + @Override + public int hashCode() { + return hashCode; + } + + @Override + public boolean equals(Object o) { + if (this == o) + return true; + if (!(o instanceof EnumBidirectionalBinding)) + return false; + + EnumBidirectionalBinding that = (EnumBidirectionalBinding) o; + + final JFXComboBox comboBox = this.comboBoxRef.get(); + final Property property = this.propertyRef.get(); + + final JFXComboBox thatComboBox = that.comboBoxRef.get(); + final Property thatProperty = that.propertyRef.get(); + + if (comboBox == null || property == null || thatComboBox == null || thatProperty == null) + return false; + + return comboBox == thatComboBox && property == thatProperty; + } + } + /** * Bind combo box selection with given enum property bidirectionally. * You should only and always use {@code bindEnum} as well as {@code unbindEnum} at the same time. * * @param comboBox the combo box being bound with {@code property}. * @param property the property being bound with {@code combo box}. - * @see #unbindEnum(JFXComboBox) + * @see #unbindEnum(JFXComboBox, Property) * @see ExtendedProperties#selectedItemPropertyFor(ComboBox) */ public static > void bindEnum(JFXComboBox comboBox, Property property) { - unbindEnum(comboBox); + EnumBidirectionalBinding binding = new EnumBidirectionalBinding<>(comboBox, property); - T currentValue = property.getValue(); - @SuppressWarnings("unchecked") - T[] enumConstants = (T[]) currentValue.getClass().getEnumConstants(); - ChangeListener listener = (a, b, newValue) -> property.setValue(enumConstants[newValue.intValue()]); + comboBox.getSelectionModel().selectedItemProperty().removeListener(binding); + property.removeListener(binding); - comboBox.getSelectionModel().select(currentValue.ordinal()); - comboBox.getProperties().put("FXUtils.bindEnum.listener", listener); - comboBox.getSelectionModel().selectedIndexProperty().addListener(listener); + comboBox.getSelectionModel().select(property.getValue()); + comboBox.getSelectionModel().selectedItemProperty().addListener(binding); + property.addListener(binding); } /** @@ -666,11 +742,10 @@ public final class FXUtils { * @see #bindEnum(JFXComboBox, Property) * @see ExtendedProperties#selectedItemPropertyFor(ComboBox) */ - public static void unbindEnum(JFXComboBox> comboBox) { - @SuppressWarnings("unchecked") - ChangeListener listener = (ChangeListener) comboBox.getProperties().remove("FXUtils.bindEnum.listener"); - if (listener != null) - comboBox.getSelectionModel().selectedIndexProperty().removeListener(listener); + public static > void unbindEnum(JFXComboBox comboBox, Property property) { + EnumBidirectionalBinding binding = new EnumBidirectionalBinding<>(comboBox, property); + comboBox.getSelectionModel().selectedItemProperty().removeListener(binding); + property.removeListener(binding); } public static void bindAllEnabled(BooleanProperty allEnabled, BooleanProperty... children) { diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/AdvancedVersionSettingPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/AdvancedVersionSettingPage.java index c2c5adfe5..5c318feea 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/AdvancedVersionSettingPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/AdvancedVersionSettingPage.java @@ -240,7 +240,7 @@ public final class AdvancedVersionSettingPage extends StackPane implements Decor FXUtils.unbind(txtWrapper, versionSetting.wrapperProperty()); FXUtils.unbind(txtPreLaunchCommand, versionSetting.preLaunchCommandProperty()); FXUtils.unbind(txtPostExitCommand, versionSetting.postExitCommandProperty()); - FXUtils.unbindEnum(cboRenderer); + FXUtils.unbindEnum(cboRenderer, versionSetting.rendererProperty()); noGameCheckPane.selectedProperty().unbindBidirectional(versionSetting.notCheckGameProperty()); noJVMCheckPane.selectedProperty().unbindBidirectional(versionSetting.notCheckJVMProperty()); noJVMArgsPane.selectedProperty().unbindBidirectional(versionSetting.noJVMArgsProperty()); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/VersionSettingsPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/VersionSettingsPage.java index 506b1bc55..bf9b96a08 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/VersionSettingsPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/VersionSettingsPage.java @@ -46,6 +46,7 @@ import org.jackhuang.hmcl.ui.decorator.DecoratorPage; import org.jackhuang.hmcl.util.Lang; import org.jackhuang.hmcl.util.Pair; import org.jackhuang.hmcl.util.javafx.BindingMapping; +import org.jackhuang.hmcl.util.javafx.PropertyUtils; import org.jackhuang.hmcl.util.javafx.SafeStringConverter; import org.jackhuang.hmcl.util.platform.Architecture; import org.jackhuang.hmcl.java.JavaRuntime; @@ -65,6 +66,7 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag private static final ObjectProperty memoryStatus = new SimpleObjectProperty<>(OperatingSystem.PhysicalMemoryStatus.INVALID); private static TimerTask memoryStatusUpdateTask; + private static void initMemoryStatusUpdateTask() { FXUtils.checkFxUserThread(); if (memoryStatusUpdateTask != null) @@ -186,6 +188,30 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag componentList = new ComponentList(); componentList.setDepth(1); + if (!globalSetting) { + BorderPane copyGlobalPane = new BorderPane(); + { + Label label = new Label(i18n("settings.game.copy_global")); + copyGlobalPane.setLeft(label); + BorderPane.setAlignment(label, Pos.CENTER_LEFT); + + JFXButton button = new JFXButton(i18n("settings.game.copy_global.copy_all")); + copyGlobalPane.setRight(button); + button.setOnAction(e -> Controllers.confirm(i18n("settings.game.copy_global.copy_all.confirm"), null, () -> { + Set ignored = new HashSet<>(Arrays.asList( + "usesGlobal", + "versionIcon" + )); + + PropertyUtils.copyProperties(profile.getGlobal(), lastVersionSetting, name -> !ignored.contains(name)); + }, null)); + button.getStyleClass().add("jfx-button-border"); + BorderPane.setAlignment(button, Pos.CENTER_RIGHT); + } + + componentList.getContent().add(copyGlobalPane); + } + javaItem = new MultiFileItem<>(); javaSublist = new ComponentSublist(); javaSublist.getContent().add(javaItem); @@ -442,7 +468,7 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag showAdvancedSettingPane.setRight(button); } - componentList.getContent().setAll( + componentList.getContent().addAll( javaSublist, gameDirSublist, maxMemoryPane, @@ -527,8 +553,8 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag FXUtils.unbindBoolean(chkAutoAllocate, lastVersionSetting.autoMemoryProperty()); FXUtils.unbindBoolean(chkFullscreen, lastVersionSetting.fullscreenProperty()); showLogsPane.selectedProperty().unbindBidirectional(lastVersionSetting.showLogsProperty()); - FXUtils.unbindEnum(cboLauncherVisibility); - FXUtils.unbindEnum(cboProcessPriority); + FXUtils.unbindEnum(cboLauncherVisibility, lastVersionSetting.launcherVisibilityProperty()); + FXUtils.unbindEnum(cboProcessPriority, lastVersionSetting.processPriorityProperty()); lastVersionSetting.usesGlobalProperty().removeListener(usesGlobalListener); lastVersionSetting.javaVersionTypeProperty().removeListener(javaListener); diff --git a/HMCL/src/main/resources/assets/lang/I18N.properties b/HMCL/src/main/resources/assets/lang/I18N.properties index 33cf23ef6..e375f658c 100644 --- a/HMCL/src/main/resources/assets/lang/I18N.properties +++ b/HMCL/src/main/resources/assets/lang/I18N.properties @@ -1206,6 +1206,9 @@ settings.advanced.wrapper_launcher.prompt=Allows launching using an extra wrappe settings.custom=Custom settings.game=Settings +settings.game.copy_global=Copy from Global Settings +settings.game.copy_global.copy_all=Copy All +settings.game.copy_global.copy_all.confirm=Are you sure you want to overwrite the current instance settings? This action cannot be undone! settings.game.current=Game settings.game.dimension=Resolution settings.game.exploration=Explore diff --git a/HMCL/src/main/resources/assets/lang/I18N_zh.properties b/HMCL/src/main/resources/assets/lang/I18N_zh.properties index be5ded78d..7e4088583 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh.properties @@ -1003,6 +1003,9 @@ settings.advanced.wrapper_launcher.prompt=如填寫「optirun」後,啟動指 settings.custom=自訂 settings.game=遊戲設定 +settings.game.copy_global=複製全域遊戲設定 +settings.game.copy_global.copy_all=複製全部 +settings.game.copy_global.copy_all.confirm=你確定要覆寫目前實例特定遊戲設定嗎?該操作無法復原! settings.game.current=遊戲 settings.game.dimension=遊戲介面解析度大小 settings.game.exploration=瀏覽 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 617cc57d8..3a5d6f190 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties @@ -1014,6 +1014,9 @@ settings.advanced.wrapper_launcher.prompt=如填写“optirun”后,启动命 settings.custom=自定义 settings.game=游戏设置 +settings.game.copy_global=复制全局游戏设置 +settings.game.copy_global.copy_all=复制全部 +settings.game.copy_global.copy_all.confirm=你确定要覆盖当前版本特定游戏设置吗?此操作无法撤销! settings.game.current=游戏 settings.game.dimension=游戏窗口分辨率 settings.game.exploration=浏览 diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/javafx/PropertyUtils.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/javafx/PropertyUtils.java index 72d8b3002..eddc9d50f 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/javafx/PropertyUtils.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/javafx/PropertyUtils.java @@ -23,6 +23,7 @@ import java.util.List; import java.util.Map; import java.util.Set; import java.util.function.Function; +import java.util.function.Predicate; import javafx.beans.InvalidationListener; import javafx.beans.Observable; @@ -158,6 +159,21 @@ public final class PropertyUtils { }); } + public static void copyProperties(Object from, Object to, Predicate predicate) { + Class type = from.getClass(); + while (!type.isInstance(to)) + type = type.getSuperclass(); + + getPropertyHandleFactories(type) + .forEach((name, factory) -> { + if (predicate.test(name)) { + PropertyHandle src = factory.apply(from); + PropertyHandle target = factory.apply(to); + target.accessor.setValue(src.accessor.getValue()); + } + }); + } + public static void attachListener(Object instance, InvalidationListener listener) { getPropertyHandleFactories(instance.getClass()) .forEach((name, factory) -> factory.apply(instance).observable.addListener(listener));